Commit 8e056d72 authored by Daniele Tafani's avatar Daniele Tafani
Browse files

Working HTTP server for Grafana with HTTP basic authentication support.

parent e6873872
......@@ -99,7 +99,7 @@ bool Configuration::readGlobal() {
_global.restAPISettings.privateKey = global.second.data();
} else if (boost::iequals(global.first, "dhFile")) {
_global.restAPISettings.dhFile = global.second.data();
} else if (boost::iequals(global.first, "authkey")) {
} else if (boost::iequals(global.first, "user")) {
//Avoid unnecessary "Value not recognized" message
} else {
LOG(warning) << " Value \"" << global.first << "\" not recognized. Omitting";
......@@ -109,7 +109,8 @@ bool Configuration::readGlobal() {
return true;
}
bool Configuration::readAuthkeys(HttpsServer* server) {
bool Configuration::checkCredentials(std::string credentials) {
//open file
std::string globalConfig = _cfgFilePath;
globalConfig.append("global.conf");
......@@ -119,44 +120,33 @@ bool Configuration::readAuthkeys(HttpsServer* server) {
try {
boost::property_tree::read_info(globalConfig, cfg);
} catch (boost::property_tree::info_parser_error& e) {
LOG(error) << "Error when reading authkeys from global.conf: " << e.what();
LOG(error) << "Error when reading credentials from global.conf: " << e.what();
return false;
}
//read authkeys
//read credentials
BOOST_FOREACH(boost::property_tree::iptree::value_type &global, cfg.get_child("restAPI")) {
if (boost::iequals(global.first, "authkey")) {
#ifdef DEBUG
LOG(info) << "Authentication token \"" << global.second.data() << "\"";
#endif
std::bitset<NUM_PERMISSIONS> permissions;
BOOST_FOREACH(boost::property_tree::iptree::value_type &perm, global.second) {
if (boost::iequals(perm.first, "GETReq")) {
#ifdef DEBUG
LOG(info) << " Permission \"GETReq\"";
#endif
permissions[GETReq] = true;
} else if (boost::iequals(perm.first, "PUTReq")) {
#ifdef DEBUG
LOG(info) << " Permission \"PUTReq\"";
#endif
permissions[PUTReq] = true;
} else {
#ifdef DEBUG
LOG(warning) << "Permission \"" << perm.first << "\" not recognized. Omitting";
#endif
if (boost::iequals(global.first, "user")) {
//Check user
if(boost::iequals(credentials.substr(0, credentials.find(":")), global.second.data())) {
BOOST_FOREACH(boost::property_tree::iptree::value_type &pass, global.second) {
if (boost::iequals(pass.first, "password")) {
//Check password
if(boost::iequals(credentials.substr(credentials.find(":")+1), pass.second.data())) {
return true;
}
}
}
}
if (!server->addAuthkey(authkeyMap_t::value_type(global.second.data(), permissions))) {
#ifdef DEBUG
LOG(warning) << "Authkey already present!";
#endif
}
} else {
//
}
}
return true;
return false;
}
global_t& Configuration::getGlobal() {
......
......@@ -10,8 +10,12 @@
#define Configuration_h
#include <stdio.h>
#include <utility>
#include <map>
#include <bitset>
#include <set>
#include "httpserver.h"
#include "HTTPServer.h"
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/log/trivial.hpp>
......@@ -52,12 +56,12 @@ public:
bool readGlobal();
/**
* Reads the authentication keys and forwards them to the HttpsServer
* Check the credentials to connect to the Grafana server
*
* @param server The Rest API server where to add the authkeys
* @param credentials the string username:password
* @return true on success, false otherwise
*/
bool readAuthkeys(HttpsServer* server);
bool checkCredentials(std::string credentials);
/*
* Return global settings
......
//
// Server.h
// httpserver
// HTTPServer.h
//
// The header file of HTTPServer.cpp.
//
// Created by Tafani, Daniele on 27.09.18.
// Copyright © 2018 LRZ. All rights reserved.
......@@ -11,8 +12,8 @@
#include "Logging.h"
#include "cassandra.h"
#ifndef Server_h
#define Server_h
#ifndef HTTPServer_h
#define HTTPServer_h
#pragma once
......@@ -23,6 +24,8 @@
#include <iostream>
#include <fstream>
#include <random>
#include <locale>
#include <ctime>
#ifdef _WIN32
#define NOMINMAX
......@@ -31,6 +34,13 @@
# include <sys/time.h>
#endif
//Headers from Boost
#include <boost/thread/condition.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>
#include <asio/ssl.hpp>
//Headers from cpprestsdk
#include "cpprest/json.h"
#include "cpprest/http_listener.h"
#include "cpprest/uri.h"
......@@ -40,6 +50,7 @@
#include "cpprest/containerstream.h"
#include "cpprest/producerconsumerstream.h"
//Headers from dcdb
#include "dcdb/connection.h"
#include "dcdb/timestamp.h"
#include "dcdb/sensorid.h"
......@@ -47,37 +58,140 @@
#include "dcdb/sensorconfig.h"
#include "dcdb/sensor.h"
#pragma warning ( push )
#pragma warning ( disable : 4457 )
#pragma warning ( pop )
#include <locale>
#include <ctime>
using namespace std;
using namespace web;
using namespace http;
using namespace utility;
using namespace http::experimental::listener;
/**
* Struct defining the REST API Settings.
* It holds connection settings of the HTTP server (hostname and port) and SSL settings (certificate, private key, dhFile).
*/
typedef struct {
std::string restHost;
std::string restPort;
std::string certificate;
std::string privateKey;
std::string dhFile;
} restAPISettings_t;
/**
* Struct defining the Cassandra Settings.
* It holds connection settings of the Apache Cassandra server (hostname and port).
*/
typedef struct {
std::string cassandraHost;
uint16_t cassandraPort;
} cassandraSettings_t;
/**
* This class implements the behaviour of a request handler of the REST server.
*/
class handler
{
public:
/**
* The default constructor of the class.
*/
handler();
/**
* The constructor of the class with URL of the REST server.
* @param url
*/
handler(utility::string_t url);
/**
* The destructor of the class.
*/
virtual ~handler();
pplx::task<void>open(){return m_listener.open();}
pplx::task<void>close(){return m_listener.close();}
protected:
private:
/**
* This function handles HTTP GET requests.
* It is only used once to handle a Grafana request to define a data source.
*
* @param message: the HTTP message containing the GET request.
*/
void handle_get(http_request message);
/**
* This function handles HTTP PUT requests.
* We do not use this handler in Grafana for the moment.
*
* @param message: the HTTP message containing the PUT request.
*/
void handle_put(http_request message);
/**
* This function handles HTTP POST requests.
* It is used to handle Grafana queries (for sensor names and data).
*
* @param message: the HTTP message containing the POST request.
*/
void handle_post(http_request message);
/**
* This function handles HTTP DELETE requests.
* Not used at the moment.
*
* @param message: the HTTP message containing the DELETE request.
*/
void handle_delete(http_request message);
void handle_error(pplx::task<void>& t);
/**
* This function decodes base64 string for HTTP basic authentication.
*
* @param input: the base64 string to be decoded.
*/
std::string decode(std::string input);
/**
* The HTTP listener of the handler that supports different HTTP requests.
*
*/
http_listener m_listener;
};
#endif /* Server_h */
/*
* Provides REST API services over configurable host and port.
*/
class HttpsServer {
public:
HttpsServer(restAPISettings_t restAPISettings, cassandraSettings_t cassandraSettings, boost::asio::io_service& io);
virtual ~HttpsServer();
/*
* Starts the handler of the server.
*/
void run() {
_handler->open().wait();
}
/*
* Stops the handler of the server.
*/
void stop() {
_handler->close().wait();
}
private:
std::unique_ptr<handler> _handler;
boost::log::sources::severity_logger<boost::log::trivial::severity_level> lg;
};
#endif /* HTTPServer_h */
include ../config.mk
CXXFLAGS = -O2 -g --std=c++11 -Wall -Wno-unused-local-typedefs -Wno-deprecated-declarations -Wno-unknown-warning-option -fmessage-length=0 -I../include/ -I../lib/include -I$(DCDBDEPLOYPATH)/include -DBOOST_DATE_TIME_POSIX_TIME_STD_CONFIG -I$(DCDBDEPSPATH)/cpp-netlib-0.12.0-final/deps/asio/asio/include -I$(DCDBDEPSPATH)/cpp-netlib-0.12.0-final -I$(DCDBDEPSPATH)/cpp-netlib-0.12.0-final/deps/cxxopts/src -DASIO_HEADER_ONLY -DBOOST_TEST_DYN_LINK
CXXFLAGS = -O2 -g --std=c++11 -Wall -Wno-unused-local-typedefs -Wno-deprecated-declarations -Wno-unknown-warning-option -fmessage-length=0 -I../include/ -I../lib/include -I$(DCDBDEPLOYPATH)/include -DBOOST_DATE_TIME_POSIX_TIME_STD_CONFIG -DBOOST_LOG_DYN_LINK -I$(DCDBDEPSPATH)/cpp-netlib-0.12.0-final/deps/asio/asio/include -DBOOST_TEST_DYN_LINK
OBJS = httpserver.o \
Configuration.o \
Logging.o
OBJS = HTTPServer.o \
Configuration.o
LIBS = -L$(DCDBDEPLOYPATH)/lib/ -L../lib -ldcdb -lcassandra -lboost_system -lboost_thread -lboost_log_setup -lboost_log -lpthread -lcrypto -lssl -lcppnetlib-server-parsers -lcppnetlib-uri
TARGET = httpserver
LIBS = -L$(DCDBDEPLOYPATH)/lib/ -L../lib -ldcdb -lcassandra -lboost_system -lboost_chrono -lboost_thread -lboost_log -lboost_log_setup -lpthread -lcrypto -lssl -lcpprest
TARGET = httpserver
.PHONY : clean install
P = $(shell cd $(DCDBDEPLOYPATH)/lib/ && pwd)
U = $(shell uname)
P = $(shell cd $(DCDBDEPLOYPATH)/lib/ && pwd)
U = $(shell uname)
$(TARGET): $(OBJS)
@LD_LIBRARY_PATH=$(DCDBDEPLOYPATH)/lib:$$LD_LIBRARY_PATH \
......
//
// Server.cpp
// httpserver
//
// Created by Tafani, Daniele on 27.09.18.
// Copyright © 2018 LRZ. All rights reserved.
//
#include "Server.h"
#include <iostream>
using namespace std;
using namespace web;
using namespace http;
using namespace utility;
using namespace http::experimental::listener;
std::unique_ptr<handler> g_httpHandler;
Configuration* _configuration;
HttpsServer* _httpsServer;
DCDB::Connection* cassandraConnection;
handler::handler()
{
//ctor
}
handler::handler(utility::string_t url):m_listener(url)
{
m_listener.support(methods::GET, std::bind(&handler::handle_get, this, std::placeholders::_1));
m_listener.support(methods::PUT, std::bind(&handler::handle_put, this, std::placeholders::_1));
m_listener.support(methods::POST, std::bind(&handler::handle_post, this, std::placeholders::_1));
m_listener.support(methods::DEL, std::bind(&handler::handle_delete, this, std::placeholders::_1));
}
handler::~handler()
{
//dtor
}
void handler::handle_error(pplx::task<void>& t)
{
try
{
t.get();
}
catch(...)
{
// Ignore the error, Log it if a logger is available
}
}
//
// Get Request
// Primarily used for
//
void handler::handle_get(http_request message)
{
ucout << message.to_string() << endl;
auto paths = http::uri::split_path(http::uri::decode(message.relative_uri().path()));
message.relative_uri().path();
//Dbms* d = new Dbms();
//d->connect();
concurrency::streams::fstream::open_istream(U("static/index.html"), std::ios::in).then([=](concurrency::streams::istream is) {
message.reply(status_codes::OK, is, U("text/html")).then([](pplx::task<void> t) {
try{
t.get();
}
catch(...){
//
}
});
}).then([=](pplx::task<void>t) {
try{
t.get();
}
catch(...){
message.reply(status_codes::InternalError,U("INTERNAL ERROR "));
}
});
return;
};
//
// A POST request
//
void handler::handle_post(http_request message) {
ucout << message.to_string() << endl;
std::string response;
http_response resp;
if(message.relative_uri().to_string() == "/search") {
DCDB::SensorConfig sensorConfig(cassandraConnection);
std::list<std::string> publicSensors;
sensorConfig.getPublicSensorNames(publicSensors);
response = "[";
for (std::list<std::string>::iterator it=publicSensors.begin(); it != publicSensors.end(); it++) {
response = response + "\"" + (*it) + "\",";
}
response.pop_back();
response = response + "]";
message.reply(status_codes::OK, response);
}
else if(message.relative_uri().to_string() == "/query") {
//extract JSON from body
pplx::task<json::value> task = message.extract_json(true);
const json::value& jsonValue = task.get();
//get start time as string
std::string startStr = jsonValue.at("range").at("from").as_string();
startStr[startStr.find("T")] = ' ';
startStr.pop_back();
//get end time as string
std::string endStr = jsonValue.at("range").at("to").as_string();
endStr[endStr.find("T")] = ' ';
endStr.pop_back();
//get sensors
std::list<std::string> sensors;
for (int i = 0; i < jsonValue.at("targets").size(); i++)
sensors.push_back(jsonValue.at("targets").at(i).at("target").as_string());
//Prepare query
DCDB::TimeStamp start, end;
start = DCDB::TimeStamp(startStr, false);
end = DCDB::TimeStamp(endStr, false);
DCDB::SensorConfig sensorConfig(cassandraConnection);
json::value output;
uint64_t targetIdx = 0;
for (std::list<std::string>::iterator it = sensors.begin(); it != sensors.end(); it++) {
DCDB::Sensor sensor(cassandraConnection, *it);
//Shoot!
std::list<DCDB::SensorDataStoreReading> results;
sensor.query(results, start, end, DCDB::AGGREGATE_NONE);
//Format response for Grafana
output[targetIdx]["target"] = json::value::string(U(*it)) ;
uint64_t dataIdx = 0;
for (std::list<DCDB::SensorDataStoreReading>::iterator reading = results.begin(); reading != results.end(); reading++) {
std::vector<json::value> elements;
elements.push_back(boost::lexical_cast<double>((*reading).value)/3600000); //just to get Wh as a momentary example
elements.push_back((*reading).timeStamp.getRaw()/1000000); //The Grafana JSON data source plugin accepts only ms for timestamps
output[targetIdx]["datapoints"][dataIdx] = json::value::array(elements);
dataIdx++;
}
targetIdx++;
ucout << output.serialize() << endl;
}
resp.set_body(output);
resp.set_status_code(status_codes::OK);
message.reply(resp);
}
return ;
};
//
// A DELETE request
//
void handler::handle_delete(http_request message)
{
ucout << message.to_string() << endl;
string rep = U("WRITE YOUR OWN DELETE OPERATION");
message.reply(status_codes::OK,rep);
return;
};
//
// A PUT request
//
void handler::handle_put(http_request message)
{
ucout << message.to_string() << endl;
string rep = U("WRITE YOUR OWN PUT OPERATION");
message.reply(status_codes::OK,rep);
return;
};
void on_initialize(const string_t& address)
{
uri_builder uri(address);
auto addr = uri.to_uri().to_string();
g_httpHandler = std::unique_ptr<handler>(new handler(addr));
g_httpHandler->open().wait();
ucout << utility::string_t(U("Listening for requests at: ")) << addr << std::endl;
return;
}
void on_shutdown()
{
g_httpHandler->close().wait();
return;
}
#ifdef _WIN32
int wmain(int argc, wchar_t *argv[])
#else
int main(int argc, char *argv[])
#endif
{
_configuration = new Configuration(argv[argc-1]);
//Read global variables from config file
if(!_configuration->readGlobal()) {
//LOG(fatal) << "Failed to read global configuration!";
std::cerr << "Failed to read global configuration!";
return 1;
}
global_t& globalSettings = _configuration->getGlobal();
cassandraSettings_t& cassandraSettings = globalSettings.cassandraSettings;
restAPISettings_t& restAPISettings = globalSettings.restAPISettings;
/* Connect to the cassandra server */
cassandraConnection = new DCDB::Connection();
cassandraConnection->setHostname(cassandraSettings.cassandraHost);
cassandraConnection->setPort(cassandraSettings.cassandraPort);
if (!cassandraConnection->connect())
std::cerr << "Cannot connect to Cassandra database." << std::endl;
utility::string_t port = U(restAPISettings.restPort);
// if(argc == 2)
// {
// port = argv[1];
// }
utility::string_t address = U("http://" + restAPISettings.restHost + ":");
address.append(port);
on_initialize(address);
std::cout << "Press ENTER to exit." << std::endl;
std::string line;
std::getline(std::cin, line);
on_shutdown();
return 0;
}
......@@ -15,12 +15,12 @@ restAPI {
privateKey /Users/di34bap/Projects/DCDB/deps/openssl-1.0.2l/certs/demo/ca-cert.pem
dhFile /Users/di34bap/Projects/DCDB/deps/openssl-1.0.2l/crypto/dh/dh2048.pem
authkey qwertz {
PUTReq
GETReq
user user1 {
password pass1
}