diff options
Diffstat (limited to 'backends/networking/sdl_net/localwebserver.cpp')
-rw-r--r-- | backends/networking/sdl_net/localwebserver.cpp | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/backends/networking/sdl_net/localwebserver.cpp b/backends/networking/sdl_net/localwebserver.cpp new file mode 100644 index 0000000000..fdc89b51e4 --- /dev/null +++ b/backends/networking/sdl_net/localwebserver.cpp @@ -0,0 +1,446 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#define FORBIDDEN_SYMBOL_ALLOW_ALL + +#include "backends/networking/sdl_net/localwebserver.h" +#include "backends/networking/sdl_net/getclienthandler.h" +#include "common/memstream.h" +#include "common/str.h" +#include "common/system.h" +#include "common/timer.h" +#include "common/translation.h" +#include <SDL_net.h> +#include <common/config-manager.h> + +#ifdef POSIX +#include <sys/types.h> +#include <ifaddrs.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#endif + +namespace Common { +class MemoryReadWriteStream; + +DECLARE_SINGLETON(Networking::LocalWebserver); + +} + +namespace Networking { + +LocalWebserver::LocalWebserver(): _set(nullptr), _serverSocket(nullptr), _timerStarted(false), + _stopOnIdle(false), _minimalMode(false), _clients(0), _idlingFrames(0), _serverPort(DEFAULT_SERVER_PORT) { + addPathHandler("/", &_indexPageHandler); + addPathHandler("/files", &_filesPageHandler); + addPathHandler("/create", &_createDirectoryHandler); + addPathHandler("/download", &_downloadFileHandler); + addPathHandler("/upload", &_uploadFileHandler); + addPathHandler("/list", &_listAjaxHandler); + addPathHandler("/filesAJAX", &_filesAjaxPageHandler); + _defaultHandler = &_resourceHandler; +} + +LocalWebserver::~LocalWebserver() { + stop(); +} + +void localWebserverTimer(void *ignored) { + LocalServer.handle(); +} + +void LocalWebserver::startTimer(int interval) { + Common::TimerManager *manager = g_system->getTimerManager(); + if (manager->installTimerProc(localWebserverTimer, interval, 0, "Networking::LocalWebserver's Timer")) { + _timerStarted = true; + } else { + warning("Failed to install Networking::LocalWebserver's timer"); + } +} + +void LocalWebserver::stopTimer() { + Common::TimerManager *manager = g_system->getTimerManager(); + manager->removeTimerProc(localWebserverTimer); + _timerStarted = false; +} + +void LocalWebserver::start(bool useMinimalMode) { + _handleMutex.lock(); + _serverPort = getPort(); + _stopOnIdle = false; + if (_timerStarted) { + _handleMutex.unlock(); + return; + } + _minimalMode = useMinimalMode; + startTimer(); + + // Create a listening TCP socket + IPaddress ip; + if (SDLNet_ResolveHost(&ip, NULL, _serverPort) == -1) { + error("LocalWebserver: SDLNet_ResolveHost: %s\n", SDLNet_GetError()); + } + + resolveAddress(&ip); + + _serverSocket = SDLNet_TCP_Open(&ip); + if (!_serverSocket) { + warning("LocalWebserver: SDLNet_TCP_Open: %s", SDLNet_GetError()); + stopTimer(); + g_system->displayMessageOnOSD(_("Failed to start local webserver.\nCheck whether selected port is not used by another application and try again.")); + _handleMutex.unlock(); + return; + } + + // Create a socket set + _set = SDLNet_AllocSocketSet(MAX_CONNECTIONS + 1); //one more for our server socket + if (!_set) { + error("LocalWebserver: SDLNet_AllocSocketSet: %s\n", SDLNet_GetError()); + } + + int numused = SDLNet_TCP_AddSocket(_set, _serverSocket); + if (numused == -1) { + error("LocalWebserver: SDLNet_AddSocket: %s\n", SDLNet_GetError()); + } + _handleMutex.unlock(); +} + +void LocalWebserver::stop() { + _handleMutex.lock(); + if (_timerStarted) + stopTimer(); + + if (_serverSocket) { + SDLNet_TCP_Close(_serverSocket); + _serverSocket = nullptr; + } + + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + _client[i].close(); + + _clients = 0; + + if (_set) { + SDLNet_FreeSocketSet(_set); + _set = nullptr; + } + _handleMutex.unlock(); +} + +void LocalWebserver::stopOnIdle() { _stopOnIdle = true; } + +void LocalWebserver::addPathHandler(Common::String path, BaseHandler *handler) { + if (_pathHandlers.contains(path)) + warning("LocalWebserver::addPathHandler: path already had a handler"); + _pathHandlers[path] = handler; +} + +Common::String LocalWebserver::getAddress() { return _address; } + +IndexPageHandler &LocalWebserver::indexPageHandler() { return _indexPageHandler; } + +bool LocalWebserver::isRunning() { + bool result = false; + _handleMutex.lock(); + result = _timerStarted; + _handleMutex.unlock(); + return result; +} + +uint32 LocalWebserver::getPort() { +#ifdef NETWORKING_LOCALWEBSERVER_ENABLE_PORT_OVERRIDE + if (ConfMan.hasKey("local_server_port")) + return ConfMan.getInt("local_server_port"); +#endif + return DEFAULT_SERVER_PORT; +} + +void LocalWebserver::handle() { + _handleMutex.lock(); + int numready = SDLNet_CheckSockets(_set, 0); + if (numready == -1) { + error("LocalWebserver: SDLNet_CheckSockets: %s\n", SDLNet_GetError()); + } else if (numready) { + acceptClient(); + } + + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + handleClient(i); + + _clients = 0; + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + if (_client[i].state() != INVALID) + ++_clients; + + if (_clients == 0) + ++_idlingFrames; + else + _idlingFrames = 0; + + if (_idlingFrames > FRAMES_PER_SECOND && _stopOnIdle) { + _handleMutex.unlock(); + stop(); + return; + } + + _handleMutex.unlock(); +} + +void LocalWebserver::handleClient(uint32 i) { + switch (_client[i].state()) { + case INVALID: + return; + case READING_HEADERS: + _client[i].readHeaders(); + break; + case READ_HEADERS: { + // decide what to do next with that client + // check whether we know a handler for such URL + BaseHandler *handler = nullptr; + if (_pathHandlers.contains(_client[i].path())) { + handler = _pathHandlers[_client[i].path()]; + } else { + // try default handler + handler = _defaultHandler; + } + + // if server's in "minimal mode", only handlers which support it are used + if (handler && (!_minimalMode || handler->minimalModeSupported())) + handler->handle(_client[i]); + + if (_client[i].state() == BEING_HANDLED || _client[i].state() == INVALID) + break; + + // if no handler, answer with default BAD REQUEST + // fallthrough + } + + case BAD_REQUEST: + setClientGetHandler(_client[i], "<html><head><title>ScummVM - Bad Request</title></head><body>BAD REQUEST</body></html>", 400); + break; + case BEING_HANDLED: + _client[i].handle(); + break; + } +} + +void LocalWebserver::acceptClient() { + if (!SDLNet_SocketReady(_serverSocket)) + return; + + TCPsocket client = SDLNet_TCP_Accept(_serverSocket); + if (!client) + return; + + if (_clients == MAX_CONNECTIONS) { //drop the connection + SDLNet_TCP_Close(client); + return; + } + + ++_clients; + for (uint32 i = 0; i < MAX_CONNECTIONS; ++i) + if (_client[i].state() == INVALID) { + _client[i].open(_set, client); + break; + } +} + +void LocalWebserver::resolveAddress(void *ipAddress) { + IPaddress *ip = (IPaddress *)ipAddress; + + // not resolved + _address = Common::String::format("http://127.0.0.1:%u/ (unresolved)", _serverPort); + + // default way (might work everywhere, surely works on Windows) + const char *name = SDLNet_ResolveIP(ip); + if (name == NULL) { + warning("LocalWebserver: SDLNet_ResolveIP: %s", SDLNet_GetError()); + } else { + IPaddress localIp; + if (SDLNet_ResolveHost(&localIp, name, _serverPort) == -1) { + warning("LocalWebserver: SDLNet_ResolveHost: %s", SDLNet_GetError()); + } else { + _address = Common::String::format( + "http://%u.%u.%u.%u:%u/", + localIp.host & 0xFF, (localIp.host >> 8) & 0xFF, (localIp.host >> 16) & 0xFF, (localIp.host >> 24) & 0xFF, + _serverPort + ); + } + } + + // check that our trick worked + if (_address.contains("/127.0.0.1:") || _address.contains("localhost") || _address.contains("/0.0.0.0:")) + warning("LocalWebserver: Failed to resolve IP with the default way"); + else + return; + + // if not - try platform-specific +#ifdef POSIX + struct ifaddrs *ifAddrStruct = NULL; + void *tmpAddrPtr = NULL; + + getifaddrs(&ifAddrStruct); + + for (struct ifaddrs *i = ifAddrStruct; i != NULL; i = i->ifa_next) { + if (!i->ifa_addr) { + continue; + } + + Common::String addr; + + // IPv4 + if (i->ifa_addr->sa_family == AF_INET) { + tmpAddrPtr = &((struct sockaddr_in *)i->ifa_addr)->sin_addr; + char addressBuffer[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN); + debug(9, "%s IP Address %s", i->ifa_name, addressBuffer); + addr = addressBuffer; + } + + // IPv6 + /* + if (i->ifa_addr->sa_family == AF_INET6) { + tmpAddrPtr = &((struct sockaddr_in6 *)i->ifa_addr)->sin6_addr; + char addressBuffer[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, tmpAddrPtr, addressBuffer, INET6_ADDRSTRLEN); + debug(9, "%s IP Address %s", i->ifa_name, addressBuffer); + addr = addressBuffer; + } + */ + + if (addr.empty()) + continue; + + // ignored IPv4 addresses + if (addr.equals("127.0.0.1") || addr.equals("0.0.0.0") || addr.equals("localhost")) + continue; + + // ignored IPv6 addresses + /* + if (addr.equals("::1")) + continue; + */ + + // use the address found + _address = "http://" + addr + Common::String::format(":%u/", _serverPort); + } + + if (ifAddrStruct != NULL) freeifaddrs(ifAddrStruct); +#endif +} + +void LocalWebserver::setClientGetHandler(Client &client, Common::String response, long code, const char *mimeType) { + byte *data = new byte[response.size()]; + memcpy(data, response.c_str(), response.size()); + Common::MemoryReadStream *stream = new Common::MemoryReadStream(data, response.size(), DisposeAfterUse::YES); + setClientGetHandler(client, stream, code, mimeType); +} + +void LocalWebserver::setClientGetHandler(Client &client, Common::SeekableReadStream *responseStream, long code, const char *mimeType) { + GetClientHandler *handler = new GetClientHandler(responseStream); + handler->setResponseCode(code); + if (mimeType) + handler->setHeader("Content-Type", mimeType); + client.setHandler(handler); +} + +void LocalWebserver::setClientRedirectHandler(Client &client, Common::String response, Common::String location, const char *mimeType) { + byte *data = new byte[response.size()]; + memcpy(data, response.c_str(), response.size()); + Common::MemoryReadStream *stream = new Common::MemoryReadStream(data, response.size(), DisposeAfterUse::YES); + setClientRedirectHandler(client, stream, location, mimeType); +} + +void LocalWebserver::setClientRedirectHandler(Client &client, Common::SeekableReadStream *responseStream, Common::String location, const char *mimeType) { + GetClientHandler *handler = new GetClientHandler(responseStream); + handler->setResponseCode(302); //redirect + handler->setHeader("Location", location); + if (mimeType) + handler->setHeader("Content-Type", mimeType); + client.setHandler(handler); +} + +namespace { +int hexDigit(char c) { + if ('0' <= c && c <= '9') return c - '0'; + if ('A' <= c && c <= 'F') return c - 'A' + 10; + if ('a' <= c && c <= 'f') return c - 'a' + 10; + return -1; +} +} + +Common::String LocalWebserver::urlDecode(Common::String value) { + Common::String result = ""; + uint32 size = value.size(); + for (uint32 i = 0; i < size; ++i) { + if (value[i] == '+') { + result += ' '; + continue; + } + + if (value[i] == '%' && i + 2 < size) { + int d1 = hexDigit(value[i + 1]); + int d2 = hexDigit(value[i + 2]); + if (0 <= d1 && d1 < 16 && 0 <= d2 && d2 < 16) { + result += (char)(d1 * 16 + d2); + i = i + 2; + continue; + } + } + + result += value[i]; + } + return result; +} + +namespace { +bool isQueryUnreserved(char c) { + return ( + ('0' <= c && c <= '9') || + ('A' <= c && c <= 'Z') || + ('a' <= c && c <= 'z') || + c == '-' || c == '_' || c == '.' || c == '!' || + c == '~' || c == '*' || c == '\'' || c == '(' || c == ')' + ); +} +} + +Common::String LocalWebserver::urlEncodeQueryParameterValue(Common::String value) { + //OK chars = alphanum | "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" + //reserved for query are ";", "/", "?", ":", "@", "&", "=", "+", "," + //that means these must be encoded too or otherwise they could malform the query + Common::String result = ""; + char hexChar[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + for (uint32 i = 0; i < value.size(); ++i) { + char c = value[i]; + if (isQueryUnreserved(c)) + result += c; + else { + result += '%'; + result += hexChar[(c >> 4) & 0xF]; + result += hexChar[c & 0xF]; + } + } + return result; +} + +} // End of namespace Networking |