RESTHttpsServer.cpp 15.7 KB
Newer Older
1
2
3
//================================================================================
// Name        : RESTHttpsServer.cpp
// Author      : Micha Mueller
Micha Müller's avatar
Micha Müller committed
4
// Contact     : info@dcdb.it
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Copyright   : Leibniz Supercomputing Centre
// Description : General HTTPS server implementation intended for RESTful APIs.
//================================================================================

//================================================================================
// This file is part of DCDB (DataCenter DataBase)
// Copyright (C) 2019-2019 Leibniz Supercomputing Centre
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//================================================================================
27
28
29
30
31
32
33
34

#include "RESTHttpsServer.h"

#include <string>

#include <boost/archive/iterators/binary_from_base64.hpp>
#include <boost/archive/iterators/remove_whitespace.hpp>
#include <boost/archive/iterators/transform_width.hpp>
35
#include <boost/uuid/detail/sha1.hpp>
36
37
38
39
40
41
42

// This is the C++11 equivalent of a generic lambda.
// The function object is used to send an HTTP message.
template<class Stream>
struct send_lambda {
    Stream& stream_;
    bool& close_;
43
    boost::beast::error_code& ec_;
44

45
    explicit send_lambda(Stream& stream, bool& close, boost::beast::error_code& ec) :
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
            stream_(stream),
            close_(close),
            ec_(ec) {
    }

    template<bool isRequest, class Body, class Fields>
    void operator()(http::message<isRequest, Body, Fields>&& msg) const {
        // Determine if we should close the connection after
        close_ = msg.need_eof();

        // We need the serializer here because the serializer requires
        // a non-const file_body, and the message oriented version of
        // http::write only works with const messages.
        http::serializer<isRequest, Body, Fields> sr{msg};
        http::write(stream_, sr, ec_);
    }
};

RESTHttpsServer::RESTHttpsServer(serverSettings_t settings) :
Alessio Netti's avatar
Alessio Netti committed
65
    _retCode(0),
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
    _isRunning(false) {

    _io = std::unique_ptr<boost::asio::io_context>(
            new boost::asio::io_context(1));

    _ctx = std::unique_ptr<ssl::context>(new ssl::context(ssl::context::tls_server));

    _ctx->set_options(ssl::context::default_workarounds |
                      ssl::context::no_tlsv1 |
                      ssl::context::single_dh_use);

    // Password callback needs to be set before setting cert and key.
    /*
    _ctx->set_password_callback([](std::size_t max_length,
                                  ssl::context::password_purpose purpose)
    {
        return "password";
    });
    */

86
87
88
89
    try {
        _ctx->use_certificate_chain_file(settings.certificate);
        _ctx->use_private_key_file(settings.privateKey, ssl::context::pem);
    } catch (const std::exception& e) {
90
        ServerLOG(fatal) << "Could not load certificate OR private key settings file! "
91
                            "Please ensure the paths in the config file are valid!";
Michael Ott's avatar
Michael Ott committed
92
93
        std::runtime_error re("RESTAPI config error");
        throw re;
94
    }
95
96
97
98
		
    // 2048bit Diffie-Hellman parameters from RFC3526
    static unsigned char const s_dh2048_pem[] = { 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x44, 0x48, 0x20, 0x50, 0x41, 0x52, 0x41, 0x4D, 0x45, 0x54, 0x45, 0x52, 0x53, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x41, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x4A, 0x44, 0x39, 0x71, 0x69, 0x49, 0x57, 0x6A, 0x43, 0x4E, 0x4D, 0x54, 0x47, 0x59, 0x6F, 0x75, 0x41, 0x33, 0x42, 0x7A, 0x52, 0x4B, 0x51, 0x4A, 0x4F, 0x43, 0x49, 0x70, 0x6E, 0x7A, 0x48, 0x51, 0x43, 0x43, 0x37, 0x36, 0x6D, 0x4F, 0x78, 0x4F, 0x62, 0x0A, 0x49, 0x6C, 0x46, 0x4B, 0x43, 0x48, 0x6D, 0x4F, 0x4E, 0x41, 0x54, 0x64, 0x37, 0x35, 0x55, 0x5A, 0x73, 0x38, 0x30, 0x36, 0x51, 0x78, 0x73, 0x77, 0x4B, 0x77, 0x70, 0x74, 0x38, 0x6C, 0x38, 0x55, 0x4E, 0x30, 0x2F, 0x68, 0x4E, 0x57, 0x31, 0x74, 0x55, 0x63, 0x4A, 0x46, 0x35, 0x49, 0x57, 0x31, 0x64, 0x6D, 0x4A, 0x65, 0x66, 0x73, 0x62, 0x30, 0x54, 0x45, 0x4C, 0x70, 0x70, 0x6A, 0x66, 0x74, 0x0A, 0x61, 0x77, 0x76, 0x2F, 0x58, 0x4C, 0x62, 0x30, 0x42, 0x72, 0x66, 0x74, 0x37, 0x6A, 0x68, 0x72, 0x2B, 0x31, 0x71, 0x4A, 0x6E, 0x36, 0x57, 0x75, 0x6E, 0x79, 0x51, 0x52, 0x66, 0x45, 0x73, 0x66, 0x35, 0x6B, 0x6B, 0x6F, 0x5A, 0x6C, 0x48, 0x73, 0x35, 0x46, 0x73, 0x39, 0x77, 0x67, 0x42, 0x38, 0x75, 0x4B, 0x46, 0x6A, 0x76, 0x77, 0x57, 0x59, 0x32, 0x6B, 0x67, 0x32, 0x48, 0x46, 0x58, 0x54, 0x0A, 0x6D, 0x6D, 0x6B, 0x57, 0x50, 0x36, 0x6A, 0x39, 0x4A, 0x4D, 0x39, 0x66, 0x67, 0x32, 0x56, 0x64, 0x49, 0x39, 0x79, 0x6A, 0x72, 0x5A, 0x59, 0x63, 0x59, 0x76, 0x4E, 0x57, 0x49, 0x49, 0x56, 0x53, 0x75, 0x35, 0x37, 0x56, 0x4B, 0x51, 0x64, 0x77, 0x6C, 0x70, 0x5A, 0x74, 0x5A, 0x77, 0x77, 0x31, 0x54, 0x6B, 0x71, 0x38, 0x6D, 0x41, 0x54, 0x78, 0x64, 0x47, 0x77, 0x49, 0x79, 0x68, 0x67, 0x68, 0x0A, 0x66, 0x44, 0x4B, 0x51, 0x58, 0x6B, 0x59, 0x75, 0x4E, 0x73, 0x34, 0x37, 0x34, 0x35, 0x35, 0x33, 0x4C, 0x42, 0x67, 0x4F, 0x68, 0x67, 0x4F, 0x62, 0x4A, 0x34, 0x4F, 0x69, 0x37, 0x41, 0x65, 0x69, 0x6A, 0x37, 0x58, 0x46, 0x58, 0x66, 0x42, 0x76, 0x54, 0x46, 0x4C, 0x4A, 0x33, 0x69, 0x76, 0x4C, 0x39, 0x70, 0x56, 0x59, 0x46, 0x78, 0x67, 0x35, 0x6C, 0x55, 0x6C, 0x38, 0x36, 0x70, 0x56, 0x71, 0x0A, 0x35, 0x52, 0x58, 0x53, 0x4A, 0x68, 0x69, 0x59, 0x2B, 0x67, 0x55, 0x51, 0x46, 0x58, 0x4B, 0x4F, 0x57, 0x6F, 0x71, 0x73, 0x71, 0x6D, 0x6A, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x2F, 0x77, 0x49, 0x42, 0x41, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x44, 0x48, 0x20, 0x50, 0x41, 0x52, 0x41, 0x4D, 0x45, 0x54, 0x45, 0x52, 0x53, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D };
    _ctx->use_tmp_dh(boost::asio::buffer(s_dh2048_pem));
99

100
101
102
103
104
105
106
107
108
109
    // This will receive the new connection
    _socket = std::unique_ptr<tcp::socket>(new tcp::socket(*_io));

    try {
        auto const address = boost::asio::ip::make_address(settings.host);
        auto const port    = static_cast<unsigned short>(std::stoul(settings.port));

        _acceptor = std::unique_ptr<tcp::acceptor>(new tcp::acceptor(*_io, {address, port}));
        _acceptor->set_option(tcp::acceptor::reuse_address(true));
    } catch (const std::exception& e) {
110
        ServerLOG(fatal) << "RestAPI address invalid! Please make sure IP address and port are valid!";
Michael Ott's avatar
Michael Ott committed
111
112
        std::runtime_error re("RESTAPI config error");
        throw re;
113
    }
114
115
116
}


