From fa2fb076090f2f76600c351419053dd6961253ae Mon Sep 17 00:00:00 2001 From: Robert Osfield Date: Tue, 30 Oct 2012 12:31:27 +0000 Subject: [PATCH] From Stephan Huber, RestHttpDevice plugin for support of remote application control via Rest http. --- CMakeLists.txt | 1 + CMakeModules/FindAsio.cmake | 20 + applications/osgviewer/osgviewer.cpp | 13 + applications/present3D/present3D.cpp | 30 ++ src/osgPlugins/CMakeLists.txt | 3 + src/osgPlugins/RestHttpDevice/CMakeLists.txt | 32 ++ .../ReaderWriterRestHttpDevice.cpp | 126 ++++++ .../RestHttpDevice/RestHttpDevice.cpp | 375 ++++++++++++++++++ .../RestHttpDevice/RestHttpDevice.hpp | 204 ++++++++++ src/osgPlugins/RestHttpDevice/connection.cpp | 93 +++++ src/osgPlugins/RestHttpDevice/connection.hpp | 75 ++++ src/osgPlugins/RestHttpDevice/header.hpp | 28 ++ .../RestHttpDevice/io_service_pool.cpp | 70 ++++ .../RestHttpDevice/io_service_pool.hpp | 56 +++ src/osgPlugins/RestHttpDevice/mime_types.cpp | 48 +++ src/osgPlugins/RestHttpDevice/mime_types.hpp | 27 ++ src/osgPlugins/RestHttpDevice/reply.cpp | 256 ++++++++++++ src/osgPlugins/RestHttpDevice/reply.hpp | 64 +++ src/osgPlugins/RestHttpDevice/request.hpp | 34 ++ .../RestHttpDevice/request_handler.cpp | 127 ++++++ .../RestHttpDevice/request_handler.hpp | 59 +++ .../RestHttpDevice/request_parser.cpp | 326 +++++++++++++++ .../RestHttpDevice/request_parser.hpp | 96 +++++ src/osgPlugins/RestHttpDevice/server.cpp | 62 +++ src/osgPlugins/RestHttpDevice/server.hpp | 64 +++ 25 files changed, 2289 insertions(+) create mode 100755 CMakeModules/FindAsio.cmake create mode 100755 src/osgPlugins/RestHttpDevice/CMakeLists.txt create mode 100755 src/osgPlugins/RestHttpDevice/ReaderWriterRestHttpDevice.cpp create mode 100755 src/osgPlugins/RestHttpDevice/RestHttpDevice.cpp create mode 100755 src/osgPlugins/RestHttpDevice/RestHttpDevice.hpp create mode 100755 src/osgPlugins/RestHttpDevice/connection.cpp create mode 100755 src/osgPlugins/RestHttpDevice/connection.hpp create mode 100755 src/osgPlugins/RestHttpDevice/header.hpp create mode 100755 src/osgPlugins/RestHttpDevice/io_service_pool.cpp create mode 100755 src/osgPlugins/RestHttpDevice/io_service_pool.hpp create mode 100755 src/osgPlugins/RestHttpDevice/mime_types.cpp create mode 100755 src/osgPlugins/RestHttpDevice/mime_types.hpp create mode 100755 src/osgPlugins/RestHttpDevice/reply.cpp create mode 100755 src/osgPlugins/RestHttpDevice/reply.hpp create mode 100755 src/osgPlugins/RestHttpDevice/request.hpp create mode 100755 src/osgPlugins/RestHttpDevice/request_handler.cpp create mode 100755 src/osgPlugins/RestHttpDevice/request_handler.hpp create mode 100755 src/osgPlugins/RestHttpDevice/request_parser.cpp create mode 100755 src/osgPlugins/RestHttpDevice/request_parser.hpp create mode 100755 src/osgPlugins/RestHttpDevice/server.cpp create mode 100755 src/osgPlugins/RestHttpDevice/server.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c11798815..13f6c669e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -516,6 +516,7 @@ ELSE() FIND_PACKAGE(GtkGl) FIND_PACKAGE(DirectInput) FIND_PACKAGE(NVTT) + FIND_PACKAGE(Asio) ENDIF() IF(CMAKE_MAJOR_VERSION EQUAL 2 AND CMAKE_MINOR_VERSION LESS 8) diff --git a/CMakeModules/FindAsio.cmake b/CMakeModules/FindAsio.cmake new file mode 100755 index 000000000..29b255e26 --- /dev/null +++ b/CMakeModules/FindAsio.cmake @@ -0,0 +1,20 @@ +# Locate ASIO-headers (http://think-async.com/Asio) +# This module defines +# ASIO_FOUND, if false, do not try to link to gdal +# ASIO_INCLUDE_DIR, where to find the headers +# +# Created by Stephan Maximilian Huber + +FIND_PATH(ASIO_INCLUDE_DIR + NAMES + asio.hpp + PATHS + /usr/include + /usr/local/include +) + +SET(ASIO_FOUND "NO") +FIND_PACKAGE( Boost 1.37 ) +IF(Boost_FOUND AND ASIO_INCLUDE_DIR) + SET(ASIO_FOUND "YES") +ENDIF() diff --git a/applications/osgviewer/osgviewer.cpp b/applications/osgviewer/osgviewer.cpp index 3563f6fe2..9b1506770 100644 --- a/applications/osgviewer/osgviewer.cpp +++ b/applications/osgviewer/osgviewer.cpp @@ -28,6 +28,8 @@ #include #include +#include + #include #include @@ -147,6 +149,7 @@ int main(int argc, char** argv) arguments.getApplicationUsage()->addCommandLineOption("--image ","Load an image and render it on a quad"); arguments.getApplicationUsage()->addCommandLineOption("--dem ","Load an image/DEM and render it on a HeightField"); arguments.getApplicationUsage()->addCommandLineOption("--login ","Provide authentication information for http file access."); + arguments.getApplicationUsage()->addCommandLineOption("--device ","add named device to the viewer"); osgViewer::Viewer viewer(arguments); @@ -182,6 +185,16 @@ int main(int argc, char** argv) ); } } + + std::string device; + while(arguments.read("--device", device)) + { + osg::ref_ptr dev = osgDB::readFile(device); + if (dev.valid()) + { + viewer.addDevice(dev.get()); + } + } // set up the camera manipulators. { diff --git a/applications/present3D/present3D.cpp b/applications/present3D/present3D.cpp index 82ced43ea..3dfe08a66 100644 --- a/applications/present3D/present3D.cpp +++ b/applications/present3D/present3D.cpp @@ -401,6 +401,36 @@ int main( int argc, char **argv ) viewer.readConfiguration(configurationFile); doSetViewer = false; } + + std::string device; + while (arguments.read("--device", device)) + { + osg::ref_ptr dev = osgDB::readFile(device); + if (dev.valid()) + { + viewer.addDevice(dev.get()); + } + } + + if (arguments.read("--http-control")) + { + + std::string server_address = "localhost"; + std::string server_port = "8080"; + std::string document_root = "htdocs"; + + while (arguments.read("--http-server-address", server_address)) {} + while (arguments.read("--http-server-port", server_port)) {} + while (arguments.read("--http-document-root", document_root)) {} + + osg::ref_ptr device_options = new osgDB::Options("documentRegisteredHandlers"); + + osg::ref_ptr rest_http_device = osgDB::readFile(server_address+":"+server_port+"/"+document_root+".resthttp", device_options); + if (rest_http_device.valid()) + { + viewer.addDevice(rest_http_device.get()); + } + } // set up stereo masks viewer.getCamera()->setCullMask(0xffffffff); diff --git a/src/osgPlugins/CMakeLists.txt b/src/osgPlugins/CMakeLists.txt index 388b49bef..7c9eb027e 100644 --- a/src/osgPlugins/CMakeLists.txt +++ b/src/osgPlugins/CMakeLists.txt @@ -264,6 +264,9 @@ IF (SDL_FOUND) ADD_SUBDIRECTORY(sdl) ENDIF(SDL_FOUND) +IF(ASIO_FOUND) + ADD_SUBDIRECTORY(RestHttpDevice) +ENDIF(ASIO_FOUND) ##########to get all the variables of Cmake #GET_CMAKE_PROPERTY(MYVARS VARIABLES) diff --git a/src/osgPlugins/RestHttpDevice/CMakeLists.txt b/src/osgPlugins/RestHttpDevice/CMakeLists.txt new file mode 100755 index 000000000..8464b9df9 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/CMakeLists.txt @@ -0,0 +1,32 @@ +INCLUDE_DIRECTORIES(${Boost_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${ASIO_INCLUDE_DIR}) + +SET(TARGET_SRC + connection.cpp + io_service_pool.cpp + mime_types.cpp + reply.cpp + request_handler.cpp + request_parser.cpp + server.cpp + RestHttpDevice.cpp + ReaderWriterRestHttpDevice.cpp +) + +SET(TARGET_H + connection.hpp + header.hpp + io_service_pool.hpp + mime_types.hpp + reply.hpp + request_handler.hpp + request_parser.hpp + request.hpp + server.hpp + RestHttpDevice.hpp +) + +SET(TARGET_ADDED_LIBRARIES osgPresentation ) + +#### end var setup ### +SETUP_PLUGIN(resthttp) diff --git a/src/osgPlugins/RestHttpDevice/ReaderWriterRestHttpDevice.cpp b/src/osgPlugins/RestHttpDevice/ReaderWriterRestHttpDevice.cpp new file mode 100755 index 000000000..b374294fb --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/ReaderWriterRestHttpDevice.cpp @@ -0,0 +1,126 @@ +/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2008 Robert Osfield + * + * This library is open source and may be redistributed and/or modified under + * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or + * (at your option) any later version. The full license is in LICENSE file + * included with this distribution, and on the openscenegraph.org website. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * OpenSceneGraph Public License for more details. +*/ + + +/* README: + * + * This code is loosely based on the QTKit implementation of Eric Wing, I removed + * some parts and added other parts. + * + * What's new: + * - it can handle URLs currently http and rtsp + * - it supports OS X's CoreVideo-technology, this will render the movie-frames + * into a bunch of textures. If you load your movie via readImageFile you'll + * get the standard behaviour, an ImageStream, where the data gets updated on + * every new video-frame. This may be slow. + * To get CoreVideo, you'll need to use readObjectFile and cast the result (if any) + * to an osg::Texture and use that as your video-texture. If you need access to the + * imagestream, just cast getImage to an image-stream. Please note, the data- + * property of the image-stream does NOT store the current frame, instead it's empty. + * + */ + + +#include +#include +#include +#include "RestHttpDevice.hpp" + + + + + +class ReaderWriterRestHttp : public osgDB::ReaderWriter +{ + public: + + ReaderWriterRestHttp() + { + supportsExtension("resthttp", "Virtual Device Integration via a HTTP-Server and a REST-interface"); + + supportsOption("documentRoot", "document root of asset files to server via the http-server"); + supportsOption("serverAddress", "server address to listen for incoming requests"); + supportsOption("serverPort", "server port to listen for incoming requests"); + supportsOption("documentRegisteredHandlers", "dump a documentation of all registered REST-handler to the console"); + + } + + virtual const char* className() const { return "Rest/HTTP Virtual Device Integration plugin"; } + + virtual ReadResult readObject(const std::string& file, const osgDB::ReaderWriter::Options* options =NULL) const + { + if (osgDB::getFileExtension(file) == "resthttp") + { + std::string document_root = options ? options->getPluginStringData("documentRoot") : "htdocs/"; + std::string server_address = options ? options->getPluginStringData("serverAddress") : "localhost"; + std::string server_port = options ? options->getPluginStringData("serverPort") : "8080"; + + // supported file-name scheme to get address, port and document-root: :/ + // example: '192.168.1.1:88888/var/www/htdocs' + + std::string file_wo_ext = osgDB::getNameLessAllExtensions(file); + if ((file_wo_ext.find('/') != std::string::npos) && (file_wo_ext.find(':') != std::string::npos)) { + std::string server_part = file_wo_ext.substr(0, file_wo_ext.find('/')); + document_root = file_wo_ext.substr(file_wo_ext.find('/')); + server_address = server_part.substr(0,server_part.find(':')); + server_port = server_part.substr(server_part.find(':') + 1); + } + + try + { + osg::ref_ptr device = new RestHttpDevice(server_address, server_port, document_root); + + device->addRequestHandler(new SendKeystrokeRequestHandler("/slide/first", osgGA::GUIEventAdapter::KEY_Home)); + device->addRequestHandler(new SendKeystrokeRequestHandler("/slide/last", osgGA::GUIEventAdapter::KEY_End)); + + device->addRequestHandler(new SendKeystrokeRequestHandler("/slide/next", osgGA::GUIEventAdapter::KEY_Right)); + device->addRequestHandler(new SendKeystrokeRequestHandler("/slide/previous", osgGA::GUIEventAdapter::KEY_Left)); + + device->addRequestHandler(new SendKeystrokeRequestHandler("/layer/next", osgGA::GUIEventAdapter::KEY_Down)); + device->addRequestHandler(new SendKeystrokeRequestHandler("/layer/previous", osgGA::GUIEventAdapter::KEY_Up)); + + device->addRequestHandler(new SendKeystrokeRequestHandler("/slideorlayer/next", osgGA::GUIEventAdapter::KEY_Page_Down)); + device->addRequestHandler(new SendKeystrokeRequestHandler("/slideorlayer/previous", osgGA::GUIEventAdapter::KEY_Page_Up)); + + device->addRequestHandler(new SendKeystrokeRequestHandler("/unpause", 'o')); + device->addRequestHandler(new SendKeystrokeRequestHandler("/pause", 'p')); + + + + if (options && (options->getPluginStringData("documentRegisteredHandlers") == "true")) + { + std::cout << *device << std::endl; + } + return device.release(); + } + catch(std::exception& e) + { + OSG_WARN << "ReaderWriterRestHttpDevice : could not create http-server! Reason: " << e.what() << std::endl; + return ReadResult::ERROR_IN_READING_FILE; + } + catch(...) + { + OSG_WARN << "ReaderWriterRestHttpDevice : could not create http-server, unknown excpetion thrown " << std::endl; + return ReadResult::ERROR_IN_READING_FILE; + } + + } + + return ReadResult::FILE_NOT_HANDLED; + } + +}; + +// now register with Registry to instantiate the above +// reader/writer. +REGISTER_OSGPLUGIN(resthttp, ReaderWriterRestHttp) diff --git a/src/osgPlugins/RestHttpDevice/RestHttpDevice.cpp b/src/osgPlugins/RestHttpDevice/RestHttpDevice.cpp new file mode 100755 index 000000000..87b3e386d --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/RestHttpDevice.cpp @@ -0,0 +1,375 @@ +/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2006 Robert Osfield + * + * This library is open source and may be redistributed and/or modified under + * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or + * (at your option) any later version. The full license is in LICENSE file + * included with this distribution, and on the openscenegraph.org website. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * OpenSceneGraph Public License for more details. +*/ + +#include "RestHttpDevice.hpp" +#include +#include +#include "request_handler.hpp" + + +class StandardRequestHandler : public RestHttpDevice::RequestHandler { +public: + StandardRequestHandler() : RestHttpDevice::RequestHandler("") {} + virtual bool operator()(const std::string& request_path, const std::string& full_request_path, const Arguments& arguments, http::server::reply& reply) + { + std::cout << "unhandled request: " << full_request_path << std::endl; + for(Arguments::const_iterator i = arguments.begin(); i != arguments.end(); ++i) + { + std::cout << " " << i->first << ": " << i->second << std::endl; + } + + return false; + } + + virtual void describeTo(std::ostream& out) const + { + out << getRequestPath() << ": fall-through request-handler, catches all requests w/o registered handler and report them to the console" << std::dec; + } +}; + +class HomeRequestHandler : public RestHttpDevice::RequestHandler { +public: + HomeRequestHandler() + : RestHttpDevice::RequestHandler("/home") + { + } + + virtual bool operator()(const std::string& request_path, const std::string& full_request_path, const Arguments& arguments, http::server::reply& reply) + { + double time = getLocalTime(arguments, reply); + getDevice()->getEventQueue()->keyPress(' ', time); + getDevice()->getEventQueue()->keyRelease(' ', time); + + return sendOkReply(reply); + } + + virtual void describeTo(std::ostream& out) const + { + out << getRequestPath() << ": sets the mouse-input-range arguments: 'x_min','y_min', 'x_max' and 'y_max'" << std::dec; + } +}; + +class SetMouseInputRangeRequestHandler : public RestHttpDevice::RequestHandler { +public: + SetMouseInputRangeRequestHandler() + : RestHttpDevice::RequestHandler("/mouse/set_input_range") + { + } + + virtual bool operator()(const std::string& request_path, const std::string& full_request_path, const Arguments& arguments, http::server::reply& reply) + { + int x_min, y_min, x_max, y_max; + + if ( getIntArgument(arguments, "x_min", reply, x_min) + && getIntArgument(arguments, "y_min", reply, y_min) + && getIntArgument(arguments, "x_max", reply, x_max) + && getIntArgument(arguments, "y_max", reply, y_max)) + { + getDevice()->getEventQueue()->setMouseInputRange(x_min, y_min, x_max, y_max); + } + + return sendOkReply(reply); + } + + virtual void describeTo(std::ostream& out) const + { + out << getRequestPath() << ": sets the mouse-input-range arguments: 'x_min','y_min', 'x_max' and 'y_max'" << std::dec; + } +}; + + + + + +class KeyCodeRequestHandler : public RestHttpDevice::RequestHandler { +public: + KeyCodeRequestHandler(bool handle_key_press) + : RestHttpDevice::RequestHandler(std::string("/key/") + ((handle_key_press) ? "press" : "release")) + , _handleKeyPress(handle_key_press) + { + } + + virtual bool operator()(const std::string& request_path, const std::string& full_request_path, const Arguments& arguments, http::server::reply& reply) + { + int keycode; + + if (getHexArgument(arguments, "code", reply, keycode)) + { + if (_handleKeyPress) + getDevice()->getEventQueue()->keyPress(keycode, getLocalTime(arguments, reply)); + else + getDevice()->getEventQueue()->keyRelease(keycode, getLocalTime(arguments, reply)); + } + + return sendOkReply(reply); + } + + virtual void describeTo(std::ostream& out) const + { + out << getRequestPath() << ": send KEY_" << (_handleKeyPress ? "DOWN" : "UP") <<", using hex-argument 'code' as keycode" << std::dec; + } +private: + bool _handleKeyPress; +}; + + +class MouseMotionRequestHandler : public RestHttpDevice::RequestHandler { +public: + MouseMotionRequestHandler() + : RestHttpDevice::RequestHandler("/mouse/motion") + { + } + + virtual bool operator()(const std::string& request_path, const std::string& full_request_path, const Arguments& arguments, http::server::reply& reply) + { + int x,y; + if (getIntArgument(arguments, "x", reply, x) && getIntArgument(arguments, "y", reply, y)) + { + double time_stamp = getTimeStamp(arguments, reply); + + if (getDevice()->isNewer(time_stamp)) + getDevice()->getEventQueue()->mouseMotion(x,y, getLocalTime(time_stamp)); + } + + return sendOkReply(reply); + } + + virtual void describeTo(std::ostream& out) const + { + out << getRequestPath() << ": send mouse motion using arguments 'x' and 'y' as coordinates" << std::dec; + } +private: + +}; + + +class MouseButtonRequestHandler : public RestHttpDevice::RequestHandler { +public: + enum Mode { PRESS, RELEASE, DOUBLE_PRESS}; + + MouseButtonRequestHandler(Mode mode) + : RestHttpDevice::RequestHandler("") + , _mode(mode) + { + switch(mode) { + case PRESS: + setRequestPath("/mouse/press"); + break; + case RELEASE: + setRequestPath("/mouse/release"); + break; + case DOUBLE_PRESS: + setRequestPath("/mouse/doublepress"); + break; + } + } + + virtual bool operator()(const std::string& request_path, const std::string& full_request_path, const Arguments& arguments, http::server::reply& reply) + { + int x,y, button; + + if (getIntArgument(arguments, "x", reply, x) + && getIntArgument(arguments, "y", reply, y) + && getIntArgument(arguments, "button", reply, button)) + { + switch (_mode) { + case PRESS: + getDevice()->getEventQueue()->mouseButtonPress(x,y, button, getLocalTime(arguments, reply)); + break; + case RELEASE: + getDevice()->getEventQueue()->mouseButtonRelease(x,y, button, getLocalTime(arguments, reply)); + break; + case DOUBLE_PRESS: + getDevice()->getEventQueue()->mouseDoubleButtonPress(x,y, button, getLocalTime(arguments, reply)); + break; + } + } + + return sendOkReply(reply); + } + + virtual void describeTo(std::ostream& out) const + { + out << getRequestPath() << ": send mouse "; + switch (_mode) { + case PRESS: + out << "press"; break; + case RELEASE: + out << "release"; break; + case DOUBLE_PRESS: + out << "double press"; break; + } + out << " using arguments 'x', 'y' and 'button' as coordinates" << std::dec; + } +private: + Mode _mode; +}; + + + + + + +class RequestHandlerDispatcherCallback: public http::server::request_handler::Callback { +public: + RequestHandlerDispatcherCallback(RestHttpDevice* parent) + : http::server::request_handler::Callback() + , _parent(parent) + { + } + + virtual bool operator()(const std::string& request_path, http::server::reply& rep); + + virtual std::string applyTemplateVars(const std::string& txt) { return txt; } +private: + RestHttpDevice* _parent; +}; + + +bool RequestHandlerDispatcherCallback::operator()(const std::string& request_path, http::server::reply& reply) +{ + return _parent->handleRequest(request_path, reply); +} + + +RestHttpDevice::RestHttpDevice(const std::string& listening_address, const std::string& listening_port, const std::string& doc_root) + : osgGA::Device() + , OpenThreads::Thread() + , _server(listening_address, listening_port, osgDB::findDataFile(doc_root), std::max(OpenThreads::GetNumberOfProcessors() - 1, 1)) + , _serverAddress(listening_address) + , _serverPort(listening_port) + , _documentRoot(doc_root) + , _firstEventLocalTimeStamp() + , _firstEventRemoteTimeStamp(-1) + , _lastEventRemoteTimeStamp(0) +{ + OSG_INFO << "RestHttpDevice :: listening on " << listening_address << ":" << listening_port << ", document root: " << doc_root << std::endl; + + if (osgDB::findDataFile(doc_root).empty()) + { + OSG_WARN << "RestHttpDevice :: warning, can't locate document-root '" << doc_root << "'for the http-server, starting anyway" << std::endl; + } + _server.setCallback(new RequestHandlerDispatcherCallback(this)); + + addRequestHandler(new KeyCodeRequestHandler(false)); + addRequestHandler(new KeyCodeRequestHandler(true)); + + addRequestHandler(new SetMouseInputRangeRequestHandler()); + addRequestHandler(new MouseMotionRequestHandler()); + addRequestHandler(new MouseButtonRequestHandler(MouseButtonRequestHandler::PRESS)); + addRequestHandler(new MouseButtonRequestHandler(MouseButtonRequestHandler::RELEASE)); + addRequestHandler(new MouseButtonRequestHandler(MouseButtonRequestHandler::DOUBLE_PRESS)); + + addRequestHandler(new HomeRequestHandler()); + + addRequestHandler(new StandardRequestHandler()); + + // start the thread + start(); +} + + +void RestHttpDevice::run() +{ + _server.run(); +} + + +RestHttpDevice::~RestHttpDevice() +{ + _server.stop(); + join(); +} + + +void RestHttpDevice::addRequestHandler(RequestHandler* handler) +{ + if (handler) + { + _map.insert(std::make_pair(handler->getRequestPath(), handler)); + handler->setDevice(this); + } +} + + +void RestHttpDevice::parseArguments(const std::string request_path, RequestHandler::Arguments& arguments) +{ + std::size_t pos = request_path.find('?'); + if (pos == std::string::npos) + return; + + std::vector list; + osgDB::split(request_path.substr(pos+1, std::string::npos), list, '&'); + for(std::vector::iterator i = list.begin(); i != list.end(); ++i) + { + std::vector sub_list; + osgDB::split(*i, sub_list, '='); + if (sub_list.size() == 2) + arguments[sub_list[0]] = sub_list[1]; + else if (sub_list.size() == 1) + arguments[sub_list[0]] = ""; + } +} + + +bool RestHttpDevice::handleRequest(const std::string& in_request_path, http::server::reply& reply) +{ + std::string request_path = in_request_path.substr(0, in_request_path.find('?')); + request_path += "/"; + RequestHandler::Arguments arguments; + bool arguments_parsed(false); + + std::size_t pos(std::string::npos); + bool handled(false); + do { + pos = request_path.find_last_of('/', pos-1); + if (pos != std::string::npos) + { + std::string mangled_path = request_path.substr(0, pos); + + std::pair range = _map.equal_range(mangled_path); + if (!arguments_parsed && (range.first != range.second)) + { + // parse arguments + parseArguments(in_request_path, arguments); + arguments_parsed = true; + } + for(RequestHandlerMap::iterator i = range.first; i != range.second; ++i) + { + if (i->second->operator()(mangled_path, in_request_path, arguments, reply) && !handled) + handled = true; + } + + } + } while ((pos != std::string::npos) && (pos > 0) && !handled); + + return handled; +} + + +void RestHttpDevice::describeTo(std::ostream& out) const +{ + out << "Server: " << _serverAddress << std::endl; + out << "Port: " << _serverPort << std::endl; + out << "Document-Root: " << _documentRoot << std::endl; + out << std::endl; + + for(RequestHandlerMap::const_iterator i = _map.begin(); i != _map.end(); ++i) + { + const RequestHandler* handler(i->second.get()); + handler->describeTo(out); + out << std::endl; + } + +} + diff --git a/src/osgPlugins/RestHttpDevice/RestHttpDevice.hpp b/src/osgPlugins/RestHttpDevice/RestHttpDevice.hpp new file mode 100755 index 000000000..3ae8ece93 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/RestHttpDevice.hpp @@ -0,0 +1,204 @@ +/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2006 Robert Osfield + * + * This library is open source and may be redistributed and/or modified under + * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or + * (at your option) any later version. The full license is in LICENSE file + * included with this distribution, and on the openscenegraph.org website. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * OpenSceneGraph Public License for more details. +*/ + + +#pragma once + +#include +#include +#include +#include "server.hpp" + +class RestHttpDevice : public osgGA::Device, OpenThreads::Thread { + +public: + + class RequestHandler : public osg::Referenced { + public: + typedef std::map Arguments; + RequestHandler(const std::string& request_path) + : osg::Referenced() + , _requestPath(request_path) + , _device(NULL) + { + } + + virtual bool operator()(const std::string& request_path, const std::string& full_request_path, const Arguments& arguments, http::server::reply& reply) = 0; + + const std::string& getRequestPath() const { return _requestPath; } + + virtual void describeTo(std::ostream& out) const + { + out << getRequestPath() << ": no description available"; + } + + protected: + void setDevice(RestHttpDevice* device) { _device = device; } + RestHttpDevice* getDevice() { return _device; } + + void reportMissingArgument(const std::string& argument, http::server::reply& reply) const + { + OSG_WARN << "RequestHandler :: missing argument '" << argument << "' for " << getRequestPath() << std::endl; + reply.content = "{ \"result\": 0, \"error\": \"missing argument '"+argument+"'\"}"; + reply.status = http::server::reply::ok; + } + + bool sendOkReply(http::server::reply& reply) + { + if (reply.content.empty()) + { + reply.status = http::server::reply::no_content; + } + return true; + } + + bool getStringArgument(const Arguments& arguments, const std::string& argument, http::server::reply& reply, std::string& result) const + { + Arguments::const_iterator itr = arguments.find(argument); + if (itr == arguments.end()) { + reportMissingArgument(argument, reply); + return false; + } + result = itr->second; + return true; + } + + bool getHexArgument(const Arguments& arguments, const std::string& argument, http::server::reply& reply, int& value) const + { + std::string hex_str; + if (!getStringArgument(arguments, argument, reply, hex_str)) + return false; + value = strtoul(hex_str.c_str(), NULL, 16); + return true; + } + + bool getIntArgument(const Arguments& arguments, const std::string& argument, http::server::reply& reply, int& value) const + { + std::string str; + if (!getStringArgument(arguments, argument, reply, str)) + return false; + value = strtol(str.c_str(), NULL, 10); + return true; + } + + bool getDoubleArgument(const Arguments& arguments, const std::string& argument, http::server::reply& reply, double& value) const + { + std::string str; + if (!getStringArgument(arguments, argument, reply, str)) + return false; + value = strtod(str.c_str(), NULL); + return true; + } + + /// set the request-path, works only from the constructor + void setRequestPath(const std::string& request_path) { _requestPath = request_path; } + protected: + + double getTimeStamp(const Arguments& arguments, http::server::reply& reply) + { + double time_stamp; + getDoubleArgument(arguments, "time", reply, time_stamp); + return time_stamp; + } + + double getLocalTime(const Arguments& arguments, http::server::reply& reply) + { + return getLocalTime(getTimeStamp(arguments, reply)); + } + + double getLocalTime(double time_stamp) + { + return getDevice()->getLocalTime(time_stamp); + } + + private: + std::string _requestPath; + RestHttpDevice* _device; + friend class RestHttpDevice; + }; + + + typedef std::multimap > RequestHandlerMap; + + RestHttpDevice(const std::string& listening_address, const std::string& listening_port, const std::string& doc_path); + ~RestHttpDevice(); + + void addRequestHandler(RequestHandler* handler); + + bool handleRequest(const std::string& request_path, http::server::reply& reply); + + virtual void run(); + + void describeTo(std::ostream& out) const; + + friend std::ostream& operator<<(std::ostream& out, const RestHttpDevice& device) + { + device.describeTo(out); + return out; + } + + double getLocalTime(double time_stamp) + { + if (_firstEventRemoteTimeStamp < 0) + { + _firstEventLocalTimeStamp = osg::Timer::instance()->time_s(); + _firstEventRemoteTimeStamp = time_stamp; + } + + return _firstEventLocalTimeStamp + (time_stamp - _firstEventRemoteTimeStamp) / 1000.0;; + } + + bool isNewer(double time_stamp) + { + bool is_newer(time_stamp > _lastEventRemoteTimeStamp); + if (is_newer) + _lastEventRemoteTimeStamp = time_stamp; + return is_newer; + } + + + + virtual void checkEvents() {} + +private: + void parseArguments(const std::string request_path, RequestHandler::Arguments& arguments); + http::server::server _server; + RequestHandlerMap _map; + std::string _serverAddress, _serverPort, _documentRoot; + double _firstEventLocalTimeStamp; + double _firstEventRemoteTimeStamp; + double _lastEventRemoteTimeStamp; + +}; + + + +class SendKeystrokeRequestHandler : public RestHttpDevice::RequestHandler { +public: + SendKeystrokeRequestHandler(const std::string& request_path, int key) : RestHttpDevice::RequestHandler(request_path), _key(key) {} + virtual bool operator()(const std::string& request_path, const std::string& full_request_path, const Arguments& arguments, http::server::reply& reply) + { + getDevice()->getEventQueue()->keyPress(_key); + getDevice()->getEventQueue()->keyRelease(_key); + + return sendOkReply(reply); + } + + virtual void describeTo(std::ostream& out) const + { + out << getRequestPath() << ": send KEY_DOWN + KEY_UP, code: 0x" << std::hex << _key << std::dec; + } +private: + int _key; +}; + diff --git a/src/osgPlugins/RestHttpDevice/connection.cpp b/src/osgPlugins/RestHttpDevice/connection.cpp new file mode 100755 index 000000000..c70375379 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/connection.cpp @@ -0,0 +1,93 @@ +// +// connection.cpp +// ~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include "connection.hpp" +#include +#include +#include "request_handler.hpp" + +namespace http { +namespace server { + +connection::connection(asio::io_service& io_service, + request_handler& handler) + : socket_(io_service), + request_handler_(handler) +{ +} + +asio::ip::tcp::socket& connection::socket() +{ + return socket_; +} + +void connection::start() +{ + socket_.async_read_some(asio::buffer(buffer_), + boost::bind(&connection::handle_read, shared_from_this(), + asio::placeholders::error, + asio::placeholders::bytes_transferred)); +} + +void connection::handle_read(const asio::error_code& e, + std::size_t bytes_transferred) +{ + if (!e) + { + boost::tribool result; + boost::tie(result, boost::tuples::ignore) = request_parser_.parse( + request_, buffer_.data(), buffer_.data() + bytes_transferred); + + if (result) + { + request_handler_.handle_request(request_, reply_); + asio::async_write(socket_, reply_.to_buffers(), + boost::bind(&connection::handle_write, shared_from_this(), + asio::placeholders::error)); + } + else if (!result) + { + reply_ = reply::stock_reply(reply::bad_request); + asio::async_write(socket_, reply_.to_buffers(), + boost::bind(&connection::handle_write, shared_from_this(), + asio::placeholders::error)); + } + else + { + socket_.async_read_some(asio::buffer(buffer_), + boost::bind(&connection::handle_read, shared_from_this(), + asio::placeholders::error, + asio::placeholders::bytes_transferred)); + } + } + + // If an error occurs then no new asynchronous operations are started. This + // means that all shared_ptr references to the connection object will + // disappear and the object will be destroyed automatically after this + // handler returns. The connection class's destructor closes the socket. +} + +void connection::handle_write(const asio::error_code& e) +{ + if (!e) + { + // Initiate graceful connection closure. + asio::error_code ignored_ec; + socket_.shutdown(asio::ip::tcp::socket::shutdown_both, ignored_ec); + } + + // No new asynchronous operations are started. This means that all shared_ptr + // references to the connection object will disappear and the object will be + // destroyed automatically after this handler returns. The connection class's + // destructor closes the socket. +} + +} // namespace server +} // namespace http diff --git a/src/osgPlugins/RestHttpDevice/connection.hpp b/src/osgPlugins/RestHttpDevice/connection.hpp new file mode 100755 index 000000000..e54b95045 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/connection.hpp @@ -0,0 +1,75 @@ +// +// connection.hpp +// ~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_CONNECTION_HPP +#define HTTP_SERVER_CONNECTION_HPP + +#include +#include +#include +#include +#include +#include "reply.hpp" +#include "request.hpp" +#include "request_handler.hpp" +#include "request_parser.hpp" + +namespace http { +namespace server { + +/// Represents a single connection from a client. +class connection + : public boost::enable_shared_from_this, + private boost::noncopyable +{ +public: + /// Construct a connection with the given io_service. + explicit connection(asio::io_service& io_service, + request_handler& handler); + + /// Get the socket associated with the connection. + asio::ip::tcp::socket& socket(); + + /// Start the first asynchronous operation for the connection. + void start(); + +private: + /// Handle completion of a read operation. + void handle_read(const asio::error_code& e, + std::size_t bytes_transferred); + + /// Handle completion of a write operation. + void handle_write(const asio::error_code& e); + + /// Socket for the connection. + asio::ip::tcp::socket socket_; + + /// The handler used to process the incoming request. + request_handler& request_handler_; + + /// Buffer for incoming data. + boost::array buffer_; + + /// The incoming request. + request request_; + + /// The parser for the incoming request. + request_parser request_parser_; + + /// The reply to be sent back to the client. + reply reply_; +}; + +typedef boost::shared_ptr connection_ptr; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_CONNECTION_HPP diff --git a/src/osgPlugins/RestHttpDevice/header.hpp b/src/osgPlugins/RestHttpDevice/header.hpp new file mode 100755 index 000000000..3c682de57 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/header.hpp @@ -0,0 +1,28 @@ +// +// header.hpp +// ~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_HEADER_HPP +#define HTTP_SERVER_HEADER_HPP + +#include + +namespace http { +namespace server { + +struct header +{ + std::string name; + std::string value; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_HEADER_HPP diff --git a/src/osgPlugins/RestHttpDevice/io_service_pool.cpp b/src/osgPlugins/RestHttpDevice/io_service_pool.cpp new file mode 100755 index 000000000..2af92d3e2 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/io_service_pool.cpp @@ -0,0 +1,70 @@ +// +// io_service_pool.cpp +// ~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include "server.hpp" +#include +#include +#include + +namespace http { +namespace server { + +io_service_pool::io_service_pool(std::size_t pool_size) + : next_io_service_(0) +{ + if (pool_size == 0) + throw std::runtime_error("io_service_pool size is 0"); + + // Give all the io_services work to do so that their run() functions will not + // exit until they are explicitly stopped. + for (std::size_t i = 0; i < pool_size; ++i) + { + io_service_ptr io_service(new asio::io_service); + work_ptr work(new asio::io_service::work(*io_service)); + io_services_.push_back(io_service); + work_.push_back(work); + } +} + +void io_service_pool::run() +{ + // Create a pool of threads to run all of the io_services. + std::vector > threads; + for (std::size_t i = 0; i < io_services_.size(); ++i) + { + boost::shared_ptr thread(new asio::thread( + boost::bind(&asio::io_service::run, io_services_[i]))); + threads.push_back(thread); + } + + // Wait for all threads in the pool to exit. + for (std::size_t i = 0; i < threads.size(); ++i) + threads[i]->join(); +} + +void io_service_pool::stop() +{ + // Explicitly stop all io_services. + for (std::size_t i = 0; i < io_services_.size(); ++i) + io_services_[i]->stop(); +} + +asio::io_service& io_service_pool::get_io_service() +{ + // Use a round-robin scheme to choose the next io_service to use. + asio::io_service& io_service = *io_services_[next_io_service_]; + ++next_io_service_; + if (next_io_service_ == io_services_.size()) + next_io_service_ = 0; + return io_service; +} + +} // namespace server +} // namespace http diff --git a/src/osgPlugins/RestHttpDevice/io_service_pool.hpp b/src/osgPlugins/RestHttpDevice/io_service_pool.hpp new file mode 100755 index 000000000..1d363a8ea --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/io_service_pool.hpp @@ -0,0 +1,56 @@ +// +// io_service_pool.hpp +// ~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_IO_SERVICE_POOL_HPP +#define HTTP_SERVER_IO_SERVICE_POOL_HPP + +#include +#include +#include +#include + +namespace http { +namespace server { + +/// A pool of io_service objects. +class io_service_pool + : private boost::noncopyable +{ +public: + /// Construct the io_service pool. + explicit io_service_pool(std::size_t pool_size); + + /// Run all io_service objects in the pool. + void run(); + + /// Stop all io_service objects in the pool. + void stop(); + + /// Get an io_service to use. + asio::io_service& get_io_service(); + +private: + typedef boost::shared_ptr io_service_ptr; + typedef boost::shared_ptr work_ptr; + + /// The pool of io_services. + std::vector io_services_; + + /// The work that keeps the io_services running. + std::vector work_; + + /// The next io_service to use for a connection. + std::size_t next_io_service_; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_IO_SERVICE_POOL_HPP diff --git a/src/osgPlugins/RestHttpDevice/mime_types.cpp b/src/osgPlugins/RestHttpDevice/mime_types.cpp new file mode 100755 index 000000000..84a28abd0 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/mime_types.cpp @@ -0,0 +1,48 @@ +// +// mime_types.cpp +// ~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include "mime_types.hpp" + +namespace http { +namespace server { +namespace mime_types { + +struct mapping +{ + const char* extension; + const char* mime_type; +} mappings[] = +{ + { "gif", "image/gif" }, + { "htm", "text/html" }, + { "html", "text/html" }, + { "jpg", "image/jpeg" }, + { "png", "image/png" }, + { "js", "text/javascript" }, + { "css", "text/css" }, + { 0, 0 } // Marks end of list. +}; + +std::string extension_to_type(const std::string& extension) +{ + for (mapping* m = mappings; m->extension; ++m) + { + if (m->extension == extension) + { + return m->mime_type; + } + } + + return "text/plain"; +} + +} // namespace mime_types +} // namespace server +} // namespace http diff --git a/src/osgPlugins/RestHttpDevice/mime_types.hpp b/src/osgPlugins/RestHttpDevice/mime_types.hpp new file mode 100755 index 000000000..56254cb74 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/mime_types.hpp @@ -0,0 +1,27 @@ +// +// mime_types.hpp +// ~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_MIME_TYPES_HPP +#define HTTP_SERVER_MIME_TYPES_HPP + +#include + +namespace http { +namespace server { +namespace mime_types { + +/// Convert a file extension into a MIME type. +std::string extension_to_type(const std::string& extension); + +} // namespace mime_types +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_MIME_TYPES_HPP diff --git a/src/osgPlugins/RestHttpDevice/reply.cpp b/src/osgPlugins/RestHttpDevice/reply.cpp new file mode 100755 index 000000000..aad8684ce --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/reply.cpp @@ -0,0 +1,256 @@ +// +// reply.cpp +// ~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include "reply.hpp" +#include +#include + +namespace http { +namespace server { + +namespace status_strings { + +const std::string ok = + "HTTP/1.0 200 OK\r\n"; +const std::string created = + "HTTP/1.0 201 Created\r\n"; +const std::string accepted = + "HTTP/1.0 202 Accepted\r\n"; +const std::string no_content = + "HTTP/1.0 204 No Content\r\n"; +const std::string multiple_choices = + "HTTP/1.0 300 Multiple Choices\r\n"; +const std::string moved_permanently = + "HTTP/1.0 301 Moved Permanently\r\n"; +const std::string moved_temporarily = + "HTTP/1.0 302 Moved Temporarily\r\n"; +const std::string not_modified = + "HTTP/1.0 304 Not Modified\r\n"; +const std::string bad_request = + "HTTP/1.0 400 Bad Request\r\n"; +const std::string unauthorized = + "HTTP/1.0 401 Unauthorized\r\n"; +const std::string forbidden = + "HTTP/1.0 403 Forbidden\r\n"; +const std::string not_found = + "HTTP/1.0 404 Not Found\r\n"; +const std::string internal_server_error = + "HTTP/1.0 500 Internal Server Error\r\n"; +const std::string not_implemented = + "HTTP/1.0 501 Not Implemented\r\n"; +const std::string bad_gateway = + "HTTP/1.0 502 Bad Gateway\r\n"; +const std::string service_unavailable = + "HTTP/1.0 503 Service Unavailable\r\n"; + +asio::const_buffer to_buffer(reply::status_type status) +{ + switch (status) + { + case reply::ok: + return asio::buffer(ok); + case reply::created: + return asio::buffer(created); + case reply::accepted: + return asio::buffer(accepted); + case reply::no_content: + return asio::buffer(no_content); + case reply::multiple_choices: + return asio::buffer(multiple_choices); + case reply::moved_permanently: + return asio::buffer(moved_permanently); + case reply::moved_temporarily: + return asio::buffer(moved_temporarily); + case reply::not_modified: + return asio::buffer(not_modified); + case reply::bad_request: + return asio::buffer(bad_request); + case reply::unauthorized: + return asio::buffer(unauthorized); + case reply::forbidden: + return asio::buffer(forbidden); + case reply::not_found: + return asio::buffer(not_found); + case reply::internal_server_error: + return asio::buffer(internal_server_error); + case reply::not_implemented: + return asio::buffer(not_implemented); + case reply::bad_gateway: + return asio::buffer(bad_gateway); + case reply::service_unavailable: + return asio::buffer(service_unavailable); + default: + return asio::buffer(internal_server_error); + } +} + +} // namespace status_strings + +namespace misc_strings { + +const char name_value_separator[] = { ':', ' ' }; +const char crlf[] = { '\r', '\n' }; + +} // namespace misc_strings + +std::vector reply::to_buffers() +{ + std::vector buffers; + buffers.push_back(status_strings::to_buffer(status)); + for (std::size_t i = 0; i < headers.size(); ++i) + { + header& h = headers[i]; + buffers.push_back(asio::buffer(h.name)); + buffers.push_back(asio::buffer(misc_strings::name_value_separator)); + buffers.push_back(asio::buffer(h.value)); + buffers.push_back(asio::buffer(misc_strings::crlf)); + } + buffers.push_back(asio::buffer(misc_strings::crlf)); + buffers.push_back(asio::buffer(content)); + return buffers; +} + +namespace stock_replies { + +const char ok[] = ""; +const char created[] = + "" + "Created" + "

