1
0
Fork 0

Update of the httpd implementation

- Add a screenshot uri handler
- Enable persistent http connections
- Simple CORS implementatation for the
  JSON property uri handler

many changes for the httpd
This commit is contained in:
Torsten Dreyer 2014-03-31 22:31:30 +02:00
parent a5c39e3009
commit 2e76a2c72a
16 changed files with 966 additions and 242 deletions

View file

@ -29,9 +29,23 @@ using std::string;
namespace flightgear {
namespace http {
bool JsonUriHandler::handleGetRequest( const HTTPRequest & request, HTTPResponse & response )
bool JsonUriHandler::handleRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection )
{
response.Header["Content-Type"] = "application/json; charset=UTF-8";
response.Header["Access-Control-Allow-Origin"] = "*";
response.Header["Access-Control-Allow-Methods"] = "OPTIONS, GET";
response.Header["Access-Control-Allow-Headers"] = "Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token";
if( request.Method == "OPTIONS" ){
return true; // OPTIONS only needs the headers
}
if( request.Method != "GET" ){
response.Header["Allow"] = "OPTIONS, GET";
response.StatusCode = 405;
response.Content = "{}";
return true;
}
string propertyPath = request.Uri;
@ -58,7 +72,7 @@ bool JsonUriHandler::handleGetRequest( const HTTPRequest & request, HTTPResponse
SGPropertyNode_ptr node = fgGetNode( string("/") + propertyPath );
if( false == node.valid() ) {
response.StatusCode = 400;
response.StatusCode = 404;
response.Content = "{}";
SG_LOG(SG_NETWORK,SG_WARN, "Node not found: '" << propertyPath << "'");
return true;

View file

@ -29,7 +29,7 @@ namespace http {
class JsonUriHandler : public URIHandler {
public:
JsonUriHandler( const char * uri = "/json/" ) : URIHandler( uri ) {}
virtual bool handleGetRequest( const HTTPRequest & request, HTTPResponse & response );
virtual bool handleRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection );
};
} // namespace http

View file

@ -218,10 +218,24 @@ static cJSON * createFeatureFor(FGPositionedRef positioned)
return feature;
}
bool NavdbUriHandler::handleRequest(const HTTPRequest & request, HTTPResponse & response)
bool NavdbUriHandler::handleRequest(const HTTPRequest & request, HTTPResponse & response, Connection * connection)
{
response.Header["Content-Type"] = "application/json; charset=UTF-8";
response.Header["Access-Control-Allow-Origin"] = "*";
response.Header["Access-Control-Allow-Methods"] = "OPTIONS, GET";
response.Header["Access-Control-Allow-Headers"] = "Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token";
if( request.Method == "OPTIONS" ){
return true; // OPTIONS only needs the headers
}
if( request.Method != "GET" ){
response.Header["Allow"] = "OPTIONS, GET";
response.StatusCode = 405;
response.Content = "{}";
return true;
}
bool indent = request.RequestVariables.get("i") == "y";

View file

@ -29,7 +29,7 @@ namespace http {
class NavdbUriHandler : public URIHandler {
public:
NavdbUriHandler( const char * uri = "/navdb" ) : URIHandler( uri ) {}
virtual bool handleRequest( const HTTPRequest & request, HTTPResponse & response );
virtual bool handleRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection );
};
} // namespace http

View file

@ -81,7 +81,7 @@ const SGPropertyNode_ptr PropertyChangeObserver::addObservation( const string pr
return entry->_node;
}
catch( string & s ) {
SG_LOG(SG_NETWORK,SG_ALERT,"httpd: can't observer '" << propertyName << "'. Invalid name." );
SG_LOG(SG_NETWORK,SG_WARN,"httpd: can't observer '" << propertyName << "'. Invalid name." );
}
SGPropertyNode_ptr empty;

View file

@ -90,15 +90,16 @@ void PropertyChangeWebsocket::handleRequest(const HTTPRequest & request, Websock
}
}
void PropertyChangeWebsocket::update(WebsocketWriter & writer)
void PropertyChangeWebsocket::poll(WebsocketWriter & writer)
{
for (WatchedNodesList::iterator it = _watchedNodes.begin(); it != _watchedNodes.end(); ++it) {
SGPropertyNode_ptr node = *it;
string newValue;
if (_propertyChangeObserver->isChangedValue(node)) {
SG_LOG(SG_NETWORK, SG_DEBUG, "httpd: new Value for " << node->getPath(true) << " '" << node->getStringValue() << "' #" << id);
writer.writeText( JSON::toJsonString( false, node, 0, fgGetDouble("/sim/time/elapsed-sec") ) );
string out = JSON::toJsonString( false, node, 0, fgGetDouble("/sim/time/elapsed-sec") );
SG_LOG(SG_NETWORK, SG_DEBUG, "PropertyChangeWebsocket::poll() new Value for " << node->getPath(true) << " '" << node->getStringValue() << "' #" << id << ": " << out );
writer.writeText( out );
}
}
}
@ -115,11 +116,13 @@ void PropertyChangeWebsocket::WatchedNodesList::handleCommand(const string & com
}
SGPropertyNode_ptr n = propertyChangeObserver->addObservation(node);
if (n.valid()) push_back(n);
SG_LOG(SG_NETWORK, SG_INFO, "httpd: " << command << " '" << node << "' success");
} else if (command == "removeListener") {
for (iterator it = begin(); it != end(); ++it) {
if (node == (*it)->getPath(true)) {
this->erase(it);
SG_LOG(SG_NETWORK, SG_INFO, "httpd: " << command << " '" << node << "' success");
return;
}
SG_LOG(SG_NETWORK, SG_WARN, "httpd: " << command << " '" << node << "' ignored (not found)");

View file

@ -37,7 +37,7 @@ public:
virtual ~PropertyChangeWebsocket();
virtual void close();
virtual void handleRequest(const HTTPRequest & request, WebsocketWriter & writer);
virtual void update(WebsocketWriter & writer);
virtual void poll(WebsocketWriter & writer);
private:
unsigned id;

View file

@ -218,7 +218,7 @@ static DOMElement * renderPropertyValueElement( SGPropertyNode_ptr node )
return root;
}
bool PropertyUriHandler::handleGetRequest( const HTTPRequest & request, HTTPResponse & response )
bool PropertyUriHandler::handleGetRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection )
{
string propertyPath = request.Uri;
@ -298,6 +298,7 @@ bool PropertyUriHandler::handleGetRequest( const HTTPRequest & request, HTTPResp
e->addChild( new DOMTextElement( propertyPath ) );
// does not exist
body->addChild( e );
response.StatusCode = 404;
} else if( node->nChildren() > 0 ) {
// Render the list of children

View file

@ -29,7 +29,7 @@ namespace http {
class PropertyUriHandler : public URIHandler {
public:
PropertyUriHandler( const char * uri = "/prop/" ) : URIHandler( uri ) {}
virtual bool handleGetRequest( const HTTPRequest & request, HTTPResponse & response );
virtual bool handleGetRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection );
};
} // namespace http

View file

@ -33,7 +33,7 @@ namespace flightgear {
namespace http {
bool RunUriHandler::handleRequest( const HTTPRequest & request, HTTPResponse & response )
bool RunUriHandler::handleRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection )
{
response.Header["Content-Type"] = "text/plain";
string command = request.RequestVariables.get("value");
@ -54,6 +54,7 @@ bool RunUriHandler::handleRequest( const HTTPRequest & request, HTTPResponse & r
}
response.Content = "command '" + command + "' failed.";
response.StatusCode = 501; // Not implemented probably suits best
SG_LOG( SG_NETWORK, SG_WARN, response.Content );
return true;
}

View file

@ -29,7 +29,7 @@ namespace http {
class RunUriHandler : public URIHandler {
public:
RunUriHandler( const char * uri = "/run.cgi" ) : URIHandler( uri ) {}
virtual bool handleRequest( const HTTPRequest & request, HTTPResponse & response );
virtual bool handleRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection );
};
} // namespace http

View file

@ -1,6 +1,8 @@
// ScreenshotUriHandler.cxx -- Provide screenshots via http
//
// Written by Torsten Dreyer, started April 2014.
// Started by Curtis Olson, started June 2001.
// osg support written by James Turner
// Ported to new httpd infrastructure by Torsten Dreyer
//
// Copyright (C) 2014 Torsten Dreyer
//
@ -18,20 +20,410 @@
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "ScreenshotUriHandler.hxx"
#include <osgDB/Registry>
#include <osgDB/ReaderWriter>
#include <osgUtil/SceneView>
#include <osgViewer/Viewer>
#include <simgear/threads/SGQueue.hxx>
#include <simgear/structure/Singleton.hxx>
#include <Main/globals.hxx>
#include <Viewer/renderer.hxx>
#include <queue>
#include <boost/lexical_cast.hpp>
using std::string;
using std::vector;
using std::list;
namespace flightgear {
namespace http {
bool ScreenshotUriHandler::handleRequest( const HTTPRequest & request, HTTPResponse & response )
///////////////////////////////////////////////////////////////////////////
class ImageReadyListener {
public:
virtual void imageReady(osg::ref_ptr<osg::Image>) = 0;
virtual ~ImageReadyListener()
{
}
};
class StringReadyListener {
public:
virtual void stringReady(const std::string &) = 0;
virtual ~StringReadyListener()
{
}
};
struct ImageCompressionTask {
StringReadyListener * stringReadyListener;
string format;
osg::ref_ptr<osg::Image> image;
ImageCompressionTask()
{
stringReadyListener = NULL;
}
ImageCompressionTask(const ImageCompressionTask & other)
{
stringReadyListener = other.stringReadyListener;
format = other.format;
image = other.image;
}
ImageCompressionTask & operator =(const ImageCompressionTask & other)
{
stringReadyListener = other.stringReadyListener;
format = other.format;
image = other.image;
return *this;
}
};
class ImageCompressor: public OpenThreads::Thread {
public:
ImageCompressor()
{
}
virtual void run();
void addTask(ImageCompressionTask & task);
private:
typedef SGBlockingQueue<ImageCompressionTask> TaskList;
TaskList _tasks;
};
typedef simgear::Singleton<ImageCompressor> ImageCompressorSingleton;
void ImageCompressor::run()
{
response.Header["Content-Type"] = "image/jpeg";
// string response = "Screenshot - not yet implemented :-(";
// mg_send_data( connection, response.c_str(), response.length() );
osg::ref_ptr<osgDB::ReaderWriter::Options> options = new osgDB::ReaderWriter::Options("JPEG_QUALITY 80 PNG_COMPRESSION 9");
SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor is running");
for (;;) {
ImageCompressionTask task = _tasks.pop();
SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor has an image");
if ( NULL != task.stringReadyListener) {
SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor checking for writer for " << task.format);
osgDB::ReaderWriter* writer = osgDB::Registry::instance()->getReaderWriterForExtension(task.format);
if (!writer)
continue;
SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor compressing to " << task.format);
std::stringstream outputStream;
osgDB::ReaderWriter::WriteResult wr;
wr = writer->writeImage(*task.image, outputStream, options);
if (wr.success()) {
SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor compressed to " << task.format);
task.stringReadyListener->stringReady(outputStream.str());
}
SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor done for this image" << task.format);
}
}
SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor exiting");
}
void ImageCompressor::addTask(ImageCompressionTask & task)
{
_tasks.push(task);
}
/**
* Based on <a href="http://code.google.com/p/osgworks">osgworks</a> ScreenCapture.cpp
*
*/
class ScreenshotCallback: public osg::Camera::DrawCallback {
public:
ScreenshotCallback()
: _min_delta_tick(1.0/8.0)
{
_previousFrameTick = osg::Timer::instance()->tick();
}
virtual void operator ()(osg::RenderInfo& renderInfo) const
{
osg::Timer_t n = osg::Timer::instance()->tick();
double dt = osg::Timer::instance()->delta_s(_previousFrameTick,n);
if (dt < _min_delta_tick)
return;
_previousFrameTick = n;
bool hasSubscribers = false;
{
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
hasSubscribers = !_subscribers.empty();
}
if (hasSubscribers) {
osg::ref_ptr<osg::Image> image = new osg::Image;
const osg::Viewport* vp = renderInfo.getState()->getCurrentViewport();
image->readPixels(vp->x(), vp->y(), vp->width(), vp->height(), GL_RGB, GL_UNSIGNED_BYTE);
{
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
while (!_subscribers.empty()) {
try {
_subscribers.back()->imageReady(image);
}
catch (...) {
}
_subscribers.pop_back();
}
}
}
}
void subscribe(ImageReadyListener * subscriber)
{
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
_subscribers.push_back(subscriber);
}
void unsubscribe(ImageReadyListener * subscriber)
{
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
_subscribers.remove( subscriber );
}
private:
mutable list<ImageReadyListener*> _subscribers;
mutable OpenThreads::Mutex _lock;
mutable double _previousFrameTick;
double _min_delta_tick;
};
///////////////////////////////////////////////////////////////////////////
class ScreenshotRequest: public ConnectionData, public ImageReadyListener, StringReadyListener {
public:
ScreenshotRequest(const string & window, const string & type, bool stream)
: _type(type), _stream(stream)
{
if ( NULL == osgDB::Registry::instance()->getReaderWriterForExtension(_type))
throw sg_format_exception("Unsupported image type: " + type, type);
osg::Camera * camera = findLastCamera(globals->get_renderer()->getViewer(), window);
if ( NULL == camera)
throw sg_error("Can't find a camera for window '" + window + "'");
// add our ScreenshotCallback to the camera
if ( NULL == camera->getFinalDrawCallback()) {
//TODO: are we leaking the Callback on reinit?
camera->setFinalDrawCallback(new ScreenshotCallback());
}
_screenshotCallback = dynamic_cast<ScreenshotCallback*>(camera->getFinalDrawCallback());
if ( NULL == _screenshotCallback)
throw sg_error("Can't find ScreenshotCallback");
requestScreenshot();
}
virtual ~ScreenshotRequest()
{
_screenshotCallback->unsubscribe(this);
}
virtual void imageReady(osg::ref_ptr<osg::Image> rawImage)
{
// called from a rendering thread, not from the main loop
ImageCompressionTask task;
task.image = rawImage;
task.format = _type;
task.stringReadyListener = this;
ImageCompressorSingleton::instance()->addTask(task);
}
void requestScreenshot()
{
_screenshotCallback->subscribe(this);
}
mutable OpenThreads::Mutex _lock;
virtual void stringReady(const string & s)
{
// called from the compressor thread
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
_compressedData = s;
}
string getScreenshot()
{
string reply;
{
// called from the main loop
OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
reply = _compressedData;
_compressedData.clear();
}
return reply;
}
osg::Camera* findLastCamera(osgViewer::ViewerBase * viewer, const string & windowName)
{
osgViewer::ViewerBase::Windows windows;
viewer->getWindows(windows);
osgViewer::GraphicsWindow* window = NULL;
if (false == windowName.empty()) {
for (osgViewer::ViewerBase::Windows::iterator itr = windows.begin(); itr != windows.end(); ++itr) {
if ((*itr)->getTraits()->windowName == windowName) {
window = *itr;
break;
}
}
}
if ( NULL == window) {
if (false == windowName.empty()) {
SG_LOG(SG_NETWORK, SG_INFO, "requested window " << windowName << " not found, using first window");
}
window = *windows.begin();
}
SG_LOG(SG_NETWORK, SG_DEBUG, "Looking for last Camera of window '" << window->getTraits()->windowName << "'");
osg::GraphicsContext::Cameras& cameras = window->getCameras();
osg::Camera* lastCamera = 0;
for (osg::GraphicsContext::Cameras::iterator cam_itr = cameras.begin(); cam_itr != cameras.end(); ++cam_itr) {
if (lastCamera) {
if ((*cam_itr)->getRenderOrder() > lastCamera->getRenderOrder()) {
lastCamera = (*cam_itr);
}
if ((*cam_itr)->getRenderOrder() == lastCamera->getRenderOrder()
&& (*cam_itr)->getRenderOrderNum() >= lastCamera->getRenderOrderNum()) {
lastCamera = (*cam_itr);
}
} else {
lastCamera = *cam_itr;
}
}
return lastCamera;
}
bool isStream() const
{
return _stream;
}
const string & getType() const
{
return _type;
}
private:
string _type;
bool _stream;
string _compressedData;
ScreenshotCallback * _screenshotCallback;
};
ScreenshotUriHandler::ScreenshotUriHandler(const char * uri)
: URIHandler(uri)
{
}
ScreenshotUriHandler::~ScreenshotUriHandler()
{
ImageCompressorSingleton::instance()->cancel();
//ImageCompressorSingleton::instance()->join();
}
const static string KEY("ScreenshotUriHandler::ScreenshotRequest");
#define BOUNDARY "--fgfs-screenshot-boundary"
bool ScreenshotUriHandler::handleGetRequest(const HTTPRequest & request, HTTPResponse & response, Connection * connection)
{
if (!ImageCompressorSingleton::instance()->isRunning())
ImageCompressorSingleton::instance()->start();
string type = request.RequestVariables.get("type");
if (type.empty()) type = "jpg";
// string camera = request.RequestVariables.get("camera");
string window = request.RequestVariables.get("window");
bool stream = (false == request.RequestVariables.get("stream").empty());
SGSharedPtr<ScreenshotRequest> screenshotRequest;
try {
SG_LOG(SG_NETWORK, SG_DEBUG, "new ScreenshotRequest("<<window<<","<<type<<"," << stream << ")");
screenshotRequest = new ScreenshotRequest(window, type, stream);
}
catch (sg_format_exception & ex)
{
SG_LOG(SG_NETWORK, SG_INFO, ex.getFormattedMessage());
response.Header["Content-Type"] = "text/plain";
response.StatusCode = 410;
response.Content = ex.getFormattedMessage();
return true;
}
catch (sg_error & ex)
{
SG_LOG(SG_NETWORK, SG_INFO, ex.getFormattedMessage());
response.Header["Content-Type"] = "text/plain";
response.StatusCode = 500;
response.Content = ex.getFormattedMessage();
return true;
}
if (false == stream) {
response.Header["Content-Type"] = string("image/").append(type);
response.Header["Content-Disposition"] = string("inline; filename=\"fgfs-screen.").append(type).append("\"");
} else {
response.Header["Content-Type"] = string("multipart/x-mixed-replace; boundary=" BOUNDARY);
}
connection->put(KEY, screenshotRequest);
return false; // call me again thru poll
}
bool ScreenshotUriHandler::poll(Connection * connection)
{
SGSharedPtr<ConnectionData> data = connection->get(KEY);
ScreenshotRequest * screenshotRequest = dynamic_cast<ScreenshotRequest*>(data.get());
if ( NULL == screenshotRequest) return true; // Should not happen, kill the connection
const string & screenshot = screenshotRequest->getScreenshot();
if (screenshot.empty()) {
SG_LOG(SG_NETWORK, SG_DEBUG, "No screenshot available.");
return false; // not ready yet, call again.
}
SG_LOG(SG_NETWORK, SG_DEBUG, "Screenshot is ready, size=" << screenshot.size());
if (screenshotRequest->isStream()) {
string s("\r\n" BOUNDARY "\r\nContent-Type: image/");
s.append(screenshotRequest->getType()).append("\r\nContent-Length:");
s += boost::lexical_cast<string>(screenshot.size());
s += "\r\n\r\n";
connection->write(s.c_str(), s.length());
}
connection->write(screenshot.data(), screenshot.size());
if (screenshotRequest->isStream()) {
screenshotRequest->requestScreenshot();
// continue until user closes connection
return false;
}
// single screenshot, send terminating chunk
connection->remove(KEY);
connection->write("", 0);
return true; // done.
}
} // namespace http

View file

@ -28,8 +28,10 @@ namespace http {
class ScreenshotUriHandler : public URIHandler {
public:
ScreenshotUriHandler( const char * uri = "/screenshot/" ) : URIHandler( uri ) {}
virtual bool handleRequest( const HTTPRequest & request, HTTPResponse & response );
ScreenshotUriHandler( const char * uri = "/screenshot/" );
~ScreenshotUriHandler();
virtual bool handleGetRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection );
virtual bool poll( Connection * connection );
};
} // namespace http

View file

@ -70,7 +70,7 @@ public:
}
virtual void close() = 0;
virtual void handleRequest(const HTTPRequest & request, WebsocketWriter & writer) = 0;
virtual void update(WebsocketWriter & writer) = 0;
virtual void poll(WebsocketWriter & writer) = 0;
};

