// 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
//
// 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()
+{
+ 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)
{
- response.Header["Content-Type"] = "image/jpeg";
-// string response = "Screenshot - not yet implemented :-(";
-// mg_send_data( connection, response.c_str(), response.length() );
- return true;
+ _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
const char * PROPERTY_ROOT = "/sim/http";
+/**
+ * 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:
+ /**
+ * 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>();
+ }
+};
+
+/**
+ * A Helper class to create a HTTPRequest from a mongoose connection struct
+ */
+class MongooseHTTPRequest: public HTTPRequest {
+private:
+ /**
+ * 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 == 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);
+ Uri = urlDecode(NotNull(connection->uri));
+ HttpVersion = NotNull(connection->http_version);
+ QueryString = urlDecode(NotNull(connection->query_string));
+
+ remoteAddress = NotNull(connection->remote_ip);
+ remotePort = connection->remote_port;
+ localAddress = NotNull(connection->local_ip);
+ localPort = connection->local_port;
+
+ using namespace simgear::strutils;
+ string_list pairs = split(string(QueryString), "&");
+ for (string_list::iterator it = pairs.begin(); it != pairs.end(); ++it) {
+ string_list nvp = split(*it, "=");
+ if (nvp.size() != 2) continue;
+ RequestVariables.insert(make_pair(nvp[0], nvp[1]));
+ }
+
+ for (int i = 0; i < connection->num_headers; i++)
+ HeaderVariables[connection->http_headers[i].name] = connection->http_headers[i].value;
+
+ Content = NotNull(connection->content, connection->content_len);
+
+ }
+
+ /**
+ * 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();
+ int a, b;
+ for (int i = 0; i < max; i++) {
+ if (s[i] == '+') {
+ r += ' ';
+ } else if (s[i] == '%' && i + 2 < max && isxdigit(s[i + 1]) && isxdigit(s[i + 2])) {
+ i++;
+ a = isdigit(s[i]) ? s[i] - '0' : toupper(s[i]) - 'A' + 10;
+ i++;
+ b = isdigit(s[i]) ? s[i] - '0' : toupper(s[i]) - 'A' + 10;
+ r += (char) (a * 16 + b);
+ } else {
+ r += s[i];
+ }
+ }
+ return r;
+ }
+
+};
+
+/**
+ * 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();
- void shutdown();
+
+ /**
+ * 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 requestHandler(struct mg_connection *);
- int websocketHandler(struct mg_connection *);
- void closeWebsocket(struct mg_connection *);
- int iterateCallback(struct mg_connection *, mg_event event);
+ 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);
- 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;
+ 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()
+{
+}
+
+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)
+ {
+ }
+
+ 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<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 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"] = "keep-alive";
+ response.Header["Cache-Control"] = "no-cache";
+ {
+ char buf[64];
+ time_t now = time(NULL);
+ strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&now));
+ response.Header["Date"] = buf;
+ }
+
+ // 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 (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 done ? MG_TRUE : MG_MORE;
+}
+
+int RegularConnection::poll(struct mg_connection * connection)
+{
+ 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_MORE;
+}
+
+int WebsocketConnection::request(struct mg_connection * connection)
+{
+ 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)
{
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));
+ _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);
- _uriHandlers.push_back(new flightgear::http::PropertyUriHandler(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);
- _uriHandlers.push_back(new flightgear::http::JsonUriHandler(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);
- _uriHandlers.push_back(new flightgear::http::RunUriHandler(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);
- _uriHandlers.push_back(new flightgear::http::NavdbUriHandler(uri));
+ _uriHandler.push_back(new flightgear::http::NavdbUriHandler(uri));
}
}
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 );
+ 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, "," );
+ 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." );
+ 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 );
+ 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 );
+ rewrites.append(fgRoot).append(1, '/').append(rhs);
}
}
- if( false == rewrites.empty() )
- mg_set_option(_server, "url_rewrites", rewrites.c_str() );
+ 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"));
void MongooseHttpd::unbind()
{
mg_destroy_server(&_server);
- _uriHandlers.clear();
+ _uriHandler.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();
}
-class MongooseHTTPRequest: public HTTPRequest {
-private:
- inline string NotNull(const char * cp, size_t n = string::npos)
- {
- if ( NULL == cp || 0 == n) return string("");
- if (string::npos == n) return string(cp);
- return string(cp, n);
- }
-
-public:
- MongooseHTTPRequest(struct mg_connection * connection)
- {
- Method = NotNull(connection->request_method);
- Uri = urlDecode(NotNull(connection->uri));
- HttpVersion = NotNull(connection->http_version);
- QueryString = urlDecode(NotNull(connection->query_string));
-
- remoteAddress = NotNull(connection->remote_ip);
- remotePort = connection->remote_port;
- localAddress = NotNull(connection->local_ip);
- localPort = connection->local_port;
-
- using namespace simgear::strutils;
- string_list pairs = split(string(QueryString), "&");
- for (string_list::iterator it = pairs.begin(); it != pairs.end(); ++it) {
- string_list nvp = split(*it, "=");
- if (nvp.size() != 2) continue;
- RequestVariables.insert(make_pair(nvp[0], nvp[1]));
- }
-
- for (int i = 0; i < connection->num_headers; i++)
- HeaderVariables[connection->http_headers[i].name] = connection->http_headers[i].value;
-
- Content = NotNull(connection->content, connection->content_len);
-
- }
-
- string urlDecode(const string & s)
- {
- string r = "";
- int max = s.length();
- int a, b;
- for (int i = 0; i < max; i++) {
- if (s[i] == '+') {
- r += ' ';
- } else if (s[i] == '%' && i + 2 < max && isxdigit(s[i + 1]) && isxdigit(s[i + 2])) {
- i++;
- a = isdigit(s[i]) ? s[i] - '0' : toupper(s[i]) - 'A' + 10;
- i++;
- b = isdigit(s[i]) ? s[i] - '0' : toupper(s[i]) - 'A' + 10;
- r += (char) (a * 16 + b);
- } else {
- r += s[i];
- }
- }
- return r;
- }
-
-};
-
-void MongooseHttpd::closeWebsocket(struct mg_connection * connection)
+int MongooseHttpd::poll(struct mg_connection * connection)
{
- Websocket * websocket = static_cast<Websocket*>(connection->connection_param);
- if ( NULL != websocket) websocket->close();
- delete websocket;
-}
-
-class MongooseWebsocketWriter: public WebsocketWriter {
-public:
- MongooseWebsocketWriter(struct mg_connection * connection)
- : _connection(connection)
- {
- }
+ if ( NULL == connection->connection_param) return MG_FALSE; // connection not yet set up - ignore poll
- virtual int writeToWebsocket(int opcode, const char * data, size_t len)
- {
- return mg_websocket_write( _connection, opcode, data, len );
- }
-private:
- struct mg_connection * _connection;
-};
+ return MongooseConnection::getConnection(this, connection)->poll(connection);
+}
-int MongooseHttpd::websocketHandler(struct mg_connection * connection)
+int MongooseHttpd::auth(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;
-
+ // 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::requestHandler(struct mg_connection * connection)
+int MongooseHttpd::request(struct mg_connection * connection)
{
- MongooseHTTPRequest request(connection);
- HTTPResponse response;
- response.Header["Server"] = "FlightGear/" FLIGHTGEAR_VERSION " Mongoose/" MONGOOSE_VERSION;
- response.Header["Connection"] = "close";
- response.Header["Cache-Control"] = "no-cache";
- {
- char buf[64];
- time_t now = time(NULL);
- strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&now));
- 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) {
- 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);
- mg_send_data(connection, response.Content.c_str(), response.Content.length());
- }
-
- return processed ? MG_TRUE : MG_FALSE;
+ return MongooseConnection::getConnection(this, connection)->request(connection);
}
-int MongooseHttpd::iterateCallback(struct mg_connection * connection, mg_event event)
+void MongooseHttpd::close(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 );
- }
- return MG_TRUE; // return value is ignored
+ MongooseConnection * c = MongooseConnection::getConnection(this, connection);
+ c->close(connection);
+ delete c;
}
-
-int MongooseHttpd::staticIterateCallback(struct mg_connection * connection, mg_event event)
+Websocket * MongooseHttpd::newWebsocket(const string & uri)
{
- return static_cast<MongooseHttpd*>(connection->server_param)->iterateCallback(connection, event);
+ 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_CLOSE:
- if (connection->is_websocket) static_cast<MongooseHttpd*>(connection->server_param)->closeWebsocket(connection);
+ case MG_POLL: // MG_TRUE: finished sending data, MG_MORE, call again
+ return static_cast<MongooseHttpd*>(connection->server_param)->poll(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..
}
}