201 Created

" + ""; +const char accepted[] = + "" + "Accepted" + "

202 Accepted

" + ""; +const char no_content[] = + "" + "No Content" + "

204 Content

" + ""; +const char multiple_choices[] = + "" + "Multiple Choices" + "

300 Multiple Choices

" + ""; +const char moved_permanently[] = + "" + "Moved Permanently" + "

301 Moved Permanently

" + ""; +const char moved_temporarily[] = + "" + "Moved Temporarily" + "

302 Moved Temporarily

" + ""; +const char not_modified[] = + "" + "Not Modified" + "

304 Not Modified

" + ""; +const char bad_request[] = + "" + "Bad Request" + "

400 Bad Request

" + ""; +const char unauthorized[] = + "" + "Unauthorized" + "

401 Unauthorized

" + ""; +const char forbidden[] = + "" + "Forbidden" + "

403 Forbidden

" + ""; +const char not_found[] = + "" + "Not Found" + "

404 Not Found

" + ""; +const char internal_server_error[] = + "" + "Internal Server Error" + "

500 Internal Server Error

" + ""; +const char not_implemented[] = + "" + "Not Implemented" + "

501 Not Implemented

" + ""; +const char bad_gateway[] = + "" + "Bad Gateway" + "

502 Bad Gateway

