mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-25 00:04:49 +02:00
Merge pull request #11152 from unknownbrackets/debugger-http
Move internal webserver to Core, add WebSocket stuff
This commit is contained in:
@@ -893,6 +893,8 @@ add_library(native STATIC
|
||||
ext/native/base/stringutil.h
|
||||
ext/native/base/timeutil.cpp
|
||||
ext/native/base/timeutil.h
|
||||
ext/native/data/base64.cpp
|
||||
ext/native/data/base64.h
|
||||
ext/native/data/compression.cpp
|
||||
ext/native/data/compression.h
|
||||
ext/native/file/chunk_file.cpp
|
||||
@@ -985,6 +987,8 @@ add_library(native STATIC
|
||||
ext/native/net/sinks.h
|
||||
ext/native/net/url.cpp
|
||||
ext/native/net/url.h
|
||||
ext/native/net/websocket_server.cpp
|
||||
ext/native/net/websocket_server.h
|
||||
ext/native/profiler/profiler.cpp
|
||||
ext/native/profiler/profiler.h
|
||||
ext/native/thin3d/thin3d.cpp
|
||||
@@ -1389,6 +1393,8 @@ add_library(${CoreLibName} ${CoreLinkType}
|
||||
Core/HDRemaster.cpp
|
||||
Core/HDRemaster.h
|
||||
Core/ThreadEventQueue.h
|
||||
Core/WebServer.cpp
|
||||
Core/WebServer.h
|
||||
Core/Debugger/Breakpoints.cpp
|
||||
Core/Debugger/Breakpoints.h
|
||||
Core/Debugger/DebugInterface.h
|
||||
|
||||
@@ -511,6 +511,7 @@
|
||||
<InlineFunctionExpansion Condition="'$(Configuration)|$(Platform)'=='Release|x64'">AnySuitable</InlineFunctionExpansion>
|
||||
</ClCompile>
|
||||
<ClCompile Include="WaveFile.cpp" />
|
||||
<ClCompile Include="WebServer.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\ext\disarm.h" />
|
||||
@@ -746,6 +747,7 @@
|
||||
<ClInclude Include="Util\ppge_atlas.h" />
|
||||
<ClInclude Include="..\ext\xxhash.h" />
|
||||
<ClInclude Include="WaveFile.h" />
|
||||
<ClInclude Include="WebServer.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\android\jni\Android.mk" />
|
||||
|
||||
@@ -689,6 +689,9 @@
|
||||
<ClCompile Include="Replay.cpp">
|
||||
<Filter>Core</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="WebServer.cpp">
|
||||
<Filter>Core</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ELF\ElfReader.h">
|
||||
@@ -1268,6 +1271,9 @@
|
||||
<ClInclude Include="Replay.h">
|
||||
<Filter>Core</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="WebServer.h">
|
||||
<Filter>Core</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="CMakeLists.txt" />
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright (c) 2014- PPSSPP Project.
|
||||
|
||||
// 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, version 2.0 or later versions.
|
||||
|
||||
// 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 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include "base/stringutil.h"
|
||||
#include "base/timeutil.h"
|
||||
#include "file/fd_util.h"
|
||||
#include "net/http_client.h"
|
||||
#include "net/http_server.h"
|
||||
#include "net/sinks.h"
|
||||
#include "thread/threadutil.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/Log.h"
|
||||
#include "Core/Config.h"
|
||||
#include "Core/WebServer.h"
|
||||
|
||||
enum class ServerStatus {
|
||||
STOPPED,
|
||||
STARTING,
|
||||
RUNNING,
|
||||
STOPPING,
|
||||
RESTARTING,
|
||||
};
|
||||
|
||||
static const char *REPORT_HOSTNAME = "report.ppsspp.org";
|
||||
static const int REPORT_PORT = 80;
|
||||
|
||||
static std::thread serverThread;
|
||||
static ServerStatus serverStatus;
|
||||
static std::mutex serverStatusLock;
|
||||
static int serverFlags;
|
||||
|
||||
static void UpdateStatus(ServerStatus s) {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
serverStatus = s;
|
||||
}
|
||||
|
||||
static bool UpdateStatus(ServerStatus s, ServerStatus old) {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
if (serverStatus == old) {
|
||||
serverStatus = s;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ServerStatus RetrieveStatus() {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
return serverStatus;
|
||||
}
|
||||
|
||||
// This reports the local IP address to report.ppsspp.org, which can then
|
||||
// relay that address to a mobile device searching for the server.
|
||||
static void RegisterServer(int port) {
|
||||
http::Client http;
|
||||
Buffer theVoid;
|
||||
|
||||
char resource4[1024] = {};
|
||||
if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV4)) {
|
||||
if (http.Connect()) {
|
||||
std::string ip = fd_util::GetLocalIP(http.sock());
|
||||
snprintf(resource4, sizeof(resource4) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
|
||||
|
||||
http.GET(resource4, &theVoid);
|
||||
theVoid.Skip(theVoid.size());
|
||||
http.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV6)) {
|
||||
// We register both IPv4 and IPv6 in case the other client is using a different one.
|
||||
if (resource4[0] != 0 && http.Connect()) {
|
||||
http.GET(resource4, &theVoid);
|
||||
theVoid.Skip(theVoid.size());
|
||||
http.Disconnect();
|
||||
}
|
||||
|
||||
// Currently, we're not using keepalive, so gotta reconnect...
|
||||
if (http.Connect()) {
|
||||
char resource6[1024] = {};
|
||||
std::string ip = fd_util::GetLocalIP(http.sock());
|
||||
snprintf(resource6, sizeof(resource6) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
|
||||
|
||||
http.GET(resource6, &theVoid);
|
||||
theVoid.Skip(theVoid.size());
|
||||
http.Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void RegisterDiscHandlers(http::Server *http, std::unordered_map<std::string, std::string> *paths) {
|
||||
for (std::string filename : g_Config.recentIsos) {
|
||||
#ifdef _WIN32
|
||||
static const std::string sep = "\\/";
|
||||
#else
|
||||
static const std::string sep = "/";
|
||||
#endif
|
||||
size_t basepos = filename.find_last_of(sep);
|
||||
std::string basename = "/" + (basepos == filename.npos ? filename : filename.substr(basepos + 1));
|
||||
|
||||
// Let's not serve directories, since they won't work. Only single files.
|
||||
// Maybe can do PBPs and other files later. Would be neat to stream virtual disc filesystems.
|
||||
if (endsWithNoCase(basename, ".cso") || endsWithNoCase(basename, ".iso")) {
|
||||
(*paths)[ReplaceAll(basename, " ", "%20")] = filename;
|
||||
}
|
||||
}
|
||||
|
||||
auto handler = [paths](const http::Request &request) {
|
||||
std::string filename = (*paths)[request.resource()];
|
||||
s64 sz = File::GetFileSize(filename);
|
||||
|
||||
std::string range;
|
||||
if (request.Method() == http::RequestHeader::HEAD) {
|
||||
request.WriteHttpResponseHeader(200, sz, "application/octet-stream", "Accept-Ranges: bytes\r\n");
|
||||
} else if (request.GetHeader("range", &range)) {
|
||||
s64 begin = 0, last = 0;
|
||||
if (sscanf(range.c_str(), "bytes=%lld-%lld", &begin, &last) != 2) {
|
||||
request.WriteHttpResponseHeader(400, -1, "text/plain");
|
||||
request.Out()->Push("Could not understand range request.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (begin < 0 || begin > last || last >= sz) {
|
||||
request.WriteHttpResponseHeader(416, -1, "text/plain");
|
||||
request.Out()->Push("Range goes outside of file.");
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *fp = File::OpenCFile(filename, "rb");
|
||||
if (!fp || fseek(fp, begin, SEEK_SET) != 0) {
|
||||
request.WriteHttpResponseHeader(500, -1, "text/plain");
|
||||
request.Out()->Push("File access failed.");
|
||||
if (fp) {
|
||||
fclose(fp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
s64 len = last - begin + 1;
|
||||
char contentRange[1024];
|
||||
sprintf(contentRange, "Content-Range: bytes %lld-%lld/%lld\r\n", begin, last, sz);
|
||||
request.WriteHttpResponseHeader(206, len, "application/octet-stream", contentRange);
|
||||
|
||||
const size_t CHUNK_SIZE = 16 * 1024;
|
||||
char *buf = new char[CHUNK_SIZE];
|
||||
for (s64 pos = 0; pos < len; pos += CHUNK_SIZE) {
|
||||
s64 chunklen = std::min(len - pos, (s64)CHUNK_SIZE);
|
||||
fread(buf, chunklen, 1, fp);
|
||||
request.Out()->Push(buf, chunklen);
|
||||
}
|
||||
fclose(fp);
|
||||
delete [] buf;
|
||||
request.Out()->Flush();
|
||||
} else {
|
||||
request.WriteHttpResponseHeader(418, -1, "text/plain");
|
||||
request.Out()->Push("This server only supports range requests.");
|
||||
}
|
||||
};
|
||||
|
||||
for (auto pair : *paths) {
|
||||
http->RegisterHandler(pair.first.c_str(), handler);
|
||||
}
|
||||
}
|
||||
|
||||
static void ExecuteWebServer() {
|
||||
setCurrentThreadName("HTTPServer");
|
||||
|
||||
auto http = new http::Server(new threading::NewThreadExecutor());
|
||||
std::unordered_map<std::string, std::string> discPaths;
|
||||
|
||||
if (serverFlags & (int)WebServerFlags::DISCS) {
|
||||
RegisterDiscHandlers(http, &discPaths);
|
||||
}
|
||||
|
||||
if (!http->Listen(g_Config.iRemoteISOPort)) {
|
||||
if (!http->Listen(0)) {
|
||||
ERROR_LOG(FILESYS, "Unable to listen on any port");
|
||||
UpdateStatus(ServerStatus::STOPPED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
UpdateStatus(ServerStatus::RUNNING);
|
||||
|
||||
g_Config.iRemoteISOPort = http->Port();
|
||||
RegisterServer(http->Port());
|
||||
double lastRegister = real_time_now();
|
||||
while (RetrieveStatus() == ServerStatus::RUNNING) {
|
||||
http->RunSlice(1.0);
|
||||
|
||||
double now = real_time_now();
|
||||
if (now > lastRegister + 540.0) {
|
||||
RegisterServer(http->Port());
|
||||
lastRegister = now;
|
||||
}
|
||||
}
|
||||
|
||||
http->Stop();
|
||||
|
||||
// Move to STARTING to lock flags/STOPPING.
|
||||
if (UpdateStatus(ServerStatus::STARTING, ServerStatus::RESTARTING)) {
|
||||
ExecuteWebServer();
|
||||
} else {
|
||||
UpdateStatus(ServerStatus::STOPPED);
|
||||
}
|
||||
}
|
||||
|
||||
bool StartWebServer(WebServerFlags flags) {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
switch (serverStatus) {
|
||||
case ServerStatus::RUNNING:
|
||||
case ServerStatus::RESTARTING:
|
||||
if ((serverFlags & (int)flags) == (int)flags) {
|
||||
// Already running those flags.
|
||||
return false;
|
||||
}
|
||||
serverStatus = ServerStatus::RESTARTING;
|
||||
serverFlags |= (int)flags;
|
||||
return true;
|
||||
|
||||
case ServerStatus::STOPPED:
|
||||
serverStatus = ServerStatus::STARTING;
|
||||
serverFlags = (int)flags;
|
||||
serverThread = std::thread(&ExecuteWebServer);
|
||||
serverThread.detach();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool StopWebServer(WebServerFlags flags) {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
if (serverStatus != ServerStatus::RUNNING && serverStatus != ServerStatus::RESTARTING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
serverFlags &= ~(int)flags;
|
||||
if (serverFlags == 0) {
|
||||
serverStatus = ServerStatus::STOPPING;
|
||||
} else {
|
||||
serverStatus = ServerStatus::RESTARTING;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebServerStopping(WebServerFlags flags) {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
if (serverStatus == ServerStatus::RESTARTING) {
|
||||
return (serverFlags & (int)flags) == 0;
|
||||
}
|
||||
return serverStatus == ServerStatus::STOPPING;
|
||||
}
|
||||
|
||||
bool WebServerStopped(WebServerFlags flags) {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
if (serverStatus == ServerStatus::RUNNING) {
|
||||
return (serverFlags & (int)flags) == 0;
|
||||
}
|
||||
return serverStatus == ServerStatus::STOPPED;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2014- PPSSPP Project.
|
||||
|
||||
// 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, version 2.0 or later versions.
|
||||
|
||||
// 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 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
enum class WebServerFlags {
|
||||
DISCS = 1,
|
||||
|
||||
ALL = 1,
|
||||
};
|
||||
|
||||
bool StartWebServer(WebServerFlags flags);
|
||||
bool StopWebServer(WebServerFlags flags);
|
||||
bool WebServerStopping(WebServerFlags flags);
|
||||
bool WebServerStopped(WebServerFlags flags);
|
||||
+3
-1
@@ -84,6 +84,7 @@
|
||||
#include "Core/HLE/sceUsbGps.h"
|
||||
#include "Core/Util/GameManager.h"
|
||||
#include "Core/Util/AudioFormat.h"
|
||||
#include "Core/WebServer.h"
|
||||
#include "GPU/GPUInterface.h"
|
||||
|
||||
#include "ui_atlas.h"
|
||||
@@ -587,7 +588,8 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch
|
||||
}
|
||||
|
||||
if (g_Config.bRemoteShareOnStartup) {
|
||||
StartRemoteISOSharing();
|
||||
// TODO: Separate config options.
|
||||
StartWebServer(WebServerFlags::ALL);
|
||||
}
|
||||
|
||||
std::string sysName = System_GetProperty(SYSPROP_NAME);
|
||||
|
||||
+8
-201
@@ -18,20 +18,15 @@
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
#include "base/timeutil.h"
|
||||
#include "file/fd_util.h"
|
||||
#include "i18n/i18n.h"
|
||||
#include "json/json_reader.h"
|
||||
#include "net/http_client.h"
|
||||
#include "net/http_server.h"
|
||||
#include "net/resolve.h"
|
||||
#include "net/sinks.h"
|
||||
#include "thread/threadutil.h"
|
||||
#include "Common/Common.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Core/Config.h"
|
||||
#include "Core/WebServer.h"
|
||||
#include "UI/RemoteISOScreen.h"
|
||||
|
||||
using namespace UI;
|
||||
@@ -39,190 +34,9 @@ using namespace UI;
|
||||
static const char *REPORT_HOSTNAME = "report.ppsspp.org";
|
||||
static const int REPORT_PORT = 80;
|
||||
|
||||
enum class ServerStatus {
|
||||
STOPPED,
|
||||
STARTING,
|
||||
RUNNING,
|
||||
STOPPING,
|
||||
};
|
||||
|
||||
static std::thread *serverThread = nullptr;
|
||||
static ServerStatus serverStatus;
|
||||
static std::mutex serverStatusLock;
|
||||
static std::condition_variable serverStatusCond;
|
||||
|
||||
static bool scanCancelled = false;
|
||||
static bool scanAborted = false;
|
||||
|
||||
static void UpdateStatus(ServerStatus s) {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
serverStatus = s;
|
||||
serverStatusCond.notify_one();
|
||||
}
|
||||
|
||||
static ServerStatus RetrieveStatus() {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
return serverStatus;
|
||||
}
|
||||
|
||||
// This reports the local IP address to report.ppsspp.org, which can then
|
||||
// relay that address to a mobile device searching for the server.
|
||||
static void RegisterServer(int port) {
|
||||
http::Client http;
|
||||
Buffer theVoid;
|
||||
|
||||
char resource4[1024] = {};
|
||||
if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV4)) {
|
||||
if (http.Connect(2, 20.0, &scanCancelled)) {
|
||||
std::string ip = fd_util::GetLocalIP(http.sock());
|
||||
snprintf(resource4, sizeof(resource4) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
|
||||
|
||||
http.GET(resource4, &theVoid);
|
||||
theVoid.Skip(theVoid.size());
|
||||
http.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV6)) {
|
||||
// We register both IPv4 and IPv6 in case the other client is using a different one.
|
||||
if (resource4[0] != 0 && http.Connect()) {
|
||||
http.GET(resource4, &theVoid);
|
||||
theVoid.Skip(theVoid.size());
|
||||
http.Disconnect();
|
||||
}
|
||||
|
||||
// Currently, we're not using keepalive, so gotta reconnect...
|
||||
if (http.Connect()) {
|
||||
char resource6[1024] = {};
|
||||
std::string ip = fd_util::GetLocalIP(http.sock());
|
||||
snprintf(resource6, sizeof(resource6) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
|
||||
|
||||
http.GET(resource6, &theVoid);
|
||||
theVoid.Skip(theVoid.size());
|
||||
http.Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ExecuteServer() {
|
||||
setCurrentThreadName("HTTPServer");
|
||||
|
||||
auto http = new http::Server(new threading::SameThreadExecutor());
|
||||
|
||||
std::map<std::string, std::string> paths;
|
||||
for (std::string filename : g_Config.recentIsos) {
|
||||
#ifdef _WIN32
|
||||
static const std::string sep = "\\/";
|
||||
#else
|
||||
static const std::string sep = "/";
|
||||
#endif
|
||||
size_t basepos = filename.find_last_of(sep);
|
||||
std::string basename = "/" + (basepos == filename.npos ? filename : filename.substr(basepos + 1));
|
||||
|
||||
// Let's not serve directories, since they won't work. Only single files.
|
||||
// Maybe can do PBPs and other files later. Would be neat to stream virtual disc filesystems.
|
||||
if (endsWithNoCase(basename, ".cso") || endsWithNoCase(basename, ".iso")) {
|
||||
paths[ReplaceAll(basename, " ", "%20")] = filename;
|
||||
}
|
||||
}
|
||||
|
||||
auto handler = [&](const http::Request &request) {
|
||||
std::string filename = paths[request.resource()];
|
||||
s64 sz = File::GetFileSize(filename);
|
||||
|
||||
std::string range;
|
||||
if (request.Method() == http::RequestHeader::HEAD) {
|
||||
request.WriteHttpResponseHeader(200, sz, "application/octet-stream", "Accept-Ranges: bytes\r\n");
|
||||
} else if (request.GetHeader("range", &range)) {
|
||||
s64 begin = 0, last = 0;
|
||||
if (sscanf(range.c_str(), "bytes=%lld-%lld", &begin, &last) != 2) {
|
||||
request.WriteHttpResponseHeader(400, -1, "text/plain");
|
||||
request.Out()->Push("Could not understand range request.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (begin < 0 || begin > last || last >= sz) {
|
||||
request.WriteHttpResponseHeader(416, -1, "text/plain");
|
||||
request.Out()->Push("Range goes outside of file.");
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *fp = File::OpenCFile(filename, "rb");
|
||||
if (!fp || fseek(fp, begin, SEEK_SET) != 0) {
|
||||
request.WriteHttpResponseHeader(500, -1, "text/plain");
|
||||
request.Out()->Push("File access failed.");
|
||||
if (fp) {
|
||||
fclose(fp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
s64 len = last - begin + 1;
|
||||
char contentRange[1024];
|
||||
sprintf(contentRange, "Content-Range: bytes %lld-%lld/%lld\r\n", begin, last, sz);
|
||||
request.WriteHttpResponseHeader(206, len, "application/octet-stream", contentRange);
|
||||
|
||||
const size_t CHUNK_SIZE = 16 * 1024;
|
||||
char *buf = new char[CHUNK_SIZE];
|
||||
for (s64 pos = 0; pos < len; pos += CHUNK_SIZE) {
|
||||
s64 chunklen = std::min(len - pos, (s64)CHUNK_SIZE);
|
||||
fread(buf, chunklen, 1, fp);
|
||||
request.Out()->Push(buf, chunklen);
|
||||
}
|
||||
fclose(fp);
|
||||
delete [] buf;
|
||||
request.Out()->Flush();
|
||||
} else {
|
||||
request.WriteHttpResponseHeader(418, -1, "text/plain");
|
||||
request.Out()->Push("This server only supports range requests.");
|
||||
}
|
||||
};
|
||||
|
||||
for (auto pair : paths) {
|
||||
http->RegisterHandler(pair.first.c_str(), handler);
|
||||
}
|
||||
|
||||
if (!http->Listen(g_Config.iRemoteISOPort)) {
|
||||
if (!http->Listen(0)) {
|
||||
ERROR_LOG(FILESYS, "Unable to listen on any port");
|
||||
UpdateStatus(ServerStatus::STOPPED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
UpdateStatus(ServerStatus::RUNNING);
|
||||
|
||||
g_Config.iRemoteISOPort = http->Port();
|
||||
RegisterServer(http->Port());
|
||||
double lastRegister = real_time_now();
|
||||
while (RetrieveStatus() == ServerStatus::RUNNING) {
|
||||
http->RunSlice(5.0);
|
||||
|
||||
double now = real_time_now();
|
||||
if (now > lastRegister + 540.0) {
|
||||
RegisterServer(http->Port());
|
||||
lastRegister = now;
|
||||
}
|
||||
}
|
||||
|
||||
http->Stop();
|
||||
|
||||
UpdateStatus(ServerStatus::STOPPED);
|
||||
}
|
||||
|
||||
bool StartRemoteISOSharing() {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
|
||||
if (serverStatus != ServerStatus::STOPPED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
serverStatus = ServerStatus::STARTING;
|
||||
serverThread = new std::thread(&ExecuteServer);
|
||||
serverThread->detach();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool FindServer(std::string &resultHost, int &resultPort) {
|
||||
http::Client http;
|
||||
Buffer result;
|
||||
@@ -393,11 +207,8 @@ RemoteISOScreen::RemoteISOScreen() : serverRunning_(false), serverStopping_(fals
|
||||
void RemoteISOScreen::update() {
|
||||
UIScreenWithBackground::update();
|
||||
|
||||
bool nowRunning = RetrieveStatus() != ServerStatus::STOPPED;
|
||||
bool nowRunning = !WebServerStopped(WebServerFlags::DISCS);
|
||||
if (serverStopping_ && !nowRunning) {
|
||||
// Server stopped, delete the thread.
|
||||
delete serverThread;
|
||||
serverThread = nullptr;
|
||||
serverStopping_ = false;
|
||||
}
|
||||
|
||||
@@ -424,11 +235,10 @@ void RemoteISOScreen::CreateViews() {
|
||||
rightColumnItems->SetSpacing(0.0f);
|
||||
Choice *browseChoice = new Choice(ri->T("Browse Games"));
|
||||
rightColumnItems->Add(browseChoice)->OnClick.Handle(this, &RemoteISOScreen::HandleBrowse);
|
||||
ServerStatus status = RetrieveStatus();
|
||||
if (status == ServerStatus::STOPPING) {
|
||||
if (WebServerStopping(WebServerFlags::DISCS)) {
|
||||
rightColumnItems->Add(new Choice(ri->T("Stopping..")))->SetDisabledPtr(&serverStopping_);
|
||||
browseChoice->SetEnabled(false);
|
||||
} else if (status != ServerStatus::STOPPED) {
|
||||
} else if (!WebServerStopped(WebServerFlags::DISCS)) {
|
||||
rightColumnItems->Add(new Choice(ri->T("Stop Sharing")))->OnClick.Handle(this, &RemoteISOScreen::HandleStopServer);
|
||||
browseChoice->SetEnabled(false);
|
||||
} else {
|
||||
@@ -450,7 +260,7 @@ void RemoteISOScreen::CreateViews() {
|
||||
}
|
||||
|
||||
UI::EventReturn RemoteISOScreen::HandleStartServer(UI::EventParams &e) {
|
||||
if (!StartRemoteISOSharing()) {
|
||||
if (!StartWebServer(WebServerFlags::DISCS)) {
|
||||
return EVENT_SKIPPED;
|
||||
}
|
||||
|
||||
@@ -458,13 +268,10 @@ UI::EventReturn RemoteISOScreen::HandleStartServer(UI::EventParams &e) {
|
||||
}
|
||||
|
||||
UI::EventReturn RemoteISOScreen::HandleStopServer(UI::EventParams &e) {
|
||||
std::lock_guard<std::mutex> guard(serverStatusLock);
|
||||
|
||||
if (serverStatus != ServerStatus::RUNNING) {
|
||||
if (!StopWebServer(WebServerFlags::DISCS)) {
|
||||
return EVENT_SKIPPED;
|
||||
}
|
||||
|
||||
serverStatus = ServerStatus::STOPPING;
|
||||
serverStopping_ = true;
|
||||
RecreateViews();
|
||||
|
||||
@@ -683,13 +490,13 @@ void RemoteISOBrowseScreen::CreateViews() {
|
||||
}
|
||||
|
||||
RemoteISOSettingsScreen::RemoteISOSettingsScreen() {
|
||||
serverRunning_ = RetrieveStatus() != ServerStatus::STOPPED;
|
||||
serverRunning_ = !WebServerStopped(WebServerFlags::DISCS);
|
||||
}
|
||||
|
||||
void RemoteISOSettingsScreen::update() {
|
||||
UIDialogScreenWithBackground::update();
|
||||
|
||||
bool nowRunning = RetrieveStatus() != ServerStatus::STOPPED;
|
||||
bool nowRunning = !WebServerStopped(WebServerFlags::DISCS);
|
||||
if (serverRunning_ != nowRunning) {
|
||||
RecreateViews();
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
#include "UI/MiscScreens.h"
|
||||
#include "UI/MainScreen.h"
|
||||
|
||||
bool StartRemoteISOSharing();
|
||||
|
||||
class RemoteISOScreen : public UIScreenWithBackground {
|
||||
public:
|
||||
RemoteISOScreen();
|
||||
|
||||
@@ -463,6 +463,7 @@
|
||||
<ClInclude Include="..\..\Core\System.h" />
|
||||
<ClInclude Include="..\..\Core\TextureReplacer.h" />
|
||||
<ClInclude Include="..\..\Core\ThreadEventQueue.h" />
|
||||
<ClInclude Include="..\..\Core\WebServer.h" />
|
||||
<ClInclude Include="..\..\Core\Util\AudioFormat.h" />
|
||||
<ClInclude Include="..\..\Core\Util\AudioFormatNEON.h" />
|
||||
<ClInclude Include="..\..\Core\Util\BlockAllocator.h" />
|
||||
@@ -673,6 +674,7 @@
|
||||
<ClCompile Include="..\..\Core\Screenshot.cpp" />
|
||||
<ClCompile Include="..\..\Core\System.cpp" />
|
||||
<ClCompile Include="..\..\Core\TextureReplacer.cpp" />
|
||||
<ClCompile Include="..\..\Core\WebServer.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\AudioFormat.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\AudioFormatNEON.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\BlockAllocator.cpp" />
|
||||
|
||||
@@ -297,6 +297,7 @@ EXEC_AND_LIB_FILES := \
|
||||
$(SRC)/Core/Screenshot.cpp \
|
||||
$(SRC)/Core/System.cpp \
|
||||
$(SRC)/Core/TextureReplacer.cpp \
|
||||
$(SRC)/Core/WebServer.cpp \
|
||||
$(SRC)/Core/Debugger/Breakpoints.cpp \
|
||||
$(SRC)/Core/Debugger/SymbolMap.cpp \
|
||||
$(SRC)/Core/Dialog/PSPDialog.cpp \
|
||||
|
||||
@@ -13,6 +13,7 @@ LOCAL_SRC_FILES :=\
|
||||
base/timeutil.cpp \
|
||||
base/colorutil.cpp \
|
||||
base/stringutil.cpp \
|
||||
data/base64.cpp \
|
||||
data/compression.cpp \
|
||||
ext/rg_etc1/rg_etc1.cpp \
|
||||
ext/cityhash/city.cpp \
|
||||
@@ -64,6 +65,7 @@ LOCAL_SRC_FILES :=\
|
||||
net/resolve.cpp \
|
||||
net/sinks.cpp \
|
||||
net/url.cpp \
|
||||
net/websocket_server.cpp \
|
||||
profiler/profiler.cpp \
|
||||
thread/executor.cpp \
|
||||
thread/threadutil.cpp \
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "data/base64.h"
|
||||
|
||||
// TODO: This is a simple but not very efficient implementation.
|
||||
std::string Base64Encode(const uint8_t *p, size_t sz) {
|
||||
const char digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
size_t unpaddedLength = (4 * sz + 3) / 3;
|
||||
std::string result;
|
||||
result.resize((unpaddedLength + 3) & ~3, '=');
|
||||
|
||||
for (size_t i = 0; i < unpaddedLength; ++i) {
|
||||
// This is the index into the original string.
|
||||
size_t pos = (i * 3) / 4;
|
||||
int8_t off = 2 * ((i * 3) % 4);
|
||||
|
||||
int c = p[pos];
|
||||
if (off > 2) {
|
||||
c <<= 8;
|
||||
off -= 8;
|
||||
|
||||
// Grab more bits from the next character.
|
||||
if (pos + 1 < sz) {
|
||||
c |= p[pos + 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Since we take from the big end, off starts at 2 and goes down.
|
||||
int8_t shift = 2 - off;
|
||||
|
||||
// Now take the bits at off and encode the character.
|
||||
result[i] = digits[(c >> shift) & 0x3F];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
std::string Base64Encode(const uint8_t *p, size_t sz);
|
||||
@@ -202,6 +202,7 @@
|
||||
<ClInclude Include="base\NativeApp.h" />
|
||||
<ClInclude Include="base\stringutil.h" />
|
||||
<ClInclude Include="base\timeutil.h" />
|
||||
<ClInclude Include="data\base64.h" />
|
||||
<ClInclude Include="data\compression.h" />
|
||||
<ClInclude Include="data\listable.h" />
|
||||
<ClInclude Include="ext\cityhash\city.h" />
|
||||
@@ -238,6 +239,7 @@
|
||||
<ClInclude Include="gfx_es2\draw_text_qt.h" />
|
||||
<ClInclude Include="gfx_es2\draw_text_win.h" />
|
||||
<ClInclude Include="json\json_reader.h" />
|
||||
<ClInclude Include="net\websocket_server.h" />
|
||||
<ClInclude Include="thin3d\d3d11_loader.h" />
|
||||
<ClInclude Include="thin3d\DataFormat.h" />
|
||||
<ClInclude Include="thin3d\DataFormatGL.h" />
|
||||
@@ -317,6 +319,7 @@
|
||||
</ClCompile>
|
||||
<ClCompile Include="base\stringutil.cpp" />
|
||||
<ClCompile Include="base\timeutil.cpp" />
|
||||
<ClCompile Include="data\base64.cpp" />
|
||||
<ClCompile Include="data\compression.cpp" />
|
||||
<ClCompile Include="ext\cityhash\city.cpp">
|
||||
<InlineFunctionExpansion Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">AnySuitable</InlineFunctionExpansion>
|
||||
@@ -700,6 +703,7 @@
|
||||
<ClCompile Include="gfx_es2\draw_text_win.cpp" />
|
||||
<ClCompile Include="json\json_reader.cpp" />
|
||||
<ClCompile Include="math\dataconv.cpp" />
|
||||
<ClCompile Include="net\websocket_server.cpp" />
|
||||
<ClCompile Include="thin3d\d3d11_loader.cpp" />
|
||||
<ClCompile Include="thin3d\DataFormatGL.cpp" />
|
||||
<ClCompile Include="thin3d\GLQueueRunner.cpp" />
|
||||
|
||||
@@ -341,6 +341,12 @@
|
||||
<ClInclude Include="json\json_reader.h">
|
||||
<Filter>json</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="net\websocket_server.h">
|
||||
<Filter>net</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="data\base64.h">
|
||||
<Filter>data</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="gfx\gl_debug_log.cpp">
|
||||
@@ -814,6 +820,12 @@
|
||||
<ClCompile Include="json\json_reader.cpp">
|
||||
<Filter>json</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="net\websocket_server.cpp">
|
||||
<Filter>net</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="data\base64.cpp">
|
||||
<Filter>data</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="gfx">
|
||||
|
||||
@@ -87,8 +87,10 @@ void Request::WriteHttpResponseHeader(int status, int64_t size, const char *mime
|
||||
net::OutputSink *buffer = Out();
|
||||
buffer->Printf("HTTP/1.0 %03d %s\r\n", status, statusStr);
|
||||
buffer->Push("Server: PPSSPPServer v0.1\r\n");
|
||||
buffer->Printf("Content-Type: %s\r\n", mimeType ? mimeType : DEFAULT_MIME_TYPE);
|
||||
buffer->Push("Connection: close\r\n");
|
||||
if (!mimeType || strcmp(mimeType, "websocket") != 0) {
|
||||
buffer->Printf("Content-Type: %s\r\n", mimeType ? mimeType : DEFAULT_MIME_TYPE);
|
||||
buffer->Push("Connection: close\r\n");
|
||||
}
|
||||
if (size >= 0) {
|
||||
buffer->Printf("Content-Length: %llu\r\n", size);
|
||||
}
|
||||
|
||||
@@ -201,6 +201,11 @@ bool InputSink::Empty() {
|
||||
return valid_ == 0;
|
||||
}
|
||||
|
||||
bool InputSink::TryFill() {
|
||||
Fill();
|
||||
return !Empty();
|
||||
}
|
||||
|
||||
OutputSink::OutputSink(size_t fd) : fd_(fd), read_(0), write_(0), valid_(0) {
|
||||
fd_util::SetNonBlocking((int)fd_, true);
|
||||
}
|
||||
@@ -316,7 +321,7 @@ bool OutputSink::Block() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OutputSink::Flush() {
|
||||
bool OutputSink::Flush(bool allowBlock) {
|
||||
while (valid_ > 0) {
|
||||
size_t avail = std::min(BUFFER_SIZE - read_, valid_);
|
||||
|
||||
@@ -325,7 +330,7 @@ bool OutputSink::Flush() {
|
||||
|
||||
if (bytes == 0) {
|
||||
// This may also drain. Either way, keep looping.
|
||||
if (!Block()) {
|
||||
if (!allowBlock || !Block()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
bool Skip(size_t bytes);
|
||||
|
||||
bool Empty();
|
||||
bool TryFill();
|
||||
|
||||
private:
|
||||
void Fill();
|
||||
@@ -49,7 +50,7 @@ public:
|
||||
bool PushCRLF(const std::string &s);
|
||||
bool Printf(const char *fmt, ...);
|
||||
|
||||
bool Flush();
|
||||
bool Flush(bool allowBlock = true);
|
||||
|
||||
bool Empty();
|
||||
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#ifndef _WIN32
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/select.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <io.h>
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#endif
|
||||
#include "base/stringutil.h"
|
||||
#include "data/base64.h"
|
||||
#include "net/http_server.h"
|
||||
#include "net/sinks.h"
|
||||
#include "net/websocket_server.h"
|
||||
// TODO: Not a great cross dependency.
|
||||
#include "Common/Crypto/sha1.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
// Function Cross-Compatibility
|
||||
#define strcasecmp _stricmp
|
||||
#endif
|
||||
|
||||
static const char *const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
namespace net {
|
||||
|
||||
enum class Opcode {
|
||||
CONTINUE = 0,
|
||||
TEXT = 1,
|
||||
BINARY = 2,
|
||||
|
||||
CLOSE = 8,
|
||||
PING = 9,
|
||||
PONG = 10,
|
||||
|
||||
PAYLOAD_MAX = 2,
|
||||
CONTROL_MIN = 8,
|
||||
CONTROL_MAX = 10,
|
||||
};
|
||||
|
||||
static inline std::string TrimString(const std::string &s) {
|
||||
auto wsfront = std::find_if_not(s.begin(), s.end(), [](int c) {
|
||||
// isspace() expects 0 - 255, so convert any sign-extended value.
|
||||
return std::isspace(c & 0xFF);
|
||||
});
|
||||
auto wsback = std::find_if_not(s.rbegin(), s.rend(), [](int c){
|
||||
return std::isspace(c & 0xFF);
|
||||
}).base();
|
||||
return wsback > wsfront ? std::string(wsfront, wsback) : std::string();
|
||||
}
|
||||
|
||||
static bool ListContainsNoCase(const std::string &list, const std::string value) {
|
||||
std::vector<std::string> split;
|
||||
SplitString(list, ',', split);
|
||||
|
||||
for (auto item : split) {
|
||||
std::transform(item.begin(), item.end(), item.begin(), tolower);
|
||||
if (TrimString(item) == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
WebSocketServer *WebSocketServer::CreateAsUpgrade(const http::Request &request, const std::string &protocol) {
|
||||
auto requireHeader = [&](const char *name, const char *expected) {
|
||||
std::string val;
|
||||
if (!request.GetHeader(name, &val)) {
|
||||
return false;
|
||||
}
|
||||
return strcasecmp(val.c_str(), expected) == 0;
|
||||
};
|
||||
auto requireHeaderContains = [&](const char *name, const char *expected) {
|
||||
std::string val;
|
||||
if (!request.GetHeader(name, &val)) {
|
||||
return false;
|
||||
}
|
||||
return ListContainsNoCase(val, expected);
|
||||
};
|
||||
|
||||
if (!requireHeader("upgrade", "websocket") || !requireHeaderContains("connection", "upgrade")) {
|
||||
request.WriteHttpResponseHeader(400, -1, "text/plain");
|
||||
request.Out()->Push("Must send a websocket request.");
|
||||
return nullptr;
|
||||
}
|
||||
if (!requireHeader("sec-websocket-version", "13")) {
|
||||
request.WriteHttpResponseHeader(400, -1, "text/plain", "Sec-WebSocket-Version: 13\r\n");
|
||||
request.Out()->Push("Unsupported version.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string requestedProtocols;
|
||||
std::string obtainedProtocolHeader;
|
||||
if (!protocol.empty() && request.GetHeader("sec-websocket-protocol", &requestedProtocols)) {
|
||||
if (ListContainsNoCase(requestedProtocols, protocol)) {
|
||||
obtainedProtocolHeader = "Sec-WebSocket-Protocol: " + protocol + "\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::string key;
|
||||
if (!request.GetHeader("sec-websocket-key", &key)) {
|
||||
request.WriteHttpResponseHeader(400, -1, "text/plain");
|
||||
request.Out()->Push("Cannot accept without key.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
key += WEBSOCKET_GUID;
|
||||
unsigned char accept[20];
|
||||
sha1((unsigned char *)key.c_str(), (int)key.size(), accept);
|
||||
|
||||
std::string acceptKey = Base64Encode(accept, 20);
|
||||
std::string otherHeaders = StringFromFormat("Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n%s", acceptKey.c_str(), obtainedProtocolHeader.c_str());
|
||||
|
||||
// Okay, we're good to go then.
|
||||
request.WriteHttpResponseHeader(101, -1, "websocket", otherHeaders.c_str());
|
||||
request.WritePartial();
|
||||
|
||||
return new WebSocketServer(request.fd(), request.In(), request.Out());
|
||||
}
|
||||
|
||||
void WebSocketServer::Send(const std::string &str) {
|
||||
assert(open_);
|
||||
assert(fragmentOpcode_ == -1);
|
||||
SendHeader(true, (int)Opcode::TEXT, str.size());
|
||||
SendBytes(str.c_str(), str.size());
|
||||
}
|
||||
|
||||
void WebSocketServer::Send(const std::vector<uint8_t> &payload) {
|
||||
assert(open_);
|
||||
assert(fragmentOpcode_ == -1);
|
||||
SendHeader(true, (int)Opcode::BINARY, payload.size());
|
||||
SendBytes((const char *)&payload[0], payload.size());
|
||||
}
|
||||
|
||||
void WebSocketServer::AddFragment(bool finish, const std::string &str) {
|
||||
assert(open_);
|
||||
if (fragmentOpcode_ == -1) {
|
||||
SendHeader(finish, (int)Opcode::TEXT, str.size());
|
||||
fragmentOpcode_ = (int)Opcode::TEXT;
|
||||
} else if (fragmentOpcode_ == (int)Opcode::TEXT) {
|
||||
SendHeader(finish, (int)Opcode::CONTINUE, str.size());
|
||||
} else {
|
||||
assert(fragmentOpcode_ == (int)Opcode::TEXT || fragmentOpcode_ == -1);
|
||||
}
|
||||
SendBytes(str.c_str(), str.size());
|
||||
if (finish) {
|
||||
fragmentOpcode_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketServer::AddFragment(bool finish, const std::vector<uint8_t> &payload) {
|
||||
assert(open_);
|
||||
if (fragmentOpcode_ == -1) {
|
||||
SendHeader(finish, (int)Opcode::BINARY, payload.size());
|
||||
fragmentOpcode_ = (int)Opcode::BINARY;
|
||||
} else if (fragmentOpcode_ == (int)Opcode::BINARY) {
|
||||
SendHeader(finish, (int)Opcode::CONTINUE, payload.size());
|
||||
} else {
|
||||
assert(fragmentOpcode_ == (int)Opcode::BINARY || fragmentOpcode_ == -1);
|
||||
}
|
||||
SendBytes((const char *)&payload[0], payload.size());
|
||||
if (finish) {
|
||||
fragmentOpcode_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketServer::Ping(const std::vector<uint8_t> &payload) {
|
||||
assert(open_);
|
||||
assert(payload.size() <= 125);
|
||||
SendHeader(true, (int)Opcode::PING, payload.size());
|
||||
SendBytes((const char *)&payload[0], payload.size());
|
||||
}
|
||||
|
||||
void WebSocketServer::Pong(const std::vector<uint8_t> &payload) {
|
||||
assert(open_);
|
||||
assert(payload.size() <= 125);
|
||||
SendHeader(true, (int)Opcode::PONG, payload.size());
|
||||
SendBytes((const char *)&payload[0], payload.size());
|
||||
}
|
||||
|
||||
void WebSocketServer::Close(WebSocketClose reason) {
|
||||
closeReason_ = reason;
|
||||
if (reason == WebSocketClose::NO_STATUS) {
|
||||
// This means we received a CLOSE without a code.
|
||||
SendHeader(true, (int)Opcode::CLOSE, 0);
|
||||
} else {
|
||||
SendHeader(true, (int)Opcode::CLOSE, 2);
|
||||
|
||||
uint16_t r = (uint16_t)reason;
|
||||
uint8_t reasonData[] = {
|
||||
(uint8_t)((r >> 8) & 0xFF),
|
||||
(uint8_t)((r >> 0) & 0xFF),
|
||||
};
|
||||
SendBytes((const char *)reasonData, sizeof(reasonData));
|
||||
}
|
||||
|
||||
sentClose_ = true;
|
||||
}
|
||||
|
||||
bool WebSocketServer::Process(float timeout) {
|
||||
if (!open_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SendFlush();
|
||||
|
||||
if (outBuf_.empty() && out_->Empty() && sentClose_) {
|
||||
// Okay, we've sent the close. Don't wait for anything else (whether we got a close or not.)
|
||||
open_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = floor(timeout);
|
||||
tv.tv_usec = (timeout - floor(timeout)) * 1000000.0;
|
||||
|
||||
fd_set read;
|
||||
FD_ZERO(&read);
|
||||
// In case we closed due to protocol error, don't even try to read.
|
||||
if (!sentClose_) {
|
||||
FD_SET(fd_, &read);
|
||||
}
|
||||
|
||||
fd_set write;
|
||||
FD_ZERO(&write);
|
||||
if (!outBuf_.empty() || !out_->Empty()) {
|
||||
FD_SET(fd_, &write);
|
||||
}
|
||||
|
||||
// First argument to select is the highest socket in the set + 1.
|
||||
int rval = select((int)fd_ + 1, &read, &write, nullptr, &tv);
|
||||
if (rval < 0) {
|
||||
// Something went wrong with the select() call.
|
||||
// TODO: Could be EINTR, for now returning true...
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rval == 0) {
|
||||
// Timed out.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (FD_ISSET(fd_, &write)) {
|
||||
SendFlush();
|
||||
}
|
||||
if (FD_ISSET(fd_, &read)) {
|
||||
if (in_->Empty() && !in_->TryFill()) {
|
||||
// Since select said it was readable, we assume this means disconnect.
|
||||
closeReason_ = WebSocketClose::ABNORMAL;
|
||||
open_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
while (ReadFrames() && !in_->Empty())
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebSocketServer::ReadFrames() {
|
||||
if (pendingLeft_ != 0) {
|
||||
return ReadPending();
|
||||
}
|
||||
|
||||
return ReadFrame();
|
||||
}
|
||||
|
||||
bool WebSocketServer::ReadFrame() {
|
||||
assert(pendingLeft_ == 0);
|
||||
|
||||
// TODO: For now blocking on header trickle, shouldn't be common.
|
||||
auto readExact = [&](void *p, size_t sz) {
|
||||
if (!in_->TakeExact((char *)p, sz)) {
|
||||
// TODO: Failing on too slow trickle timeout for now.
|
||||
Close(WebSocketClose::POLICY_VIOLATION);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Client frames are always between 6 and 14 bytes. We start with 6.
|
||||
uint8_t header[14];
|
||||
if (!readExact(header, 6))
|
||||
return false;
|
||||
|
||||
// Don't allow reserved bits to be set, require masking.
|
||||
if ((header[0] & 0x70) != 0 || (header[1] & 0x80) == 0) {
|
||||
Close(WebSocketClose::PROTOCOL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool fin = (header[0] & 0x80) != 0;
|
||||
const int opcode = header[0] & 0x0F;
|
||||
uint64_t sz = header[1] & 0x7F;
|
||||
const uint8_t *mask = &header[2];
|
||||
|
||||
if (opcode >= (int)Opcode::CONTROL_MIN && (sz > 125 || !fin)) {
|
||||
// Control frames must be <= 125 bytes.
|
||||
Close(WebSocketClose::PROTOCOL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opcode > (int)Opcode::CONTROL_MAX || (opcode > (int)Opcode::PAYLOAD_MAX && opcode < (int)Opcode::CONTROL_MIN)) {
|
||||
// Undefined opcode.
|
||||
Close(WebSocketClose::PROTOCOL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pendingFin_ && opcode == (int)Opcode::CONTINUE) {
|
||||
// Can't continue what you haven't started.
|
||||
Close(WebSocketClose::PROTOCOL_ERROR);
|
||||
return false;
|
||||
}
|
||||
if (pendingFin_ && opcode != (int)Opcode::CONTINUE && opcode < (int)Opcode::CONTROL_MIN) {
|
||||
// Can't start something else until you finish your thought.
|
||||
Close(WebSocketClose::PROTOCOL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sz == 126) {
|
||||
// Read the rest of the mask.
|
||||
if (!readExact((char *)&header[6], 2))
|
||||
return false;
|
||||
|
||||
mask = &header[4];
|
||||
sz = (header[2] << 8) | (header[3] << 0);
|
||||
} else if (sz == 127) {
|
||||
// We only have half the size so far - read the rest, and the mask.
|
||||
if (!readExact((char *)&header[6], 8))
|
||||
return false;
|
||||
|
||||
mask = &header[10];
|
||||
// Read from big endian.
|
||||
uint64_t high = (header[2] << 24) | (header[3] << 16) || (header[4] << 8) | (header[5] << 0);
|
||||
uint64_t low = (header[6] << 24) | (header[7] << 16) || (header[8] << 8) | (header[9] << 0);
|
||||
sz = (high << 32) | low;
|
||||
|
||||
if ((sz & 0x8000000000000000ULL) != 0) {
|
||||
Close(WebSocketClose::PROTOCOL_ERROR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (opcode >= (int)Opcode::CONTROL_MIN) {
|
||||
// It's safe to overwrite this since we can be between fragmented frames, but not inside a frame.
|
||||
memcpy(pendingMask_, mask, sizeof(pendingMask_));
|
||||
return ReadControlFrame(opcode, sz);
|
||||
}
|
||||
|
||||
// The data could be split among many TCP packets, so read it as it comes.
|
||||
if (!pendingFin_)
|
||||
pendingOpcode_ = opcode;
|
||||
pendingFin_ = !fin;
|
||||
pendingLeft_ = sz;
|
||||
memcpy(pendingMask_, mask, sizeof(pendingMask_));
|
||||
|
||||
// Payload data is actually read in ReadPending().
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebSocketServer::ReadPending() {
|
||||
size_t pos = pendingBuf_.size();
|
||||
pendingBuf_.resize(pendingBuf_.size() + pendingLeft_);
|
||||
|
||||
// Read what we can.
|
||||
size_t readBytes = in_->TakeAtMost((char *)&pendingBuf_[pos], pendingLeft_);
|
||||
for (size_t i = 0; i < readBytes; ++i) {
|
||||
pendingBuf_[pos + i] ^= pendingMask_[i & 3];
|
||||
}
|
||||
pendingLeft_ -= readBytes;
|
||||
|
||||
if (pendingLeft_ != 0) {
|
||||
// Still more to read. Careful: we might need to rotate the mask.
|
||||
// Example: if we read only 3 bytes, next read should start at fourth byte in mask.
|
||||
int offset = readBytes & 3;
|
||||
if (offset) {
|
||||
uint8_t orig[4];
|
||||
memcpy(orig, pendingMask_, sizeof(orig));
|
||||
for (size_t i = 0; i < sizeof(orig); ++i) {
|
||||
pendingMask_[i] = orig[(offset + i) & 3];
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate out the unread bytes for next time.
|
||||
pendingBuf_.resize(pos + readBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're done, but were we waiting for a FIN packet?
|
||||
if (pendingFin_)
|
||||
return true;
|
||||
|
||||
if (pendingOpcode_ == (int)Opcode::TEXT) {
|
||||
if (text_) {
|
||||
text_(std::string(pendingBuf_.begin(), pendingBuf_.end()));
|
||||
}
|
||||
} else if (pendingOpcode_ == (int)Opcode::BINARY) {
|
||||
if (binary_) {
|
||||
binary_(pendingBuf_);
|
||||
}
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
// All done, clear it out.
|
||||
pendingBuf_.clear();
|
||||
pendingOpcode_ = -1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebSocketServer::ReadControlFrame(int opcode, size_t sz) {
|
||||
std::vector<uint8_t> payload;
|
||||
payload.resize(sz);
|
||||
// Just block here to read the payload.
|
||||
if (!in_->TakeExact((char *)&payload[0], sz)) {
|
||||
// TODO: Failing on too slow trickle timeout for now.
|
||||
Close(WebSocketClose::POLICY_VIOLATION);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sz; ++i) {
|
||||
payload[i] ^= pendingMask_[i & 3];
|
||||
}
|
||||
|
||||
if (opcode == (int)Opcode::PING) {
|
||||
Pong(payload);
|
||||
// Try to send immediately if possible, but don't block.
|
||||
SendFlush();
|
||||
|
||||
if (ping_) {
|
||||
ping_(payload);
|
||||
}
|
||||
} else if (opcode == (int)Opcode::PONG) {
|
||||
if (pong_) {
|
||||
pong_(payload);
|
||||
}
|
||||
} else if (opcode == (int)Opcode::CLOSE) {
|
||||
if (payload.size() >= 2) {
|
||||
uint16_t reason = (payload[0] << 8) | payload[1];
|
||||
// Send back a close right away.
|
||||
Close(WebSocketClose(reason));
|
||||
} else {
|
||||
Close(WebSocketClose::NO_STATUS);
|
||||
}
|
||||
// Don't read anything more.
|
||||
return false;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketServer::SendHeader(bool fin, int opcode, size_t sz) {
|
||||
assert((opcode & 0x0F) == opcode);
|
||||
uint8_t frameHeader = (fin ? 0x80 : 0x00) | opcode;
|
||||
SendBytes(&frameHeader, 1);
|
||||
|
||||
// We never mask from the server.
|
||||
if (sz <= 125) {
|
||||
uint8_t frameSize = (int8_t)sz;
|
||||
SendBytes(&frameSize, 1);
|
||||
} else if (sz <= 0xFFFF) {
|
||||
uint8_t frameSize[] = {
|
||||
126,
|
||||
(uint8_t)((sz >> 8) & 0xFF),
|
||||
(uint8_t)((sz >> 0) & 0xFF),
|
||||
};
|
||||
SendBytes(frameSize, sizeof(frameSize));
|
||||
} else {
|
||||
uint64_t sz64 = sz;
|
||||
assert((sz64 & 0x8000000000000000ULL) == 0);
|
||||
uint8_t frameSize[] = {
|
||||
127,
|
||||
(uint8_t)((sz64 >> 56) & 0xFF),
|
||||
(uint8_t)((sz64 >> 48) & 0xFF),
|
||||
(uint8_t)((sz64 >> 40) & 0xFF),
|
||||
(uint8_t)((sz64 >> 32) & 0xFF),
|
||||
(uint8_t)((sz64 >> 24) & 0xFF),
|
||||
(uint8_t)((sz64 >> 16) & 0xFF),
|
||||
(uint8_t)((sz64 >> 8) & 0xFF),
|
||||
(uint8_t)((sz64 >> 0) & 0xFF),
|
||||
};
|
||||
SendBytes(frameSize, sizeof(frameSize));
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketServer::SendBytes(const void *p, size_t sz) {
|
||||
const char *data = (const char *)p;
|
||||
if (outBuf_.empty()) {
|
||||
size_t pushed = out_->PushAtMost(data, sz);
|
||||
data += pushed;
|
||||
sz -= pushed;
|
||||
}
|
||||
|
||||
if (sz != 0) {
|
||||
size_t pos = outBuf_.size();
|
||||
outBuf_.resize(pos + sz);
|
||||
memcpy(&outBuf_[pos], data, sz);
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketServer::SendFlush() {
|
||||
out_->Flush(false);
|
||||
|
||||
// Drain out as much of our buffer as possible.
|
||||
size_t totalPushed = 0;
|
||||
while (!outBuf_.empty()) {
|
||||
size_t pushed = out_->PushAtMost((const char *)&outBuf_[totalPushed], outBuf_.size() - totalPushed);
|
||||
if (pushed == 0)
|
||||
break;
|
||||
|
||||
totalPushed += pushed;
|
||||
out_->Flush(false);
|
||||
}
|
||||
|
||||
if (totalPushed != 0) {
|
||||
// Hopefully this is usually the entire buffer.
|
||||
outBuf_.erase(outBuf_.begin(), outBuf_.begin() + totalPushed);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include "net/http_server.h"
|
||||
#include "net/sinks.h"
|
||||
|
||||
namespace net {
|
||||
|
||||
enum class WebSocketClose : uint16_t {
|
||||
NORMAL = 1000,
|
||||
GOING_AWAY = 1001,
|
||||
PROTOCOL_ERROR = 1002,
|
||||
UNSUPPORTED_DATA = 1003,
|
||||
INVALID_DATA = 1007,
|
||||
POLICY_VIOLATION = 1008,
|
||||
MESSAGE_TOO_LONG = 1009,
|
||||
MISSING_EXTENSION = 1010,
|
||||
INTERNAL_ERROR = 1011,
|
||||
SERVICE_RESTART = 1012,
|
||||
TRY_AGAIN_LATER = 1013,
|
||||
BAD_GATEWAY = 1014,
|
||||
|
||||
NO_STATUS = 1005,
|
||||
ABNORMAL = 1006,
|
||||
};
|
||||
|
||||
// RFC 6455
|
||||
class WebSocketServer {
|
||||
public:
|
||||
static WebSocketServer *CreateAsUpgrade(const http::Request &request, const std::string &protocol = "");
|
||||
|
||||
void Send(const std::string &str);
|
||||
void Send(const std::vector<uint8_t> &payload);
|
||||
|
||||
// Call with finish = false to start and continue, then finally with finish = true to complete.
|
||||
// Note: Fragmented data cannot be interleaved, per protocol.
|
||||
void AddFragment(bool finish, const std::string &str);
|
||||
void AddFragment(bool finish, const std::vector<uint8_t> &payload);
|
||||
|
||||
void Ping(const std::vector<uint8_t> &payload = {});
|
||||
void Pong(const std::vector<uint8_t> &payload = {});
|
||||
void Close(WebSocketClose reason = WebSocketClose::GOING_AWAY);
|
||||
|
||||
// Note: may interrupt early. Call in a loop.
|
||||
bool Process(float timeout = -1.0f);
|
||||
|
||||
void SetTextHandler(std::function<void(const std::string &)> func) {
|
||||
text_ = func;
|
||||
}
|
||||
void SetBinaryHandler(std::function<void(const std::vector<uint8_t> &)> func) {
|
||||
binary_ = func;
|
||||
}
|
||||
// Doesn't need to send a Pong.
|
||||
void SetPingHandler(std::function<void(const std::vector<uint8_t> &)> func) {
|
||||
ping_ = func;
|
||||
}
|
||||
void SetPongHandler(std::function<void(const std::vector<uint8_t> &)> func) {
|
||||
pong_ = func;
|
||||
}
|
||||
|
||||
bool IsOpen() {
|
||||
return open_;
|
||||
}
|
||||
WebSocketClose CloseReason() {
|
||||
return closeReason_;
|
||||
}
|
||||
|
||||
protected:
|
||||
WebSocketServer(size_t fd, InputSink *in, OutputSink *out) : fd_(fd), in_(in), out_(out) {
|
||||
}
|
||||
|
||||
void SendHeader(bool fin, int opcode, size_t sz);
|
||||
void SendBytes(const void *p, size_t sz);
|
||||
void SendFlush();
|
||||
bool ReadFrames();
|
||||
bool ReadFrame();
|
||||
bool ReadPending();
|
||||
bool ReadControlFrame(int opcode, size_t sz);
|
||||
|
||||
bool open_ = true;
|
||||
bool sentClose_ = false;
|
||||
int fragmentOpcode_ = -1;
|
||||
size_t fd_ = 0;
|
||||
InputSink *in_ = nullptr;
|
||||
OutputSink *out_ = nullptr;
|
||||
WebSocketClose closeReason_ = WebSocketClose::NO_STATUS;
|
||||
std::vector<uint8_t> outBuf_;
|
||||
|
||||
std::vector<uint8_t> pendingBuf_;
|
||||
uint8_t pendingMask_[4]{};
|
||||
// Bytes left to read in the frame (in case of a partial frame read.)
|
||||
uint64_t pendingLeft_ = 0;
|
||||
int pendingOpcode_ = -1;
|
||||
// Waiting for a frame with FIN.
|
||||
bool pendingFin_ = false;
|
||||
|
||||
std::function<void(const std::string &)> text_;
|
||||
std::function<void(const std::vector<uint8_t> &)> binary_;
|
||||
std::function<void(const std::vector<uint8_t> &)> ping_;
|
||||
std::function<void(const std::vector<uint8_t> &)> pong_;
|
||||
};
|
||||
|
||||
};
|
||||
@@ -1,11 +1,16 @@
|
||||
#include "thread/executor.h"
|
||||
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
namespace threading {
|
||||
|
||||
void SameThreadExecutor::Run(std::function<void()> func) {
|
||||
func();
|
||||
func();
|
||||
}
|
||||
|
||||
void NewThreadExecutor::Run(std::function<void()> func) {
|
||||
std::thread(func).detach();
|
||||
}
|
||||
|
||||
} // namespace threading
|
||||
|
||||
@@ -6,13 +6,18 @@ namespace threading {
|
||||
|
||||
// Stuff that can execute other stuff, like threadpools, should inherit from this.
|
||||
class Executor {
|
||||
public:
|
||||
virtual void Run(std::function<void()> func) = 0;
|
||||
public:
|
||||
virtual void Run(std::function<void()> func) = 0;
|
||||
};
|
||||
|
||||
class SameThreadExecutor : public Executor {
|
||||
public:
|
||||
virtual void Run(std::function<void()> func);
|
||||
public:
|
||||
void Run(std::function<void()> func) override;
|
||||
};
|
||||
|
||||
class NewThreadExecutor : public Executor {
|
||||
public:
|
||||
void Run(std::function<void()> func) override;
|
||||
};
|
||||
|
||||
} // namespace threading
|
||||
|
||||
Reference in New Issue
Block a user