117
void RESTHttpsServer::handle_session(tcp::socket& socket, ssl::context& ctx) {
118
    ServerLOG(debug) << _remoteEndpoint.address().to_string() << ":"
119
120
121
122
123
124
125
126
127
128
129
                    << _remoteEndpoint.port() << " connecting";

    bool close = false;
    boost::beast::error_code ec;

    // Construct the stream around the socket
    boost::beast::ssl_stream<tcp::socket&> stream{socket, ctx};

    // Perform the SSL handshake
    stream.handshake(ssl::stream_base::server, ec);
    if(ec) {
130
        ServerLOG(debug) << "handshake error: " << ec.message();
131
132
133
        goto serverError;
    }

134
135
136
137
138
139
140
    {//scope, so any goto before does not cross variable initialization
        // This buffer is required to persist across reads
        boost::beast::flat_buffer buffer;

        // This lambda is used to send messages
        send_lambda<boost::beast::ssl_stream<tcp::socket&>> lambda{stream, close, ec};

141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
		while(true) {
			// Declare a parser for a request with a string body
			http::request_parser<http::string_body> parser;
			
			// Read the header
			read_header(stream, buffer, parser, ec);
			if(ec == http::error::end_of_stream) {
				break;
			} else if (ec) {
				ServerLOG(debug) << "read error (header): " << ec.message();
				goto serverError;
			}
			
			if(!validateUser(parser.get(), lambda)) {
				break;
			}
			
			// Check for the Expect field value
			if(parser.get()[http::field::expect] == "100-continue")
			{
				// send 100 response
				http::response<http::empty_body> res;
				res.version(11);
				res.result(http::status::continue_);
				write(stream, res, ec);
				if (ec) {
					ServerLOG(debug) << "read write (continue): " << ec.message();
					goto serverError;
				}
			}
			
			http::read(stream, buffer, parser, ec);
			if(ec == http::error::end_of_stream) {
				break;
			} else if(ec) {
				ServerLOG(debug) << "read error (body): " << ec.message();
				goto serverError;
			}

			// Send the response
			handle_request(parser.get(), lambda);
			
			if(ec) {
				ServerLOG(debug) << "write error: " << ec.message();
				goto serverError;
			}
			
			if(close) {
				// This means we should close the connection, usually because
				// the response indicated the "Connection: close" semantic.
				break;
				
			}
		}
	}
196
197
198

    // Perform the SSL shutdown
    stream.shutdown(ec);
199
    if(ec) { ServerLOG(debug) << "stream shutdown error: " << ec.message(); }
200
201

serverError:
202
203
204
    //For graceful closure of a connected socket we shut it down first although
    //this is not strictly necessary.
    //Fails if client already disconnected from socket.
205
    socket.shutdown(tcp::socket::shutdown_both, ec);
206
    if(ec) { ServerLOG(debug) << "socket shutdown: " << ec.message(); }
207

208
    socket.close(ec);
209
    if(ec) { ServerLOG(debug) << "socket close error: " << ec.message(); }
210
211
212
213
    startAccept();
}

template<class Body, class Send>
214
void RESTHttpsServer::handle_request(http::request<Body>& req, Send&& send) {
215
216
217
218
219
220
221
222

    http::response<http::string_body> res {http::status::internal_server_error, req.version()};
    res.set(http::field::server, SERVER_STRING);
    res.set(http::field::content_type, "text/plain");
    res.keep_alive(req.keep_alive());
    res.body() = "Unknown error occurred\n";

    //split target and find matching endpoint handler
223
    queries_t queries;
224
    const std::string endpointName = splitUri(req.target().to_string(), queries);
225
226
227

    //Look up the endpoint
    try {
228
        apiEndpoint_t endpoint = _endpoints.at(endpointName);
229
230
231

        if (endpoint.first == req.method()) {
            //Everything matches --> call the endpoint function
232
            ServerLOG(debug) << req.method_string() << " " << endpointName << " requested";
233
            endpoint.second(req, res, queries);
234
        } else {
235
236
            const std::string msg = "Request method " + req.method_string().to_string() +
                                    " does not match endpoint " + endpointName + "\n";
237
            ServerLOG(debug) << msg;
238
239
            res.result(http::status::bad_request);
            res.body() = msg;
240
241
        }
    } catch (const std::out_of_range& e) {
242
        ServerLOG(debug) << "Requested endpoint " << endpointName << " not found";
243
        res.result(http::status::not_implemented);
244
        res.body() = "Invalid endpoint\n";
245
246
    }

247
    //ServerLOG(info) << "Responding:\n" << res.body();
248

249
250
251
252
253
254
255
256
    res.prepare_payload();
    send(std::move(res));
    return;
}

template<class Body, class Send>
bool RESTHttpsServer::validateUser(const http::request<Body>& req, Send&& send) {

257
258
259
260
261
262
    http::response<http::string_body> res {http::status::unauthorized, req.version()};
    res.set(http::field::server, SERVER_STRING);
    res.set(http::field::content_type, "text/plain");
    res.keep_alive(req.keep_alive());
    res.body() = "Unauthorized access!\n";
    res.prepare_payload();
263

Michael Ott's avatar
Michael Ott committed
264
265
    //GET /help and /version do not need any authorization
    if ((req.method() == http::verb::get) && ((req.target() == "/help") || (req.target() == "/version"))) {
266
267
268
269
270
271
272
        return true;
    }

    std::string auth;
    std::string credentials;

    try {
273
        auth = req.base().at(http::field::authorization).to_string();
274
275
    } catch (const std::out_of_range& e) {
        ServerLOG(info) << "No credentials were provided";
276
        send(std::move(res));
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
        return false;
    }

    //Remove the substring "Basic" and decode the credentials.
    auth.erase(0,6);
    using namespace boost::archive::iterators;
    using ItBinaryT =
            transform_width<binary_from_base64<remove_whitespace<std::string::const_iterator>>, 8, 6>;

    try {
        // If the input isn't a multiple of 4, pad with =
        size_t num_pad_chars((4 - auth.size() % 4) % 4);
        auth.append(num_pad_chars, '=');

        size_t pad_chars(std::count(auth.begin(), auth.end(), '='));
        std::replace(auth.begin(), auth.end(), '=', 'A');
        std::string output(ItBinaryT(auth.begin()), ItBinaryT(auth.end()));
        output.erase(output.end() - pad_chars, output.end());
        credentials = output;
    } catch (std::exception const&) {
        credentials = std::string("");
    }

    size_t pos = credentials.find(':');
    const std::string usr = credentials.substr(0, pos);
    const std::string pwd = credentials.substr(pos+1, credentials.length());

    //Check credentials
    userAttributes_t userData;
    try {
        userData = _users.at(usr);
    } catch (const std::out_of_range& e) {
        ServerLOG(warning) << "User does not exist: " << usr;
310
        send(std::move(res));
311
312
313
        return false;
    }

314
315
316
317
318
319
	boost::uuids::detail::sha1 sha1;
	sha1.process_bytes(pwd.data(), pwd.size());
	unsigned hash[5] = {0};
	sha1.get_digest(hash);
	std::stringstream ss;
	for (int i = 0; i < 5; i++)	{
320
		ss << std::hex << std::setfill('0') << std::setw(8) << hash[i];
321
322
323
324
	}
	
    if (ss.str() != userData.first) {
        ServerLOG(warning) << "Invalid password provided for user " << usr;
325
        send(std::move(res));
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
        return false;
    }

    permission perm;

    switch (req.method()) {
        case http::verb::get:
            perm = permission::GET;
            break;
        case http::verb::put:
            perm = permission::PUT;
            break;
        case http::verb::post:
            perm = permission::POST;
            break;
        case http::verb::delete_:
            perm = permission::DELETE;
            break;
        default:
            perm = permission::NUM_PERMISSIONS;
            break;
    }

    try {
        if (!userData.second.test(perm)) {
            ServerLOG(warning) << "User " << usr << " has insufficient permissions";
352
353
354
355
            res.result(http::status::forbidden);
            res.body() = "Insufficient permissions\n";
            res.prepare_payload();
            send(std::move(res));
356
357
358
            return false;
        }
    } catch (const std::out_of_range& e) {
359
        ServerLOG(debug) << "Permission out of range (method not supported)";
360
361
362
363
        res.result(http::status::not_implemented);
        res.body() = "Request method not supported!\n";
        res.prepare_payload();
        send(std::move(res));
364
365
366
367
368
369
        return false;
    }

    return true;
}

370
std::string RESTHttpsServer::splitUri(const std::string& uri, queries_t& queries) {
371
372
373
374
    //split into path and query
    std::string path;
    std::string query;

375
    //ServerLOG(debug) << "Splitting URI " << uri;
376

377
378
379
380
381
382
383
384
385
386
387
388
    size_t pos = uri.find('?');
    path = uri.substr(0, pos);
    query = uri.substr(pos+1, uri.length());

    //split query part into the individual queries (key-value pairs)
    std::vector<std::string> queryStrs;
    std::stringstream stream(query);
    std::string part;

    while(std::getline(stream, part, ';')) {
        queryStrs.push_back(part);
    }
389

390
391
392
    for(auto& key : queryStrs) {
        size_t pos = key.find("=");
        if (pos != std::string::npos) {
393
            const std::string value = key.substr(pos+1);
394
            key.erase(pos);
395
            queries[key] = value;
396
397
        }
    }
398
399

    return path;
400
401
}