" + ""; +const char service_unavailable[] = + "" + "Service Unavailable" + "

503 Service Unavailable

" + ""; + +std::string to_string(reply::status_type status) +{ + switch (status) + { + case reply::ok: + return ok; + case reply::created: + return created; + case reply::accepted: + return accepted; + case reply::no_content: + return no_content; + case reply::multiple_choices: + return multiple_choices; + case reply::moved_permanently: + return moved_permanently; + case reply::moved_temporarily: + return moved_temporarily; + case reply::not_modified: + return not_modified; + case reply::bad_request: + return bad_request; + case reply::unauthorized: + return unauthorized; + case reply::forbidden: + return forbidden; + case reply::not_found: + return not_found; + case reply::internal_server_error: + return internal_server_error; + case reply::not_implemented: + return not_implemented; + case reply::bad_gateway: + return bad_gateway; + case reply::service_unavailable: + return service_unavailable; + default: + return internal_server_error; + } +} + +} // namespace stock_replies + +reply reply::stock_reply(reply::status_type status) +{ + reply rep; + rep.status = status; + rep.content = stock_replies::to_string(status); + rep.headers.resize(2); + rep.headers[0].name = "Content-Length"; + rep.headers[0].value = boost::lexical_cast(rep.content.size()); + rep.headers[1].name = "Content-Type"; + rep.headers[1].value = "text/html"; + return rep; +} + +} // namespace server +} // namespace http diff --git a/src/osgPlugins/RestHttpDevice/reply.hpp b/src/osgPlugins/RestHttpDevice/reply.hpp new file mode 100755 index 000000000..89f264a1a --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/reply.hpp @@ -0,0 +1,64 @@ +// +// reply.hpp +// ~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_REPLY_HPP +#define HTTP_SERVER_REPLY_HPP + +#include +#include +#include +#include "header.hpp" + +namespace http { +namespace server { + +/// A reply to be sent to a client. +struct reply +{ + /// The status of the reply. + enum status_type + { + ok = 200, + created = 201, + accepted = 202, + no_content = 204, + multiple_choices = 300, + moved_permanently = 301, + moved_temporarily = 302, + not_modified = 304, + bad_request = 400, + unauthorized = 401, + forbidden = 403, + not_found = 404, + internal_server_error = 500, + not_implemented = 501, + bad_gateway = 502, + service_unavailable = 503 + } status; + + /// The headers to be included in the reply. + std::vector
headers; + + /// The content to be sent in the reply. + std::string content; + + /// Convert the reply into a vector of buffers. The buffers do not own the + /// underlying memory blocks, therefore the reply object must remain valid and + /// not be changed until the write operation has completed. + std::vector to_buffers(); + + /// Get a stock reply. + static reply stock_reply(status_type status); +}; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_REPLY_HPP diff --git a/src/osgPlugins/RestHttpDevice/request.hpp b/src/osgPlugins/RestHttpDevice/request.hpp new file mode 100755 index 000000000..d69163a5f --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/request.hpp @@ -0,0 +1,34 @@ +// +// request.hpp +// ~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_REQUEST_HPP +#define HTTP_SERVER_REQUEST_HPP + +#include +#include +#include "header.hpp" + +namespace http { +namespace server { + +/// A request received from a client. +struct request +{ + std::string method; + std::string uri; + int http_version_major; + int http_version_minor; + std::vector
headers; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_REQUEST_HPP diff --git a/src/osgPlugins/RestHttpDevice/request_handler.cpp b/src/osgPlugins/RestHttpDevice/request_handler.cpp new file mode 100755 index 000000000..3921c1cc1 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/request_handler.cpp @@ -0,0 +1,127 @@ +// +// request_handler.cpp +// ~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include "request_handler.hpp" +#include +#include +#include +#include +#include "mime_types.hpp" +#include "reply.hpp" +#include "request.hpp" + +namespace http { +namespace server { + +request_handler::request_handler(const std::string& doc_root) + : doc_root_(doc_root) +{ +} + +void request_handler::handle_request(const request& req, reply& rep) +{ + // Decode url to path. + std::string request_path; + if (!url_decode(req.uri, request_path)) + { + rep = reply::stock_reply(reply::bad_request); + return; + } + + // Request path must be absolute and not contain "..". + if (request_path.empty() || request_path[0] != '/' + || request_path.find("..") != std::string::npos) + { + rep = reply::stock_reply(reply::bad_request); + return; + } + + // If path ends in slash (i.e. is a directory) then add "index.html". + if (request_path[request_path.size() - 1] == '/') + { + request_path += "index.html"; + } + + // Determine the file extension. + std::size_t last_slash_pos = request_path.find_last_of("/"); + std::size_t last_dot_pos = request_path.find_last_of("."); + std::string extension; + if (last_dot_pos != std::string::npos && last_dot_pos > last_slash_pos) + { + extension = request_path.substr(last_dot_pos + 1); + } + + // Open the file to send back. + std::string full_path = doc_root_ + request_path; + std::ifstream is(full_path.c_str(), std::ios::in | std::ios::binary); + if (!is ) + { + if (!_cb.valid() || (_cb->operator()(request_path, rep) == false)) { + rep = reply::stock_reply(reply::not_found); + } + return; + } + + // Fill out the reply to be sent to the client. + rep.status = reply::ok; + char buf[512]; + while (is.read(buf, sizeof(buf)).gcount() > 0) + rep.content.append(buf, is.gcount()); + + if (_cb.valid() && (mime_types::extension_to_type(extension) == "text/html")) + rep.content = _cb->applyTemplateVars(rep.content); + rep.headers.resize(2); + rep.headers[0].name = "Content-Length"; + rep.headers[0].value = boost::lexical_cast(rep.content.size()); + rep.headers[1].name = "Content-Type"; + rep.headers[1].value = mime_types::extension_to_type(extension); +} + +bool request_handler::url_decode(const std::string& in, std::string& out) +{ + out.clear(); + out.reserve(in.size()); + for (std::size_t i = 0; i < in.size(); ++i) + { + if (in[i] == '%') + { + if (i + 3 <= in.size()) + { + int value = 0; + std::istringstream is(in.substr(i + 1, 2)); + if (is >> std::hex >> value) + { + out += static_cast(value); + i += 2; + } + else + { + return false; + } + } + else + { + return false; + } + } + else if (in[i] == '+') + { + out += ' '; + } + else + { + out += in[i]; + } + } + return true; +} + +} // namespace server +} // namespace http diff --git a/src/osgPlugins/RestHttpDevice/request_handler.hpp b/src/osgPlugins/RestHttpDevice/request_handler.hpp new file mode 100755 index 000000000..5b2858ff1 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/request_handler.hpp @@ -0,0 +1,59 @@ +// +// request_handler.hpp +// ~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_REQUEST_HANDLER_HPP +#define HTTP_SERVER_REQUEST_HANDLER_HPP + +#include +#include + +#include + +namespace http { +namespace server { + +struct reply; +struct request; + +/// The common handler for all incoming requests. +class request_handler + : private boost::noncopyable +{ +public: + class Callback : public osg::Referenced { + public: + Callback() : osg::Referenced() {} + + virtual bool operator()(const std::string& request_path, reply& rep) = 0; + virtual std::string applyTemplateVars(const std::string& txt) = 0; + }; + + /// Construct with a directory containing files to be served. + explicit request_handler(const std::string& doc_root); + + /// Handle a request and produce a reply. + void handle_request(const request& req, reply& rep); + + void setCallback(Callback* cb) { _cb = cb; } + +private: + /// The directory containing the files to be served. + std::string doc_root_; + + /// Perform URL-decoding on a string. Returns false if the encoding was + /// invalid. + static bool url_decode(const std::string& in, std::string& out); + osg::observer_ptr _cb; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_REQUEST_HANDLER_HPP diff --git a/src/osgPlugins/RestHttpDevice/request_parser.cpp b/src/osgPlugins/RestHttpDevice/request_parser.cpp new file mode 100755 index 000000000..0a48350bb --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/request_parser.cpp @@ -0,0 +1,326 @@ +// +// request_parser.cpp +// ~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include "request_parser.hpp" +#include "request.hpp" + +namespace http { +namespace server { + +request_parser::request_parser() + : state_(method_start) +{ +} + +void request_parser::reset() +{ + state_ = method_start; +} + +boost::tribool request_parser::consume(request& req, char input) +{ + switch (state_) + { + case method_start: + if (!is_char(input) || is_ctl(input) || is_tspecial(input)) + { + return false; + } + else + { + state_ = method; + req.method.push_back(input); + return boost::indeterminate; + } + case method: + if (input == ' ') + { + state_ = uri; + return boost::indeterminate; + } + else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) + { + return false; + } + else + { + req.method.push_back(input); + return boost::indeterminate; + } + case uri_start: + if (is_ctl(input)) + { + return false; + } + else + { + state_ = uri; + req.uri.push_back(input); + return boost::indeterminate; + } + case uri: + if (input == ' ') + { + state_ = http_version_h; + return boost::indeterminate; + } + else if (is_ctl(input)) + { + return false; + } + else + { + req.uri.push_back(input); + return boost::indeterminate; + } + case http_version_h: + if (input == 'H') + { + state_ = http_version_t_1; + return boost::indeterminate; + } + else + { + return false; + } + case http_version_t_1: + if (input == 'T') + { + state_ = http_version_t_2; + return boost::indeterminate; + } + else + { + return false; + } + case http_version_t_2: + if (input == 'T') + { + state_ = http_version_p; + return boost::indeterminate; + } + else + { + return false; + } + case http_version_p: + if (input == 'P') + { + state_ = http_version_slash; + return boost::indeterminate; + } + else + { + return false; + } + case http_version_slash: + if (input == '/') + { + req.http_version_major = 0; + req.http_version_minor = 0; + state_ = http_version_major_start; + return boost::indeterminate; + } + else + { + return false; + } + case http_version_major_start: + if (is_digit(input)) + { + req.http_version_major = req.http_version_major * 10 + input - '0'; + state_ = http_version_major; + return boost::indeterminate; + } + else + { + return false; + } + case http_version_major: + if (input == '.') + { + state_ = http_version_minor_start; + return boost::indeterminate; + } + else if (is_digit(input)) + { + req.http_version_major = req.http_version_major * 10 + input - '0'; + return boost::indeterminate; + } + else + { + return false; + } + case http_version_minor_start: + if (is_digit(input)) + { + req.http_version_minor = req.http_version_minor * 10 + input - '0'; + state_ = http_version_minor; + return boost::indeterminate; + } + else + { + return false; + } + case http_version_minor: + if (input == '\r') + { + state_ = expecting_newline_1; + return boost::indeterminate; + } + else if (is_digit(input)) + { + req.http_version_minor = req.http_version_minor * 10 + input - '0'; + return boost::indeterminate; + } + else + { + return false; + } + case expecting_newline_1: + if (input == '\n') + { + state_ = header_line_start; + return boost::indeterminate; + } + else + { + return false; + } + case header_line_start: + if (input == '\r') + { + state_ = expecting_newline_3; + return boost::indeterminate; + } + else if (!req.headers.empty() && (input == ' ' || input == '\t')) + { + state_ = header_lws; + return boost::indeterminate; + } + else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) + { + return false; + } + else + { + req.headers.push_back(header()); + req.headers.back().name.push_back(input); + state_ = header_name; + return boost::indeterminate; + } + case header_lws: + if (input == '\r') + { + state_ = expecting_newline_2; + return boost::indeterminate; + } + else if (input == ' ' || input == '\t') + { + return boost::indeterminate; + } + else if (is_ctl(input)) + { + return false; + } + else + { + state_ = header_value; + req.headers.back().value.push_back(input); + return boost::indeterminate; + } + case header_name: + if (input == ':') + { + state_ = space_before_header_value; + return boost::indeterminate; + } + else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) + { + return false; + } + else + { + req.headers.back().name.push_back(input); + return boost::indeterminate; + } + case space_before_header_value: + if (input == ' ') + { + state_ = header_value; + return boost::indeterminate; + } + else + { + return false; + } + case header_value: + if (input == '\r') + { + state_ = expecting_newline_2; + return boost::indeterminate; + } + else if (is_ctl(input)) + { + return false; + } + else + { + req.headers.back().value.push_back(input); + return boost::indeterminate; + } + case expecting_newline_2: + if (input == '\n') + { + state_ = header_line_start; + return boost::indeterminate; + } + else + { + return false; + } + case expecting_newline_3: + return (input == '\n'); + default: + return false; + } +} + +bool request_parser::is_char(int c) +{ + return c >= 0 && c <= 127; +} + +bool request_parser::is_ctl(int c) +{ + return (c >= 0 && c <= 31) || (c == 127); +} + +bool request_parser::is_tspecial(int c) +{ + switch (c) + { + case '(': case ')': case '<': case '>': case '@': + case ',': case ';': case ':': case '\\': case '"': + case '/': case '[': case ']': case '?': case '=': + case '{': case '}': case ' ': case '\t': + return true; + default: + return false; + } +} + +bool request_parser::is_digit(int c) +{ + return c >= '0' && c <= '9'; +} + +} // namespace server +} // namespace http diff --git a/src/osgPlugins/RestHttpDevice/request_parser.hpp b/src/osgPlugins/RestHttpDevice/request_parser.hpp new file mode 100755 index 000000000..d2aad6535 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/request_parser.hpp @@ -0,0 +1,96 @@ +// +// request_parser.hpp +// ~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_REQUEST_PARSER_HPP +#define HTTP_SERVER_REQUEST_PARSER_HPP + +#include +#include + +namespace http { +namespace server { + +struct request; + +/// Parser for incoming requests. +class request_parser +{ +public: + /// Construct ready to parse the request method. + request_parser(); + + /// Reset to initial parser state. + void reset(); + + /// Parse some data. The tribool return value is true when a complete request + /// has been parsed, false if the data is invalid, indeterminate when more + /// data is required. The InputIterator return value indicates how much of the + /// input has been consumed. + template + boost::tuple parse(request& req, + InputIterator begin, InputIterator end) + { + while (begin != end) + { + boost::tribool result = consume(req, *begin++); + if (result || !result) + return boost::make_tuple(result, begin); + } + boost::tribool result = boost::indeterminate; + return boost::make_tuple(result, begin); + } + +private: + /// Handle the next character of input. + boost::tribool consume(request& req, char input); + + /// Check if a byte is an HTTP character. + static bool is_char(int c); + + /// Check if a byte is an HTTP control character. + static bool is_ctl(int c); + + /// Check if a byte is defined as an HTTP tspecial character. + static bool is_tspecial(int c); + + /// Check if a byte is a digit. + static bool is_digit(int c); + + /// The current state of the parser. + enum state + { + method_start, + method, + uri_start, + uri, + http_version_h, + http_version_t_1, + http_version_t_2, + http_version_p, + http_version_slash, + http_version_major_start, + http_version_major, + http_version_minor_start, + http_version_minor, + expecting_newline_1, + header_line_start, + header_lws, + header_name, + space_before_header_value, + header_value, + expecting_newline_2, + expecting_newline_3 + } state_; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_REQUEST_PARSER_HPP diff --git a/src/osgPlugins/RestHttpDevice/server.cpp b/src/osgPlugins/RestHttpDevice/server.cpp new file mode 100755 index 000000000..e43f8193f --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/server.cpp @@ -0,0 +1,62 @@ +// +// server.cpp +// ~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include "server.hpp" +#include + +namespace http { +namespace server { + +server::server(const std::string& address, const std::string& port, + const std::string& doc_root, std::size_t io_service_pool_size) + : io_service_pool_(io_service_pool_size), + acceptor_(io_service_pool_.get_io_service()), + new_connection_(new connection( + io_service_pool_.get_io_service(), request_handler_)), + request_handler_(doc_root) +{ + // Open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR). + asio::ip::tcp::resolver resolver(acceptor_.get_io_service()); + asio::ip::tcp::resolver::query query(address, port); + asio::ip::tcp::endpoint endpoint = *resolver.resolve(query); + acceptor_.open(endpoint.protocol()); + acceptor_.set_option(asio::ip::tcp::acceptor::reuse_address(true)); + acceptor_.bind(endpoint); + acceptor_.listen(); + acceptor_.async_accept(new_connection_->socket(), + boost::bind(&server::handle_accept, this, + asio::placeholders::error)); +} + +void server::run() +{ + io_service_pool_.run(); +} + +void server::stop() +{ + io_service_pool_.stop(); +} + +void server::handle_accept(const asio::error_code& e) +{ + if (!e) + { + new_connection_->start(); + new_connection_.reset(new connection( + io_service_pool_.get_io_service(), request_handler_)); + acceptor_.async_accept(new_connection_->socket(), + boost::bind(&server::handle_accept, this, + asio::placeholders::error)); + } +} + +} // namespace server +} // namespace http diff --git a/src/osgPlugins/RestHttpDevice/server.hpp b/src/osgPlugins/RestHttpDevice/server.hpp new file mode 100755 index 000000000..a14f26360 --- /dev/null +++ b/src/osgPlugins/RestHttpDevice/server.hpp @@ -0,0 +1,64 @@ +// +// server.hpp +// ~~~~~~~~~~ +// +// Copyright (c) 2003-2011 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef HTTP_SERVER_SERVER_HPP +#define HTTP_SERVER_SERVER_HPP + +#include +#include +#include +#include +#include +#include "connection.hpp" +#include "io_service_pool.hpp" +#include "request_handler.hpp" + +namespace http { +namespace server { + +/// The top-level class of the HTTP server. +class server + : private boost::noncopyable +{ +public: + /// Construct the server to listen on the specified TCP address and port, and + /// serve up files from the given directory. + explicit server(const std::string& address, const std::string& port, + const std::string& doc_root, std::size_t io_service_pool_size); + + /// Run the server's io_service loop. + void run(); + + /// Stop the server. + void stop(); + + void setCallback(request_handler::Callback* cb) { request_handler_.setCallback(cb); } + +private: + /// Handle completion of an asynchronous accept operation. + void handle_accept(const asio::error_code& e); + + /// The pool of io_service objects used to perform asynchronous operations. + io_service_pool io_service_pool_; + + /// Acceptor used to listen for incoming connections. + asio::ip::tcp::acceptor acceptor_; + + /// The next connection to be accepted. + connection_ptr new_connection_; + + /// The handler for all incoming requests. + request_handler request_handler_; +}; + +} // namespace server +} // namespace http + +#endif // HTTP_SERVER_SERVER_HPP