View file

@ -43,154 +43,63 @@ namespace http {
const char * PROPERTY_ROOT = "/sim/http";
class MongooseHttpd: public FGHttpd {
/**
* A Helper class for URI Handlers
*
* This class stores a list of URI Handlers and provides a lookup
* method for find the handler by it's URI prefix
*/
class URIHandlerMap: public vector<SGSharedPtr<URIHandler> > {
public:
MongooseHttpd(SGPropertyNode_ptr);
~MongooseHttpd();
void init();
void bind();
void unbind();
void shutdown();
void update(double dt);
private:
int requestHandler(struct mg_connection *);
int websocketHandler(struct mg_connection *);
void closeWebsocket(struct mg_connection *);
int iterateCallback(struct mg_connection *, mg_event event);
static int staticRequestHandler(struct mg_connection *, mg_event event);
static int staticIterateCallback(struct mg_connection *, mg_event event);
struct mg_server *_server;
SGPropertyNode_ptr _configNode;
typedef int (MongooseHttpd::*handler_t)(struct mg_connection *);
typedef vector<SGSharedPtr<URIHandler> > URIHandlerMap;
URIHandlerMap _uriHandlers;
PropertyChangeObserver _propertyChangeObserver;
/**
* Find a URI Handler for a given URI
*
* Look for the first handler with a uri matching the beginning
* of the given uri parameter.
*
* @param uri The uri to find the handler for
* @return a SGSharedPtr of the URIHandler or an invalid SGSharedPtr if not found
*/
SGSharedPtr<URIHandler> findHandler(const std::string & uri)
{
for (iterator it = begin(); it != end(); ++it) {
SGSharedPtr<URIHandler> handler = *it;
// check if the request-uri starts with the registered uri-string
if (0 == uri.find(handler->getUri())) return handler;
}
return SGSharedPtr<URIHandler>();
}
};
MongooseHttpd::MongooseHttpd(SGPropertyNode_ptr configNode)
: _server(NULL), _configNode(configNode)
{
}
MongooseHttpd::~MongooseHttpd()
{
mg_destroy_server(&_server);
}
void MongooseHttpd::init()
{
SGPropertyNode_ptr n = _configNode->getNode("uri-handler");
if (n.valid()) {
const char * uri;
if ((uri = n->getStringValue("screenshot"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding screenshot uri handler at " << uri);
_uriHandlers.push_back(new flightgear::http::ScreenshotUriHandler(uri));
}
if ((uri = n->getStringValue("property"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding screenshot property handler at " << uri);
_uriHandlers.push_back(new flightgear::http::PropertyUriHandler(uri));
}
if ((uri = n->getStringValue("json"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding json property handler at " << uri);
_uriHandlers.push_back(new flightgear::http::JsonUriHandler(uri));
}
if ((uri = n->getStringValue("run"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding run handler at " << uri);
_uriHandlers.push_back(new flightgear::http::RunUriHandler(uri));
}
if ((uri = n->getStringValue("navdb"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding navdb handler at " << uri);
_uriHandlers.push_back(new flightgear::http::NavdbUriHandler(uri));
}
}
_server = mg_create_server(this, MongooseHttpd::staticRequestHandler);
n = _configNode->getNode("options");
if (n.valid()) {
const string fgRoot = fgGetString("/sim/fg-root");
string docRoot = n->getStringValue("document-root", fgRoot.c_str() );
if (docRoot[0] != '/') docRoot.insert(0, "/").insert(0, fgRoot );
mg_set_option(_server, "document_root", docRoot.c_str());
mg_set_option(_server, "listening_port", n->getStringValue("listening-port", "8080"));
{
// build url rewrites relative to fg-root
string rewrites = n->getStringValue( "url-rewrites", "" );
string_list rwl = simgear::strutils::split( rewrites, "," );
rewrites.clear();
for( string_list::iterator it = rwl.begin(); it != rwl.end(); ++it ) {
string_list rw_entries = simgear::strutils::split( *it, "=" );
if( rw_entries.size() != 2 ) {
SG_LOG( SG_NETWORK,SG_WARN, "invalid entry '" << *it << "' in url-rewrites ignored." );
continue;
}
string & lhs = rw_entries[0];
string & rhs = rw_entries[1];
if( false == rewrites.empty()) rewrites.append(1,',');
rewrites.append( lhs ).append(1,'=');
if( rhs[0] == '/' ) {
rewrites.append( rhs );
} else {
rewrites.append( fgRoot ).append( 1, '/' ).append( rhs );
}
}
if( false == rewrites.empty() )
mg_set_option(_server, "url_rewrites", rewrites.c_str() );
}
mg_set_option(_server, "enable_directory_listing", n->getStringValue("enable-directory-listing", "yes"));
mg_set_option(_server, "idle_timeout_ms", n->getStringValue("idle-timeout-ms", "30000"));
mg_set_option(_server, "index_files", n->getStringValue("index-files", "index.html"));
}
}
void MongooseHttpd::bind()
{
}
void MongooseHttpd::unbind()
{
mg_destroy_server(&_server);
_uriHandlers.clear();
_propertyChangeObserver.clear();
}
void MongooseHttpd::shutdown()
{
}
void MongooseHttpd::update(double dt)
{
_propertyChangeObserver.check();
mg_poll_server(_server, 0);
mg_iterate_over_connections(_server, &MongooseHttpd::staticIterateCallback);
_propertyChangeObserver.uncheck();
}
/**
* A Helper class to create a HTTPRequest from a mongoose connection struct
*/
class MongooseHTTPRequest: public HTTPRequest {
private:
inline string NotNull(const char * cp, size_t n = string::npos)
/**
* Creates a std::string from a char pointer and an optionally given length
* If the pointer is NULL or the length is zero, return an empty string
* If no length is given, create a std::string from a c-string (up to the /0 terminator)
* If length is given, use as many chars as given in length (can exceed the /0 terminator)
*
* @param cp Points to the source of the string
* @param len The number of chars to copy to the new string (optional)
* @return a std::string containing a copy of the source
*/
static inline string NotNull(const char * cp, size_t len = string::npos)
{
if ( NULL == cp || 0 == n) return string("");
if (string::npos == n) return string(cp);
return string(cp, n);
if ( NULL == cp || 0 == len) return string("");
if (string::npos == len) return string(cp);
return string(cp, len);
}
public:
/**
* Constructs a HTTPRequest from a mongoose connection struct
* Copies all fields into STL compatible local elements, performs urlDecode etc.
*
* @param connection the mongoose connection struct with the source data
*/
MongooseHTTPRequest(struct mg_connection * connection)
{
Method = NotNull(connection->request_method);
@ -218,7 +127,15 @@ public:
}
string urlDecode(const string & s)
/**
* Decodes a URL encoded string
* replaces '+' by ' '
* replaces %nn hexdigits
*
* @param s The source string do decode
* @return The decoded String
*/
static string urlDecode(const string & s)
{
string r = "";
int max = s.length();
@ -241,15 +158,146 @@ public:
};
void MongooseHttpd::closeWebsocket(struct mg_connection * connection)
/**
* A FGHttpd implementation based on mongoose httpd
*
* Mongoose API is documented here: http://cesanta.com/docs/API.shtml
*/
class MongooseHttpd: public FGHttpd {
public:
/**
* Construct a MongooseHttpd object from options in a PropertyNode
*/
MongooseHttpd(SGPropertyNode_ptr);
/**
* Cleanup et.al.
*/
~MongooseHttpd();
/**
* override SGSubsystem::init()
*
* Reads the configuration PropertyNode, installs URIHandlers and configures mongoose
*/
void init();
/**
* override SGSubsystem::bind()
*
* Currently a noop
*/
void bind();
/**
* override SGSubsystem::unbind()
* shutdown of mongoose, clear connections, unregister URIHandlers
*/
void unbind();
/**
* overrride SGSubsystem::update()
* poll connections, check for changed properties
*/
void update(double dt);
/**
* Returns a URIHandler for the given uri
*
* @see URIHandlerMap::findHandler( const std::string & uri )
*/
SGSharedPtr<URIHandler> findHandler(const std::string & uri)
{
return _uriHandler.findHandler(uri);
}
Websocket * newWebsocket(const string & uri);
private:
int poll(struct mg_connection * connection);
int auth(struct mg_connection * connection);
int request(struct mg_connection * connection);
void close(struct mg_connection * connection);
static int staticRequestHandler(struct mg_connection *, mg_event event);
struct mg_server *_server;
SGPropertyNode_ptr _configNode;
typedef int (MongooseHttpd::*handler_t)(struct mg_connection *);
URIHandlerMap _uriHandler;
PropertyChangeObserver _propertyChangeObserver;
};
class MongooseConnection: public Connection {
public:
MongooseConnection(MongooseHttpd * httpd)
: _httpd(httpd)
{
}
virtual ~MongooseConnection();
virtual void close(struct mg_connection * connection) = 0;
virtual int poll(struct mg_connection * connection) = 0;
virtual int request(struct mg_connection * connection) = 0;
virtual void write(const char * data, size_t len)
{
if (_connection) mg_send_data(_connection, data, len);
}
static MongooseConnection * getConnection(MongooseHttpd * httpd, struct mg_connection * connection);
protected:
void setConnection(struct mg_connection * connection)
{
_connection = connection;
}
MongooseHttpd * _httpd;
struct mg_connection * _connection;
};
MongooseConnection::~MongooseConnection()
{
Websocket * websocket = static_cast<Websocket*>(connection->connection_param);
if ( NULL != websocket) websocket->close();
delete websocket;
}
class MongooseWebsocketWriter: public WebsocketWriter {
class RegularConnection: public MongooseConnection {
public:
RegularConnection(MongooseHttpd * httpd)
: MongooseConnection(httpd)
{
}
virtual ~RegularConnection()
{
}
virtual void close(struct mg_connection * connection);
virtual int poll(struct mg_connection * connection);
virtual int request(struct mg_connection * connection);
private:
SGSharedPtr<URIHandler> _handler;
};
class WebsocketConnection: public MongooseConnection {
public:
WebsocketConnection(MongooseHttpd * httpd)
: MongooseConnection(httpd), _websocket(NULL)
{
}
virtual ~WebsocketConnection()
{
delete _websocket;
}
virtual void close(struct mg_connection * connection);
virtual int poll(struct mg_connection * connection);
virtual int request(struct mg_connection * connection);
private:
class MongooseWebsocketWriter: public WebsocketWriter {
public:
MongooseWebsocketWriter(struct mg_connection * connection)
: _connection(connection)
{
@ -257,40 +305,42 @@ public:
virtual int writeToWebsocket(int opcode, const char * data, size_t len)
{
return mg_websocket_write( _connection, opcode, data, len );
return mg_websocket_write(_connection, opcode, data, len);
}
private:
private:
struct mg_connection * _connection;
};
Websocket * _websocket;
};
int MongooseHttpd::websocketHandler(struct mg_connection * connection)
MongooseConnection * MongooseConnection::getConnection(MongooseHttpd * httpd, struct mg_connection * connection)
{
MongooseHTTPRequest request(connection);
MongooseWebsocketWriter writer(connection);
Websocket * websocket = static_cast<Websocket*>(connection->connection_param);
if ( NULL == websocket) {
if (request.Uri.find("/PropertyListener") == 0) {
SG_LOG(SG_ALL, SG_INFO, "new PropertyChangeWebsocket for: " << request.Uri);
websocket = new PropertyChangeWebsocket(&_propertyChangeObserver);
connection->connection_param = websocket;
} else {
SG_LOG(SG_ALL, SG_ALERT, "httpd: unhandled websocket uri: " << request.Uri);
return MG_FALSE;
}
}
websocket->handleRequest(request, writer);
return MG_TRUE;
if (connection->connection_param) return static_cast<MongooseConnection*>(connection->connection_param);
MongooseConnection * c;
if (connection->is_websocket) c = new WebsocketConnection(httpd);
else c = new RegularConnection(httpd);
connection->connection_param = c;
return c;
}
int MongooseHttpd::requestHandler(struct mg_connection * connection)
int RegularConnection::request(struct mg_connection * connection)
{
setConnection(connection);
MongooseHTTPRequest request(connection);
SG_LOG(SG_NETWORK, SG_INFO, "RegularConnection::request for " << request.Uri);
// find a handler for the uri and remember it for possible polls on this connection
_handler = _httpd->findHandler(request.Uri);
if (false == _handler.valid()) {
// uri not registered - pass false to indicate we have not processed the request
return MG_FALSE;
}
// We handle this URI, prepare the response
HTTPResponse response;
response.Header["Server"] = "FlightGear/" FLIGHTGEAR_VERSION " Mongoose/" MONGOOSE_VERSION;
response.Header["Connection"] = "close";
response.Header["Connection"] = "keep-alive";
response.Header["Cache-Control"] = "no-cache";
{
char buf[64];
@ -299,64 +349,245 @@ int MongooseHttpd::requestHandler(struct mg_connection * connection)
response.Header["Date"] = buf;
}
bool processed = false;
for (URIHandlerMap::iterator it = _uriHandlers.begin(); it != _uriHandlers.end(); ++it) {
URIHandler * handler = *it;
if (request.Uri.find(handler->getUri()) == 0) {
processed = handler->handleRequest(request, response);
break;
}
}
if (processed) {
// hand the request over to the handler, returns true if request is finished,
// false the handler wants to get polled again (calling handlePoll() next time)
bool done = _handler->handleRequest(request, response, this);
// fill in the response header
mg_send_status(connection, response.StatusCode);
for (HTTPResponse::Header_t::const_iterator it = response.Header.begin(); it != response.Header.end(); ++it) {
const string name = it->first;
const string value = it->second;
if (name.empty() || value.empty()) continue;
mg_send_header(connection, name.c_str(), value.c_str());
}
mg_send_status(connection, response.StatusCode);
if (done || false == response.Content.empty()) {
SG_LOG(SG_NETWORK, SG_ALERT,
"RegularConnection::request() responding " << response.Content.length() << " Bytes, done=" << done);
mg_send_data(connection, response.Content.c_str(), response.Content.length());
}
return processed ? MG_TRUE : MG_FALSE;
return done ? MG_TRUE : MG_MORE;
}
int MongooseHttpd::iterateCallback(struct mg_connection * connection, mg_event event)
int RegularConnection::poll(struct mg_connection * connection)
{
if (connection->is_websocket && event == MG_POLL) {
Websocket * websocket = static_cast<Websocket*>(connection->connection_param);
if ( NULL == websocket) return MG_TRUE;
MongooseWebsocketWriter writer( connection );
websocket->update( writer );
setConnection(connection);
if (false == _handler.valid()) return MG_FALSE;
// only return MG_TRUE if we handle this request
return _handler->poll(this) ? MG_TRUE : MG_MORE;
}
void RegularConnection::close(struct mg_connection * connection)
{
setConnection(connection);
// nothing to close
}
void WebsocketConnection::close(struct mg_connection * connection)
{
setConnection(connection);
if ( NULL != _websocket) _websocket->close();
delete _websocket;
_websocket = NULL;
}
int WebsocketConnection::poll(struct mg_connection * connection)
{
setConnection(connection);
// we get polled before the first request came in but we know
// nothing about how to handle that before we know the URI.
// so simply ignore that poll
if ( NULL != _websocket) {
MongooseWebsocketWriter writer(connection);
_websocket->poll(writer);
}
return MG_TRUE; // return value is ignored
return MG_MORE;
}
int MongooseHttpd::staticIterateCallback(struct mg_connection * connection, mg_event event)
int WebsocketConnection::request(struct mg_connection * connection)
{
return static_cast<MongooseHttpd*>(connection->server_param)->iterateCallback(connection, event);
setConnection(connection);
MongooseHTTPRequest request(connection);
SG_LOG(SG_NETWORK, SG_INFO, "WebsocketConnection::request for " << request.Uri);
if ( NULL == _websocket) _websocket = _httpd->newWebsocket(request.Uri);
if ( NULL == _websocket) {
SG_LOG(SG_NETWORK, SG_WARN, "httpd: unhandled websocket uri: " << request.Uri);
return MG_TRUE; // close connection - good bye
}
MongooseWebsocketWriter writer(connection);
_websocket->handleRequest(request, writer);
return MG_MORE;
}
MongooseHttpd::MongooseHttpd(SGPropertyNode_ptr configNode)
: _server(NULL), _configNode(configNode)
{
}
MongooseHttpd::~MongooseHttpd()
{
mg_destroy_server(&_server);
}
void MongooseHttpd::init()
{
SGPropertyNode_ptr n = _configNode->getNode("uri-handler");
if (n.valid()) {
const char * uri;
if ((uri = n->getStringValue("screenshot"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding screenshot uri handler at " << uri);
_uriHandler.push_back(new flightgear::http::ScreenshotUriHandler(uri));
}
if ((uri = n->getStringValue("property"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding screenshot property handler at " << uri);
_uriHandler.push_back(new flightgear::http::PropertyUriHandler(uri));
}
if ((uri = n->getStringValue("json"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding json property handler at " << uri);
_uriHandler.push_back(new flightgear::http::JsonUriHandler(uri));
}
if ((uri = n->getStringValue("run"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding run handler at " << uri);
_uriHandler.push_back(new flightgear::http::RunUriHandler(uri));
}
if ((uri = n->getStringValue("navdb"))[0] != 0) {
SG_LOG(SG_NETWORK, SG_INFO, "httpd: adding navdb handler at " << uri);
_uriHandler.push_back(new flightgear::http::NavdbUriHandler(uri));
}
}
_server = mg_create_server(this, MongooseHttpd::staticRequestHandler);
n = _configNode->getNode("options");
if (n.valid()) {
const string fgRoot = fgGetString("/sim/fg-root");
string docRoot = n->getStringValue("document-root", fgRoot.c_str());
if (docRoot[0] != '/') docRoot.insert(0, "/").insert(0, fgRoot);
mg_set_option(_server, "document_root", docRoot.c_str());
mg_set_option(_server, "listening_port", n->getStringValue("listening-port", "8080"));
{
// build url rewrites relative to fg-root
string rewrites = n->getStringValue("url-rewrites", "");
string_list rwl = simgear::strutils::split(rewrites, ",");
rewrites.clear();
for (string_list::iterator it = rwl.begin(); it != rwl.end(); ++it) {
string_list rw_entries = simgear::strutils::split(*it, "=");
if (rw_entries.size() != 2) {
SG_LOG(SG_NETWORK, SG_WARN, "invalid entry '" << *it << "' in url-rewrites ignored.");
continue;
}
string & lhs = rw_entries[0];
string & rhs = rw_entries[1];
if (false == rewrites.empty()) rewrites.append(1, ',');
rewrites.append(lhs).append(1, '=');
if (rhs[0] == '/') {
rewrites.append(rhs);
} else {
rewrites.append(fgRoot).append(1, '/').append(rhs);
}
}
if (false == rewrites.empty()) mg_set_option(_server, "url_rewrites", rewrites.c_str());
}
mg_set_option(_server, "enable_directory_listing", n->getStringValue("enable-directory-listing", "yes"));
mg_set_option(_server, "idle_timeout_ms", n->getStringValue("idle-timeout-ms", "30000"));
mg_set_option(_server, "index_files", n->getStringValue("index-files", "index.html"));
}
}
void MongooseHttpd::bind()
{
}
void MongooseHttpd::unbind()
{
mg_destroy_server(&_server);
_uriHandler.clear();
_propertyChangeObserver.clear();
}
void MongooseHttpd::update(double dt)
{
_propertyChangeObserver.check();
mg_poll_server(_server, 0);
_propertyChangeObserver.uncheck();
}
int MongooseHttpd::poll(struct mg_connection * connection)
{
if ( NULL == connection->connection_param) return MG_FALSE; // connection not yet set up - ignore poll
return MongooseConnection::getConnection(this, connection)->poll(connection);
}
int MongooseHttpd::auth(struct mg_connection * connection)
{
// auth preceeds request for websockets and regular connections,
// and eventually the websocket has been already set up by mongoose
// use this to choose the connection type
MongooseConnection::getConnection(this, connection);
//return MongooseConnection::getConnection(this,connection)->auth(connection);
return MG_TRUE; // unrestricted access for now
}
int MongooseHttpd::request(struct mg_connection * connection)
{
return MongooseConnection::getConnection(this, connection)->request(connection);
}
void MongooseHttpd::close(struct mg_connection * connection)
{
MongooseConnection * c = MongooseConnection::getConnection(this, connection);
c->close(connection);
delete c;
}
Websocket * MongooseHttpd::newWebsocket(const string & uri)
{
if (uri.find("/PropertyListener") == 0) {
SG_LOG(SG_NETWORK, SG_INFO, "new PropertyChangeWebsocket for: " << uri);
return new PropertyChangeWebsocket(&_propertyChangeObserver);
}
return NULL;
}
int MongooseHttpd::staticRequestHandler(struct mg_connection * connection, mg_event event)
{
switch (event) {
case MG_REQUEST:
return
connection->is_websocket ?
static_cast<MongooseHttpd*>(connection->server_param)->websocketHandler(connection) :
static_cast<MongooseHttpd*>(connection->server_param)->requestHandler(connection);
case MG_POLL: // MG_TRUE: finished sending data, MG_MORE, call again
return static_cast<MongooseHttpd*>(connection->server_param)->poll(connection);
case MG_CLOSE:
if (connection->is_websocket) static_cast<MongooseHttpd*>(connection->server_param)->closeWebsocket(connection);
case MG_AUTH: // If callback returns MG_FALSE, authentication fails
return static_cast<MongooseHttpd*>(connection->server_param)->auth(connection);
case MG_REQUEST: // If callback returns MG_FALSE, Mongoose continues with req
return static_cast<MongooseHttpd*>(connection->server_param)->request(connection);
case MG_CLOSE: // Connection is closed, callback return value is ignored
static_cast<MongooseHttpd*>(connection->server_param)->close(connection);
return MG_TRUE;
case MG_AUTH:
return MG_TRUE; // allow anybody (for now)
case MG_HTTP_ERROR: // If callback returns MG_FALSE, Mongoose continues with err
return MG_FALSE; // we don't handle errors - let mongoose do the work
default:
// LUA not implemented - and will go away anyway
case MG_LUA: // Called before LSP page invoked
// client services not used/implemented. Signal 'close connection' to be sure
case MG_CONNECT: // If callback returns MG_FALSE, connect fails
case MG_REPLY: // If callback returns MG_FALSE, Mongoose closes connection
return MG_FALSE;
default:
return MG_FALSE; // keep compiler happy..
}
}

View file

@ -24,31 +24,97 @@
#include "HTTPRequest.hxx"
#include "HTTPResponse.hxx"
#include <simgear/structure/SGReferenced.hxx>
#include <simgear/structure/SGSharedPtr.hxx>
#include <string>
#include <map>
namespace flightgear {
namespace http {
class ConnectionData : public SGReferenced {
public:
// make this polymorphic
virtual ~ConnectionData() {}
};
class Connection {
public:
void put( const std::string & key, SGSharedPtr<ConnectionData> value ) {
connectionData[key] = value;
}
SGSharedPtr<ConnectionData> get(const std::string & key ) {
return connectionData[key];
}
void remove( const std::string & key ) {
connectionData.erase(key);
}
virtual void write(const char * data, size_t len) = 0;
private:
std::map<std::string,SGSharedPtr<ConnectionData> > connectionData;
};
/**
* A Base class for URI Handlers.
* Provides methods for handling a request and handling subsequent polls.
* All methods are implemented as noops and should be overridden by deriving classes
*/
class URIHandler : public SGReferenced {
public:
URIHandler( const char * uri ) : _uri(uri) {}
virtual ~URIHandler() {}
virtual bool handleRequest( const HTTPRequest & request, HTTPResponse & response ) {
if( request.Method == "GET" ) return handleGetRequest( request, response );
if( request.Method == "PUT" ) return handlePutRequest( request, response );
return false;
/**
* This method gets called from the httpd if a request has been detected on the connection
* @param request The HTTP Request filled in by the httpd
* @param response the HTTP Response to be filled in by the hander
* @param connection Connection specific information, can be used to store persistent state
* @return true if the request has been completely answered, false to get poll()ed
*/
virtual bool handleRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection = NULL ) {
if( request.Method == "GET" ) return handleGetRequest( request, response, connection );
if( request.Method == "PUT" ) return handlePutRequest( request, response, connection );
return true;
}
virtual bool handleGetRequest( const HTTPRequest & request, HTTPResponse & response ) {
return false;
/**
* Convenience method for GET Requests, gets called by handleRequest if not overridden
* @param request @see handleRequest()
* @param response @see handleRequest()
* @param connection @see handleRequest()
* @return @see handleRequest()
*
*/
virtual bool handleGetRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection ) {
return true;
}
virtual bool handlePutRequest( const HTTPRequest & request, HTTPResponse & response ) {
return false;
/**
* Convenience method for PUT Requests, gets called by handleRequest if not overridden
* @param request @see handleRequest()
* @param response @see handleRequest()
* @param connection @see handleRequest()
* @return @see handleRequest()
*
*/
virtual bool handlePutRequest( const HTTPRequest & request, HTTPResponse & response, Connection * connection ) {
return true;
}
/**
* This method gets called from the httpd if the preceding handleRequest() or poll() method returned false.
* @param connection @see handleRequest()
* @return @see handleRequest()
*/
virtual bool poll( Connection * connection ) { return false; }
/**
* Getter for the URI this handler serves
*
* @return the URI this handler serves
*/
const std::string & getUri() const { return _uri; }
private: