1 // httpd.cxx -- a http daemon subsystem based on Mongoose http
3 // Written by Torsten Dreyer, started April 2014.
5 // Copyright (C) 2014 Torsten Dreyer
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 // General Public License for more details.
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 #include "HTTPRequest.hxx"
23 #include "PropertyChangeWebsocket.hxx"
24 #include "ScreenshotUriHandler.hxx"
25 #include "PropertyUriHandler.hxx"
26 #include "JsonUriHandler.hxx"
27 #include "FlightHistoryUriHandler.hxx"
28 #include "PkgUriHandler.hxx"
29 #include "RunUriHandler.hxx"
30 #include "NavdbUriHandler.hxx"
31 #include "PropertyChangeObserver.hxx"
32 #include <Main/fg_props.hxx>
33 #include <Include/version.h>
34 #include <3rdparty/mongoose/mongoose.h>
35 #include <3rdparty/cjson/cJSON.h>
43 namespace flightgear {
46 const char * PROPERTY_ROOT = "/sim/http";
49 * A Helper class for URI Handlers
51 * This class stores a list of URI Handlers and provides a lookup
52 * method for find the handler by it's URI prefix
54 class URIHandlerMap: public vector<SGSharedPtr<URIHandler> > {
57 * Find a URI Handler for a given URI
59 * Look for the first handler with a uri matching the beginning
60 * of the given uri parameter.
62 * @param uri The uri to find the handler for
63 * @return a SGSharedPtr of the URIHandler or an invalid SGSharedPtr if not found
65 SGSharedPtr<URIHandler> findHandler(const std::string & uri)
67 for (iterator it = begin(); it != end(); ++it) {
68 SGSharedPtr<URIHandler> handler = *it;
69 // check if the request-uri starts with the registered uri-string
70 if (0 == uri.find(handler->getUri())) return handler;
72 return SGSharedPtr<URIHandler>();
77 * A Helper class to create a HTTPRequest from a mongoose connection struct
79 class MongooseHTTPRequest: public HTTPRequest {
82 * Creates a std::string from a char pointer and an optionally given length
83 * If the pointer is NULL or the length is zero, return an empty string
84 * If no length is given, create a std::string from a c-string (up to the /0 terminator)
85 * If length is given, use as many chars as given in length (can exceed the /0 terminator)
87 * @param cp Points to the source of the string
88 * @param len The number of chars to copy to the new string (optional)
89 * @return a std::string containing a copy of the source
91 static inline string NotNull(const char * cp, size_t len = string::npos)
93 if ( NULL == cp || 0 == len) return string("");
94 if (string::npos == len) return string(cp);
95 return string(cp, len);
100 * Constructs a HTTPRequest from a mongoose connection struct
101 * Copies all fields into STL compatible local elements, performs urlDecode etc.
103 * @param connection the mongoose connection struct with the source data
105 MongooseHTTPRequest(struct mg_connection * connection)
107 Method = NotNull(connection->request_method);
108 Uri = urlDecode(NotNull(connection->uri));
109 HttpVersion = NotNull(connection->http_version);
110 QueryString = NotNull(connection->query_string);
112 remoteAddress = NotNull(connection->remote_ip);
113 remotePort = connection->remote_port;
114 localAddress = NotNull(connection->local_ip);
115 localPort = connection->local_port;
117 using namespace simgear::strutils;
118 string_list pairs = split(string(QueryString), "&");
119 for (string_list::iterator it = pairs.begin(); it != pairs.end(); ++it) {
120 string_list nvp = split(*it, "=");
121 if (nvp.size() != 2) continue;
122 RequestVariables.insert(make_pair(urlDecode(nvp[0]), urlDecode(nvp[1])));
125 for (int i = 0; i < connection->num_headers; i++)
126 HeaderVariables[connection->http_headers[i].name] = connection->http_headers[i].value;
128 Content = NotNull(connection->content, connection->content_len);
133 * Decodes a URL encoded string
134 * replaces '+' by ' '
135 * replaces %nn hexdigits
137 * @param s The source string do decode
138 * @return The decoded String
140 static string urlDecode(const string & s)
143 int max = s.length();
145 for (int i = 0; i < max; i++) {
148 } else if (s[i] == '%' && i + 2 < max && isxdigit(s[i + 1]) && isxdigit(s[i + 2])) {
150 a = isdigit(s[i]) ? s[i] - '0' : toupper(s[i]) - 'A' + 10;
152 b = isdigit(s[i]) ? s[i] - '0' : toupper(s[i]) - 'A' + 10;
153 r += (char) (a * 16 + b);
164 * A FGHttpd implementation based on mongoose httpd
166 * Mongoose API is documented here: http://cesanta.com/docs/API.shtml
168 class MongooseHttpd: public FGHttpd {
172 * Construct a MongooseHttpd object from options in a PropertyNode
174 MongooseHttpd(SGPropertyNode_ptr);
182 * override SGSubsystem::init()
184 * Reads the configuration PropertyNode, installs URIHandlers and configures mongoose
189 * override SGSubsystem::bind()
196 * override SGSubsystem::unbind()
197 * shutdown of mongoose, clear connections, unregister URIHandlers
202 * overrride SGSubsystem::update()
203 * poll connections, check for changed properties
205 void update(double dt);
208 * Returns a URIHandler for the given uri
210 * @see URIHandlerMap::findHandler( const std::string & uri )
212 SGSharedPtr<URIHandler> findHandler(const std::string & uri)
214 return _uriHandler.findHandler(uri);
217 Websocket * newWebsocket(const string & uri);
220 int poll(struct mg_connection * connection);
221 int auth(struct mg_connection * connection);
222 int request(struct mg_connection * connection);
223 void close(struct mg_connection * connection);
225 static int staticRequestHandler(struct mg_connection *, mg_event event);
227 struct mg_server *_server;
228 SGPropertyNode_ptr _configNode;
230 typedef int (MongooseHttpd::*handler_t)(struct mg_connection *);
231 URIHandlerMap _uriHandler;
233 PropertyChangeObserver _propertyChangeObserver;
236 class MongooseConnection: public Connection {
238 MongooseConnection(MongooseHttpd * httpd)
242 virtual ~MongooseConnection();
244 virtual void close(struct mg_connection * connection) = 0;
245 virtual int poll(struct mg_connection * connection) = 0;
246 virtual int request(struct mg_connection * connection) = 0;
247 virtual void write(const char * data, size_t len)
249 if (_connection) mg_send_data(_connection, data, len);
252 static MongooseConnection * getConnection(MongooseHttpd * httpd, struct mg_connection * connection);
255 void setConnection(struct mg_connection * connection)
257 _connection = connection;
259 MongooseHttpd * _httpd;
260 struct mg_connection * _connection;
264 MongooseConnection::~MongooseConnection()
268 class RegularConnection: public MongooseConnection {
270 RegularConnection(MongooseHttpd * httpd)
271 : MongooseConnection(httpd)
274 virtual ~RegularConnection()
278 virtual void close(struct mg_connection * connection);
279 virtual int poll(struct mg_connection * connection);
280 virtual int request(struct mg_connection * connection);
283 SGSharedPtr<URIHandler> _handler;
286 class WebsocketConnection: public MongooseConnection {
288 WebsocketConnection(MongooseHttpd * httpd)
289 : MongooseConnection(httpd), _websocket(NULL)
292 virtual ~WebsocketConnection()
296 virtual void close(struct mg_connection * connection);
297 virtual int poll(struct mg_connection * connection);
298 virtual int request(struct mg_connection * connection);
301 class MongooseWebsocketWriter: public WebsocketWriter {
303 MongooseWebsocketWriter(struct mg_connection * connection)
304 : _connection(connection)
308 virtual int writeToWebsocket(int opcode, const char * data, size_t len)
310 return mg_websocket_write(_connection, opcode, data, len);
313 struct mg_connection * _connection;
315 Websocket * _websocket;
318 MongooseConnection * MongooseConnection::getConnection(MongooseHttpd * httpd, struct mg_connection * connection)
320 if (connection->connection_param) return static_cast<MongooseConnection*>(connection->connection_param);
321 MongooseConnection * c;
322 if (connection->is_websocket) c = new WebsocketConnection(httpd);
323 else c = new RegularConnection(httpd);
325 connection->connection_param = c;
329 int RegularConnection::request(struct mg_connection * connection)
331 setConnection(connection);
332 MongooseHTTPRequest request(connection);
333 SG_LOG(SG_NETWORK, SG_INFO, "RegularConnection::request for " << request.Uri);
335 // find a handler for the uri and remember it for possible polls on this connection
336 _handler = _httpd->findHandler(request.Uri);
337 if (false == _handler.valid()) {
338 // uri not registered - pass false to indicate we have not processed the request
342 // We handle this URI, prepare the response
343 HTTPResponse response;
344 response.Header["Server"] = "FlightGear/" FLIGHTGEAR_VERSION " Mongoose/" MONGOOSE_VERSION;
345 response.Header["Connection"] = "keep-alive";
346 response.Header["Cache-Control"] = "no-cache";
349 time_t now = time(NULL);
350 strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&now));
351 response.Header["Date"] = buf;
354 // hand the request over to the handler, returns true if request is finished,
355 // false the handler wants to get polled again (calling handlePoll() next time)
356 bool done = _handler->handleRequest(request, response, this);
357 // fill in the response header
358 mg_send_status(connection, response.StatusCode);
359 for (HTTPResponse::Header_t::const_iterator it = response.Header.begin(); it != response.Header.end(); ++it) {
360 const string name = it->first;
361 const string value = it->second;
362 if (name.empty() || value.empty()) continue;
363 mg_send_header(connection, name.c_str(), value.c_str());
365 if (done || false == response.Content.empty()) {
366 SG_LOG(SG_NETWORK, SG_INFO,
367 "RegularConnection::request() responding " << response.Content.length() << " Bytes, done=" << done);
368 mg_send_data(connection, response.Content.c_str(), response.Content.length());
370 return done ? MG_TRUE : MG_MORE;
373 int RegularConnection::poll(struct mg_connection * connection)
375 setConnection(connection);
376 if (false == _handler.valid()) return MG_FALSE;
377 // only return MG_TRUE if we handle this request
378 return _handler->poll(this) ? MG_TRUE : MG_MORE;
381 void RegularConnection::close(struct mg_connection * connection)
383 setConnection(connection);
387 void WebsocketConnection::close(struct mg_connection * connection)
389 setConnection(connection);
390 if ( NULL != _websocket) _websocket->close();
395 int WebsocketConnection::poll(struct mg_connection * connection)
397 setConnection(connection);
398 // we get polled before the first request came in but we know
399 // nothing about how to handle that before we know the URI.
400 // so simply ignore that poll
401 if ( NULL != _websocket) {
402 MongooseWebsocketWriter writer(connection);
403 _websocket->poll(writer);
408 int WebsocketConnection::request(struct mg_connection * connection)
410 setConnection(connection);
411 MongooseHTTPRequest request(connection);
412 SG_LOG(SG_NETWORK, SG_INFO, "WebsocketConnection::request for " << request.Uri);
414 if ( NULL == _websocket) _websocket = _httpd->newWebsocket(request.Uri);
415 if ( NULL == _websocket) {
416 SG_LOG(SG_NETWORK, SG_WARN, "httpd: unhandled websocket uri: " << request.Uri);
417 return MG_TRUE; // close connection - good bye
420 MongooseWebsocketWriter writer(connection);
421 _websocket->handleRequest(request, writer);
425 MongooseHttpd::MongooseHttpd(SGPropertyNode_ptr configNode)
426 : _server(NULL), _configNode(configNode)
430 MongooseHttpd::~MongooseHttpd()
432 mg_destroy_server(&_server);
435 void MongooseHttpd::init()
437 SGPropertyNode_ptr n = _configNode->getNode("uri-handler");
441 if ((uri = n->getStringValue("screenshot"))[0] != 0) {
442 SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding screenshot uri handler at " << uri);
443 _uriHandler.push_back(new flightgear::http::ScreenshotUriHandler(uri));
446 if ((uri = n->getStringValue("property"))[0] != 0) {
447 SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding property uri handler at " << uri);
448 _uriHandler.push_back(new flightgear::http::PropertyUriHandler(uri));
451 if ((uri = n->getStringValue("json"))[0] != 0) {
452 SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding json uri handler at " << uri);
453 _uriHandler.push_back(new flightgear::http::JsonUriHandler(uri));
456 if ((uri = n->getStringValue("pkg"))[0] != 0) {
457 SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding pkg uri handler at " << uri);
458 _uriHandler.push_back(new flightgear::http::PkgUriHandler(uri));
461 if ((uri = n->getStringValue("flighthistory"))[0] != 0) {
462 SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding flighthistory uri handler at " << uri);
463 _uriHandler.push_back(new flightgear::http::FlightHistoryUriHandler(uri));
466 if ((uri = n->getStringValue("run"))[0] != 0) {
467 SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding run uri handler at " << uri);
468 _uriHandler.push_back(new flightgear::http::RunUriHandler(uri));
471 if ((uri = n->getStringValue("navdb"))[0] != 0) {
472 SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding navdb uri handler at " << uri);
473 _uriHandler.push_back(new flightgear::http::NavdbUriHandler(uri));
477 _server = mg_create_server(this, MongooseHttpd::staticRequestHandler);
479 n = _configNode->getNode("options");
482 const string fgRoot = fgGetString("/sim/fg-root");
483 string docRoot = n->getStringValue("document-root", fgRoot.c_str());
484 if (docRoot[0] != '/') docRoot.insert(0, "/").insert(0, fgRoot);
486 mg_set_option(_server, "document_root", docRoot.c_str());
488 mg_set_option(_server, "listening_port", n->getStringValue("listening-port", "8080"));
490 // build url rewrites relative to fg-root
491 string rewrites = n->getStringValue("url-rewrites", "");
492 string_list rwl = simgear::strutils::split(rewrites, ",");
493 rwl.push_back(string("/aircraft-dir/=") + fgGetString("/sim/aircraft-dir") + "/" );
495 for (string_list::iterator it = rwl.begin(); it != rwl.end(); ++it) {
496 string_list rw_entries = simgear::strutils::split(*it, "=");
497 if (rw_entries.size() != 2) {
498 SG_LOG(SG_NETWORK, SG_WARN, "invalid entry '" << *it << "' in url-rewrites ignored.");
501 string & lhs = rw_entries[0];
502 string & rhs = rw_entries[1];
503 if (false == rewrites.empty()) rewrites.append(1, ',');
504 rewrites.append(lhs).append(1, '=');
505 SGPath targetPath(rhs);
506 if (targetPath.isAbsolute() ) {
507 rewrites.append(rhs);
509 // don't use targetPath here because SGPath strips trailing '/'
510 rewrites.append(fgRoot).append(1, '/').append(rhs);
513 if (false == rewrites.empty()) mg_set_option(_server, "url_rewrites", rewrites.c_str());
515 mg_set_option(_server, "enable_directory_listing", n->getStringValue("enable-directory-listing", "yes"));
516 mg_set_option(_server, "idle_timeout_ms", n->getStringValue("idle-timeout-ms", "30000"));
517 mg_set_option(_server, "index_files", n->getStringValue("index-files", "index.html"));
518 mg_set_option(_server, "extra_mime_types", n->getStringValue("extra-mime-types", ""));
519 mg_set_option(_server, "access_log_file", n->getStringValue("access-log-file", ""));
521 if( sglog().would_log(SG_NETWORK,SG_INFO) ) {
522 SG_LOG(SG_NETWORK,SG_INFO,"starting mongoose with these options: ");
523 const char ** optionNames = mg_get_valid_option_names();
524 for( int i = 0; optionNames[i] != NULL; i+= 2 ) {
525 SG_LOG(SG_NETWORK,SG_INFO, " > " << optionNames[i] << ": '" << mg_get_option(_server, optionNames[i]) << "'" );
527 SG_LOG(SG_NETWORK,SG_INFO,"end of mongoose options.");
532 _configNode->setBoolValue("running",true);
536 void MongooseHttpd::bind()
540 void MongooseHttpd::unbind()
542 _configNode->setBoolValue("running",false);
543 mg_destroy_server(&_server);
545 _propertyChangeObserver.clear();
548 void MongooseHttpd::update(double dt)
550 _propertyChangeObserver.check();
551 mg_poll_server(_server, 0);
552 _propertyChangeObserver.uncheck();
555 int MongooseHttpd::poll(struct mg_connection * connection)
557 if ( NULL == connection->connection_param) return MG_FALSE; // connection not yet set up - ignore poll
559 return MongooseConnection::getConnection(this, connection)->poll(connection);
562 int MongooseHttpd::auth(struct mg_connection * connection)
564 // auth preceeds request for websockets and regular connections,
565 // and eventually the websocket has been already set up by mongoose
566 // use this to choose the connection type
567 MongooseConnection::getConnection(this, connection);
568 //return MongooseConnection::getConnection(this,connection)->auth(connection);
569 return MG_TRUE; // unrestricted access for now
572 int MongooseHttpd::request(struct mg_connection * connection)
574 return MongooseConnection::getConnection(this, connection)->request(connection);
577 void MongooseHttpd::close(struct mg_connection * connection)
579 MongooseConnection * c = MongooseConnection::getConnection(this, connection);
580 c->close(connection);
583 Websocket * MongooseHttpd::newWebsocket(const string & uri)
585 if (uri.find("/PropertyListener") == 0) {
586 SG_LOG(SG_NETWORK, SG_INFO, "new PropertyChangeWebsocket for: " << uri);
587 return new PropertyChangeWebsocket(&_propertyChangeObserver);
592 int MongooseHttpd::staticRequestHandler(struct mg_connection * connection, mg_event event)
595 case MG_POLL: // MG_TRUE: finished sending data, MG_MORE, call again
596 return static_cast<MongooseHttpd*>(connection->server_param)->poll(connection);
598 case MG_AUTH: // If callback returns MG_FALSE, authentication fails
599 return static_cast<MongooseHttpd*>(connection->server_param)->auth(connection);
601 case MG_REQUEST: // If callback returns MG_FALSE, Mongoose continues with req
602 return static_cast<MongooseHttpd*>(connection->server_param)->request(connection);
604 case MG_CLOSE: // Connection is closed, callback return value is ignored
605 static_cast<MongooseHttpd*>(connection->server_param)->close(connection);
608 case MG_HTTP_ERROR: // If callback returns MG_FALSE, Mongoose continues with err
609 return MG_FALSE; // we don't handle errors - let mongoose do the work
611 // client services not used/implemented. Signal 'close connection' to be sure
612 case MG_CONNECT: // If callback returns MG_FALSE, connect fails
613 case MG_REPLY: // If callback returns MG_FALSE, Mongoose closes connection
617 return MG_FALSE; // keep compiler happy..
621 FGHttpd * FGHttpd::createInstance(SGPropertyNode_ptr configNode)
623 // only create a server if a port has been configured
624 if (false == configNode.valid()) return NULL;
625 string port = configNode->getStringValue("options/listening-port", "");
626 if (port.empty()) return NULL;
627 return new MongooseHttpd(configNode);
631 } // namespace flightgear