diff --git a/src/Network/http/JsonUriHandler.cxx b/src/Network/http/JsonUriHandler.cxx index 6d7b08f9d..1a29f319f 100644 --- a/src/Network/http/JsonUriHandler.cxx +++ b/src/Network/http/JsonUriHandler.cxx @@ -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; diff --git a/src/Network/http/JsonUriHandler.hxx b/src/Network/http/JsonUriHandler.hxx index 463c5f227..a0c672f91 100644 --- a/src/Network/http/JsonUriHandler.hxx +++ b/src/Network/http/JsonUriHandler.hxx @@ -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 diff --git a/src/Network/http/NavdbUriHandler.cxx b/src/Network/http/NavdbUriHandler.cxx index 2b568dc1c..5e73d7aff 100644 --- a/src/Network/http/NavdbUriHandler.cxx +++ b/src/Network/http/NavdbUriHandler.cxx @@ -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"; diff --git a/src/Network/http/NavdbUriHandler.hxx b/src/Network/http/NavdbUriHandler.hxx index 949ed52d0..40f6f71ce 100644 --- a/src/Network/http/NavdbUriHandler.hxx +++ b/src/Network/http/NavdbUriHandler.hxx @@ -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 diff --git a/src/Network/http/PropertyChangeObserver.cxx b/src/Network/http/PropertyChangeObserver.cxx index 6bb555c62..b15a18947 100644 --- a/src/Network/http/PropertyChangeObserver.cxx +++ b/src/Network/http/PropertyChangeObserver.cxx @@ -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; diff --git a/src/Network/http/PropertyChangeWebsocket.cxx b/src/Network/http/PropertyChangeWebsocket.cxx index c0576455f..ecfdefbb4 100644 --- a/src/Network/http/PropertyChangeWebsocket.cxx +++ b/src/Network/http/PropertyChangeWebsocket.cxx @@ -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)"); diff --git a/src/Network/http/PropertyChangeWebsocket.hxx b/src/Network/http/PropertyChangeWebsocket.hxx index 752df1888..f195c4917 100644 --- a/src/Network/http/PropertyChangeWebsocket.hxx +++ b/src/Network/http/PropertyChangeWebsocket.hxx @@ -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; diff --git a/src/Network/http/PropertyUriHandler.cxx b/src/Network/http/PropertyUriHandler.cxx index edbc0f676..c9ebda7d8 100644 --- a/src/Network/http/PropertyUriHandler.cxx +++ b/src/Network/http/PropertyUriHandler.cxx @@ -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 diff --git a/src/Network/http/PropertyUriHandler.hxx b/src/Network/http/PropertyUriHandler.hxx index 4557552f5..6faa15995 100644 --- a/src/Network/http/PropertyUriHandler.hxx +++ b/src/Network/http/PropertyUriHandler.hxx @@ -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 diff --git a/src/Network/http/RunUriHandler.cxx b/src/Network/http/RunUriHandler.cxx index d682e65b7..09252fdfd 100644 --- a/src/Network/http/RunUriHandler.cxx +++ b/src/Network/http/RunUriHandler.cxx @@ -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; } diff --git a/src/Network/http/RunUriHandler.hxx b/src/Network/http/RunUriHandler.hxx index 0d7b0ecfe..ccc12a106 100644 --- a/src/Network/http/RunUriHandler.hxx +++ b/src/Network/http/RunUriHandler.hxx @@ -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 diff --git a/src/Network/http/ScreenshotUriHandler.cxx b/src/Network/http/ScreenshotUriHandler.cxx index 42204e136..e856ba6ef 100644 --- a/src/Network/http/ScreenshotUriHandler.cxx +++ b/src/Network/http/ScreenshotUriHandler.cxx @@ -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 +#include +#include +#include + +#include +#include +#include
+#include + +#include +#include + 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) = 0; + virtual ~ImageReadyListener() + { + } +}; + +class StringReadyListener { +public: + virtual void stringReady(const std::string &) = 0; + virtual ~StringReadyListener() + { + } +}; + +struct ImageCompressionTask { + StringReadyListener * stringReadyListener; + string format; + osg::ref_ptr 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 TaskList; + TaskList _tasks; +}; + +typedef simgear::Singleton 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() ); - return true; + osg::ref_ptr 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 osgworks 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 lock(_lock); + hasSubscribers = !_subscribers.empty(); + + } + if (hasSubscribers) { + osg::ref_ptr 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 lock(_lock); + while (!_subscribers.empty()) { + try { + _subscribers.back()->imageReady(image); + } + catch (...) { + } + _subscribers.pop_back(); + + } + } + } + } + + void subscribe(ImageReadyListener * subscriber) + { + OpenThreads::ScopedLock lock(_lock); + _subscribers.push_back(subscriber); + } + + void unsubscribe(ImageReadyListener * subscriber) + { + OpenThreads::ScopedLock lock(_lock); + _subscribers.remove( subscriber ); + } + +private: + mutable list _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(camera->getFinalDrawCallback()); + if ( NULL == _screenshotCallback) + throw sg_error("Can't find ScreenshotCallback"); + + requestScreenshot(); + } + + virtual ~ScreenshotRequest() + { + _screenshotCallback->unsubscribe(this); + } + + virtual void imageReady(osg::ref_ptr 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 lock(_lock); + _compressedData = s; + } + + string getScreenshot() + { + string reply; + { + // called from the main loop + OpenThreads::ScopedLock 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; + try { + SG_LOG(SG_NETWORK, SG_DEBUG, "new ScreenshotRequest("<getType()).append("\r\nContent-Length:"); + s += boost::lexical_cast(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 diff --git a/src/Network/http/ScreenshotUriHandler.hxx b/src/Network/http/ScreenshotUriHandler.hxx index 954dbb306..858a32861 100644 --- a/src/Network/http/ScreenshotUriHandler.hxx +++ b/src/Network/http/ScreenshotUriHandler.hxx @@ -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 diff --git a/src/Network/http/Websocket.hxx b/src/Network/http/Websocket.hxx index c37470797..679d33a68 100644 --- a/src/Network/http/Websocket.hxx +++ b/src/Network/http/Websocket.hxx @@ -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; }; diff --git a/src/Network/http/httpd.cxx b/src/Network/http/httpd.cxx index b1e9cbb58..881ce5170 100644 --- a/src/Network/http/httpd.cxx +++ b/src/Network/http/httpd.cxx @@ -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 > { 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 > 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 findHandler(const std::string & uri) + { + for (iterator it = begin(); it != end(); ++it) { + SGSharedPtr handler = *it; + // check if the request-uri starts with the registered uri-string + if (0 == uri.find(handler->getUri())) return handler; + } + return SGSharedPtr(); + } }; -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,56 +158,189 @@ public: }; -void MongooseHttpd::closeWebsocket(struct mg_connection * connection) -{ - Websocket * websocket = static_cast(connection->connection_param); - if ( NULL != websocket) websocket->close(); - delete websocket; -} - -class MongooseWebsocketWriter: public WebsocketWriter { +/** + * A FGHttpd implementation based on mongoose httpd + * + * Mongoose API is documented here: http://cesanta.com/docs/API.shtml + */ +class MongooseHttpd: public FGHttpd { public: - MongooseWebsocketWriter(struct mg_connection * connection) - : _connection(connection) + + /** + * 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 findHandler(const std::string & uri) { + return _uriHandler.findHandler(uri); } - virtual int writeToWebsocket(int opcode, const char * data, size_t len) - { - return mg_websocket_write( _connection, opcode, data, len ); - } + Websocket * newWebsocket(const string & uri); + private: - struct mg_connection * _connection; + 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; }; -int MongooseHttpd::websocketHandler(struct mg_connection * connection) -{ - MongooseHTTPRequest request(connection); - MongooseWebsocketWriter writer(connection); +class MongooseConnection: public Connection { +public: + MongooseConnection(MongooseHttpd * httpd) + : _httpd(httpd) + { + } + virtual ~MongooseConnection(); - Websocket * websocket = static_cast(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; - } + 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); } - websocket->handleRequest(request, writer); - return MG_TRUE; + 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() +{ } -int MongooseHttpd::requestHandler(struct mg_connection * connection) +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 _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) + { + } + + virtual int writeToWebsocket(int opcode, const char * data, size_t len) + { + return mg_websocket_write(_connection, opcode, data, len); + } + private: + struct mg_connection * _connection; + }; + Websocket * _websocket; +}; + +MongooseConnection * MongooseConnection::getConnection(MongooseHttpd * httpd, struct mg_connection * connection) { + if (connection->connection_param) return static_cast(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 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; - } + // 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()); } - - if (processed) { - 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(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(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(connection->server_param)->websocketHandler(connection) : - static_cast(connection->server_param)->requestHandler(connection); + case MG_POLL: // MG_TRUE: finished sending data, MG_MORE, call again + return static_cast(connection->server_param)->poll(connection); - case MG_CLOSE: - if (connection->is_websocket) static_cast(connection->server_param)->closeWebsocket(connection); + case MG_AUTH: // If callback returns MG_FALSE, authentication fails + return static_cast(connection->server_param)->auth(connection); + + case MG_REQUEST: // If callback returns MG_FALSE, Mongoose continues with req + return static_cast(connection->server_param)->request(connection); + + case MG_CLOSE: // Connection is closed, callback return value is ignored + static_cast(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.. } } diff --git a/src/Network/http/urihandler.hxx b/src/Network/http/urihandler.hxx index ec6d5611d..1072e90e7 100644 --- a/src/Network/http/urihandler.hxx +++ b/src/Network/http/urihandler.hxx @@ -24,31 +24,97 @@ #include "HTTPRequest.hxx" #include "HTTPResponse.hxx" #include +#include #include #include namespace flightgear { namespace http { +class ConnectionData : public SGReferenced { +public: + // make this polymorphic + virtual ~ConnectionData() {} +}; + +class Connection { +public: + void put( const std::string & key, SGSharedPtr value ) { + connectionData[key] = value; + } + SGSharedPtr 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 > 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: