Files
ppsspp/Core/WebServer.cpp
T
Acts1631 2c3437940b Fix upload filename traversal
Reject multipart filenames containing path separators before joining
them to the selected upload path. This keeps LAN upload clients
from creating files outside the selected directory.
2026-07-23 15:04:04 -04:00

877 lines
26 KiB
C++

// 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 <string_view>
#include "Common/Net/HTTPClient.h"
#include "Common/Net/HTTPServer.h"
#include "Common/Net/Sinks.h"
#include "Common/Net/URL.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/Log.h"
#include "Common/File/FileUtil.h"
#include "Common/File/FileDescriptor.h"
#include "Common/File/DirListing.h"
#include "Common/File/VFS/VFS.h"
#include "Common/Data/Text/I18n.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
#include "Common/System/System.h"
#include "Common/System/OSD.h"
#include "Core/Util/RecentFiles.h"
#include "Core/Util/PathUtil.h"
#include "Core/Config.h"
#include "Core/Debugger/WebSocket.h"
#include "Core/WebServer.h"
enum class ServerStatus {
STOPPED,
STARTING,
RUNNING,
STOPPING,
FINISHED,
};
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 WebServerFlags serverFlags;
std::mutex g_webServerLock;
static Path g_uploadPath; // TODO: Supply this through registration instead.
static std::atomic<int> g_uploadSessionId(1);
std::map<int, UploadProgress> g_uploadsInProgress;
// NOTE: These *only* encode spaces, which is almost enough.
std::vector<UploadProgress> GetUploadsInProgress() {
std::lock_guard<std::mutex> guard(g_webServerLock);
std::vector<UploadProgress> uploads;
uploads.reserve(g_uploadsInProgress.size());
for (const auto &pair : g_uploadsInProgress) {
uploads.push_back(pair.second);
}
return uploads;
}
std::string ServerUriEncode(std::string_view plain) {
return ReplaceAll(plain, " ", "%20");
}
std::string ServerUriDecode(std::string_view encoded) {
return ReplaceAll(encoded, "%20", " ");
}
static void UpdateStatus(ServerStatus s) {
std::lock_guard<std::mutex> guard(serverStatusLock);
serverStatus = s;
}
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 on the same wifi/LAN searching for the server.
static bool RegisterServer(int port) {
bool success = false;
http::Client http(nullptr);
bool cancelled = false;
net::RequestProgress progress(&cancelled);
Buffer theVoid = Buffer::Void();
http.SetUserAgent(StringFromFormat("PPSSPP/%s", PPSSPP_GIT_VERSION));
char resource4[1024]{};
if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV4)) {
if (http.Connect()) {
std::string ip = http.GetLocalIpAsString();
snprintf(resource4, sizeof(resource4) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
if (http.GET(http::RequestParams(resource4), &theVoid, &progress) > 0)
success = true;
theVoid.Skip(theVoid.size());
http.Disconnect();
}
}
if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV6)) {
// If IPv4 was successful, don't give this as much time (it blocks and sometimes IPv6 is broken.)
double timeout = success ? 2.0 : 10.0;
// We register both IPv4 and IPv6 in case the other client is using a different one.
if (resource4[0] != 0 && http.Connect(timeout)) {
if (http.GET(http::RequestParams(resource4), &theVoid, &progress) > 0)
success = true;
theVoid.Skip(theVoid.size());
http.Disconnect();
}
// Currently, we're not using keepalive, so gotta reconnect...
if (http.Connect(timeout)) {
char resource6[1024] = {};
std::string ip = http.GetLocalIpAsString();
snprintf(resource6, sizeof(resource6) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
if (http.GET(http::RequestParams(resource6), &theVoid, &progress) > 0)
success = true;
theVoid.Skip(theVoid.size());
http.Disconnect();
}
}
return success;
}
bool RemoteISOFileSupported(const std::string &filename) {
// Disc-like files.
if (endsWithNoCase(filename, ".cso") || endsWithNoCase(filename, ".iso") || endsWithNoCase(filename, ".chd")) {
return true;
}
// May work - but won't have supporting files.
if (endsWithNoCase(filename, ".pbp")) {
return true;
}
// Debugging files.
if (endsWithNoCase(filename, ".ppdmp")) {
return true;
}
return false;
}
static std::string RemotePathForRecent(const std::string &filename) {
Path path(filename);
if (path.Type() == PathType::HTTP) {
// Don't re-share HTTP files from some other device.
return std::string();
}
#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));
if (basename == "/EBOOT.PBP") {
// Go up one more folder.
size_t nextpos = filename.find_last_of(sep, basepos - 1);
basename = "/" + (nextpos == filename.npos ? filename : filename.substr(nextpos + 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 (RemoteISOFileSupported(basename)) {
return ServerUriEncode(basename);
}
return std::string();
}
static Path LocalFromRemotePath(std::string_view path) {
// This happens for some reason...
if (startsWith(path, "//")) {
path.remove_prefix(1);
}
switch ((RemoteISOShareType)g_Config.iRemoteISOShareType) {
case RemoteISOShareType::RECENT:
for (const std::string &filename : g_recentFiles.GetRecentFiles()) {
std::string basename = RemotePathForRecent(filename);
if (basename == path) {
return Path(filename);
}
}
return Path();
case RemoteISOShareType::LOCAL_FOLDER:
{
std::string decoded = ServerUriDecode(path);
if (decoded.empty() || decoded.front() != '/') {
return Path();
}
// First reject backslashes, in case of any Windows shenanigans.
if (decoded.find('\\') != std::string::npos) {
return Path();
}
// Then, reject slashes combined with ".." to prevent directory traversal. Hope this is enough.
if (decoded.find("/..") != std::string::npos) {
return Path();
}
return Path(g_Config.sRemoteISOSharedDir) / decoded;
}
default:
return Path();
}
}
static void DiscHandler(const http::ServerRequest &request, const Path &filename) {
s64 sz = File::GetFileSize(filename);
if (sz == 0) {
// Probably failed
request.WriteHttpResponseHeader("1.0", 404, -1, "text/plain");
request.Out()->Push("File not found.");
return;
}
std::string range;
if (request.Method() == http::RequestHeader::HEAD) {
request.WriteHttpResponseHeader("1.0", 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("1.0", 400, -1, "text/plain");
request.Out()->Push("Could not understand range request.");
return;
}
if (begin < 0 || begin > last || last >= sz) {
request.WriteHttpResponseHeader("1.0", 416, -1, "text/plain");
request.Out()->Push("Range goes outside of file.");
return;
}
FILE *fp = File::OpenCFile(filename, "rb");
if (!fp || File::Fseek(fp, begin, SEEK_SET) != 0) {
request.WriteHttpResponseHeader("1.0", 500, -1, "text/plain");
request.Out()->Push("File access failed.");
if (fp) {
fclose(fp);
}
return;
}
s64 len = last - begin + 1;
char contentRange[1024];
snprintf(contentRange, sizeof(contentRange), "Content-Range: bytes %lld-%lld/%lld\r\n", begin, last, sz);
request.WriteHttpResponseHeader("1.0", 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);
if (fread(buf, chunklen, 1, fp) != 1)
break;
request.Out()->Push(buf, chunklen);
}
fclose(fp);
delete[] buf;
request.Out()->Flush();
} else {
request.WriteHttpResponseHeader("1.0", 418, -1, "text/plain");
request.Out()->Push("This server only supports range requests.");
}
}
static void HandleListing(const http::ServerRequest &request) {
AndroidJNIThreadContext jniContext;
request.WriteHttpResponseHeader("1.0", 200, -1, "text/plain");
request.Out()->Printf("/\n");
if (serverFlags & WebServerFlags::DISCS) {
switch ((RemoteISOShareType)g_Config.iRemoteISOShareType) {
case RemoteISOShareType::RECENT:
// List the current discs in their recent order.
for (const std::string &filename : g_recentFiles.GetRecentFiles()) {
std::string basename = RemotePathForRecent(filename);
if (!basename.empty()) {
request.Out()->Printf("%s\n", basename.c_str());
}
}
break;
case RemoteISOShareType::LOCAL_FOLDER:
{
std::vector<File::FileInfo> entries;
std::string resource(request.resource());
Path localDir = LocalFromRemotePath(resource);
File::GetFilesInDir(localDir, &entries);
for (const auto &entry : entries) {
// TODO: Support browsing into subdirs. How are folders marked?
if (!entry.isDirectory && !RemoteISOFileSupported(entry.name)) {
continue;
}
std::string name = entry.name;
if (entry.isDirectory) {
name.push_back('/');
}
std::string encoded = ServerUriEncode(name);
request.Out()->Printf("%s\n", encoded.c_str());
}
break;
}
}
}
if (serverFlags & WebServerFlags::DEBUGGER) {
request.Out()->Printf("/debugger\n");
}
}
std::string RenderTemplate(std::string_view input, const std::map<std::string, std::string>& values) {
std::string output;
output.reserve(input.size());
size_t i = 0;
while (i < input.size()) {
if (i + 3 < input.size() && input[i] == '{' && input[i + 1] == '{') {
size_t end = input.find("}}", i + 2);
if (end != std::string_view::npos) {
// Trim whitespace around key
size_t startKey = i + 2;
while (startKey < end && std::isspace(static_cast<unsigned char>(input[startKey])))
startKey++;
size_t endKey = end;
while (endKey > startKey && std::isspace(static_cast<unsigned char>(input[endKey - 1])))
endKey--;
std::string key(input.substr(startKey, endKey - startKey));
auto it = values.find(key);
if (it != values.end()) {
output += it->second;
} else {
// Keep the original placeholder if key not found
output.append(input.substr(i, end + 2 - i));
}
i = end + 2;
continue;
}
}
output.push_back(input[i]);
++i;
}
return output;
}
static bool ServeAssetFile(const http::ServerRequest &request) {
// Skip the slash at the start of the resource path.
std::string_view filename = request.resource().substr(1);
if (filename.find("..") != std::string_view::npos) {
// Don't allow directory traversal.
return false;
}
std::string ext = Path(filename).GetFileExtension();
std::string_view mimeType = "text/plain";
if (ext == ".html") {
mimeType = "text/html";
} else if (ext == ".ico") {
mimeType = "image/x-icon";
} else if (ext == ".js") {
mimeType = "application/javascript";
} else if (ext == ".svg") {
mimeType = "image/svg+xml";
} else if (ext == ".png") {
mimeType = "image/png";
} else if (ext == ".css") {
mimeType = "text/css";
}
size_t size;
// TODO: ReadFile should take a string_view.
uint8_t *data = g_VFS.ReadFile(std::string(filename).c_str(), &size);
if (!data) {
// Try appending index.html
data = g_VFS.ReadFile((std::string(filename) + "/index.html").c_str(), &size);
mimeType = "text/html";
if (!data) {
return false;
}
INFO_LOG(Log::HTTP, "Redirected to /index.html");
}
std::string html = std::string((const char *)data, size);
delete[] data;
// This is a gross, gross hack to have here in ServeAssetFile, but oh well.
if (mimeType == "text/html") {
if (html.find("<!--upload-->") != std::string::npos) {
std::string uploadPath = GetFriendlyPath(g_uploadPath);
std::string deviceName = System_GetProperty(SYSPROP_NAME);
std::string computerName = System_GetProperty(SYSPROP_COMPUTER_NAME);
if (!computerName.empty()) {
deviceName = computerName;
}
std::map<std::string, std::string> values = {
{"upload_path", uploadPath},
{"device_name", deviceName},
};
// Use templating to insert some information.
html = RenderTemplate(html, values);
}
}
request.WriteHttpResponseHeader("1.0", 200, html.size(), std::string(mimeType).c_str());
request.Out()->Push(html.data(), html.size());
return true;
}
static void RedirectToDebugger(const http::ServerRequest &request) {
static const std::string payload = "Redirecting to debugger UI...\r\n";
request.WriteHttpResponseHeader("1.0", 301, payload.size(), "text/plain", "Location: /debugger/index.html\r\n");
request.Out()->Push(payload);
}
// TODO: Allow registering ServeAssetFile roots as well.
static void HandleFallback(const http::ServerRequest &request) {
SetCurrentThreadName("HandleFallback");
AndroidJNIThreadContext jniContext;
if ((serverFlags & WebServerFlags::DEBUGGER) != 0) {
if (request.resource() == "/debugger/") {
RedirectToDebugger(request);
return;
}
// Actually serve debugger files.
if (startsWith(request.resource(), "/debugger/")) {
if (ServeAssetFile(request)) {
return;
}
}
}
if (serverFlags & WebServerFlags::FILE_UPLOAD) {
Path uploadPath;
{
std::lock_guard<std::mutex> guard(g_webServerLock);
uploadPath = g_uploadPath;
}
if (startsWith(request.resource(), "/upload") && !uploadPath.empty()) {
if (ServeAssetFile(request)) {
return;
}
}
}
if (serverFlags & WebServerFlags::DISCS) {
std::string_view resource = request.resource();
Path localPath = LocalFromRemotePath(resource);
INFO_LOG(Log::Loader, "Serving %.*s from %s", (int)resource.size(), resource.data(), localPath.c_str());
if (!localPath.empty()) {
if (File::IsDirectory(localPath)) {
HandleListing(request);
} else {
DiscHandler(request, localPath);
}
return;
}
}
static const std::string payload = "404 not found\r\n";
request.WriteHttpResponseHeader("1.0", 404, payload.size(), "text/plain");
request.Out()->Push(payload);
}
static void ForwardDebuggerRequest(const http::ServerRequest &request) {
SetCurrentThreadName("ForwardDebuggerRequest");
// Hm, is this needed?
AndroidJNIThreadContext jniContext;
if (serverFlags & WebServerFlags::DEBUGGER) {
// Check if this is a websocket request...
std::string upgrade;
if (!request.GetHeader("upgrade", &upgrade)) {
upgrade.clear();
}
// Yes - proceed with the socket.
if (strcasecmp(upgrade.c_str(), "websocket") == 0) {
HandleDebuggerRequest(request);
} else {
RedirectToDebugger(request);
}
} else {
HandleFallback(request);
}
}
class ProgressTracker {
public:
ProgressTracker(int sessionId, s64 totalSize) : sessionId_(sessionId) {
std::lock_guard<std::mutex> guard(g_webServerLock);
g_uploadsInProgress[sessionId_] = UploadProgress{totalSize};
}
~ProgressTracker() {
std::lock_guard<std::mutex> guard(g_webServerLock);
g_uploadsInProgress.erase(sessionId_);
}
void SetFile(std::string_view filename, size_t size) {
std::lock_guard<std::mutex> guard(g_webServerLock);
auto &upload = g_uploadsInProgress[sessionId_];
upload.filename = filename;
upload.currentFileSize = size;
upload.uploadedBytes = 0;
}
void AddBytes(size_t bytes) {
std::lock_guard<std::mutex> guard(g_webServerLock);
g_uploadsInProgress[sessionId_].uploadedBytes += bytes;
}
private:
int sessionId_;
};
enum class MultiPartResult {
MoveToNext,
RequestError,
LocalError,
Done,
};
static bool IsSafeUploadFilename(std::string_view filename) {
return !filename.empty() && filename != "." && filename != ".." &&
filename.find_first_of("/\\") == std::string_view::npos;
}
static MultiPartResult HandleMultipartPart(const http::ServerRequest &request, std::string boundary, const Path &uploadPath, ProgressTracker &progress) {
std::string firstBoundary = request.In()->ReadLine();
if (firstBoundary != "--" + boundary) {
WARN_LOG(Log::HTTP, "Bad boundary: Expected --%s but got %s", boundary.c_str(), firstBoundary.c_str());
return MultiPartResult::RequestError;
}
std::string disposition = request.In()->ReadLine();
INFO_LOG(Log::HTTP, "Disposition: %s", disposition.c_str());
std::vector<std::string_view> parts;
SplitString(disposition, ';', parts);
if (parts.size() < 2 || !startsWith(parts[0], "Content-Disposition: form-data")) {
WARN_LOG(Log::HTTP, "Bad content disposition: %s", disposition.c_str());
return MultiPartResult::RequestError;
}
std::string filename;
for (const auto &part : parts) {
std::string_view key;
std::string_view value;
if (SplitStringOnce(part, &key, &value, '=')) {
key = StripSpaces(key);
value = StripQuotes(StripSpaces(value));
if (key == "name") {
INFO_LOG(Log::HTTP, "Upload field name: %.*s", STR_VIEW(value));
} else if (key == "filename") {
INFO_LOG(Log::HTTP, "Upload filename: %.*s", STR_VIEW(value));
filename = value;
} else if (key == "fileSize") {
INFO_LOG(Log::HTTP, "Upload filesize: %.*s", STR_VIEW(value));
}
} else if (equalsNoCase(StripSpaces(part), "Content-Disposition: form-data")) {
// this is the first part, ok, ignore.
} else {
WARN_LOG(Log::HTTP, "Bad content disposition part: %.*s", STR_VIEW(part));
}
}
if (!IsSafeUploadFilename(filename)) {
ERROR_LOG(Log::HTTP, "Invalid upload filename");
return MultiPartResult::RequestError;
}
std::string fileContentType = request.In()->ReadLine();
std::string secondBoundary = request.In()->ReadLine();
Path destPath = uploadPath / filename;
bool dryRun = false;
if (File::Exists(destPath)) {
INFO_LOG(Log::HTTP, "File already exists, entering dry run mode: %s", destPath.ToVisualString().c_str());
dryRun = true;
}
// Make sure the destination exists.
File::CreateFullPath(destPath.NavigateUp());
INFO_LOG(Log::HTTP, "Receiving '%s', writing to '%s' (unknown number of bytes)...", filename.c_str(), destPath.ToVisualString().c_str());
// OK, enter a loop where we read some data until we hit the boundary again.
// The boundary is chosen to be "unique" and unlikely to appear in the file. We trust that.
FILE *fp = nullptr;
if (!dryRun) {
fp = File::OpenCFile(destPath, "wb");
if (!fp) {
ERROR_LOG(Log::HTTP, "Failed to open destination file '%s' for writing, entering dry run mode.", destPath.ToVisualString().c_str());
dryRun = true; // We still want to keep reading the input stream - there might be more files following.
}
}
progress.SetFile(filename, 0);
u64 bytesTransferred = 0;
char buffer[net::InputSink::BUFFER_SIZE];
while (true) {
bool terminatorFound = false;
size_t readBytes = request.In()->ReadBinaryUntilTerminator(buffer, sizeof(buffer), "\r\n--" + boundary, &terminatorFound);
if (fp) {
if (fwrite(buffer, 1, readBytes, fp) != readBytes) {
ERROR_LOG(Log::HTTP, "Failed to write %d bytes to destination file '%s' - disk full?", (int)readBytes, destPath.ToVisualString().c_str());
fclose(fp);
// Delete the partially written file, so the user doesn't try to play it.
File::Delete(destPath);
return MultiPartResult::RequestError;
}
}
progress.AddBytes(readBytes);
bytesTransferred += readBytes;
if (terminatorFound) {
INFO_LOG(Log::HTTP, "Found terminator, skipping and proceeding.");
break;
}
}
INFO_LOG(Log::HTTP, "Total bytes transferred: %d", (int)bytesTransferred);
if (fp) {
fclose(fp);
}
auto n = GetI18NCategory(I18NCat::NETWORKING);
g_OSD.Show(OSDType::MESSAGE_SUCCESS, ApplySafeSubstitutions(n->T("File transfer complete: %1"), filename));
// NOTE: We already read the boundary above.
// However if this is the last part, the boundary will have "--\r\n" after it, otherwise there will be a line break.
// So, let's read two more bytes, to see if it's "--" or "\r\n".
std::string ending(2, 'x');
request.In()->TakeExact(ending.data(), 2);
if (ending == "--") {
INFO_LOG(Log::HTTP, "Upload of '%s' complete, '--' encountered. %d bytes left in buffer.", filename.c_str(), (int)request.In()->ValidAmount());
// Read the final \r\n.
std::string finalCRLF;
finalCRLF.resize(2);
request.In()->TakeExact(finalCRLF.data(), 2);
if (finalCRLF != "\r\n") {
// Not a big deal, but log it.
WARN_LOG(Log::HTTP, "Expected final CRLF after ending '--', got '%02x %02x'", finalCRLF[0], finalCRLF[1]);
}
return MultiPartResult::Done;
} else if (ending == "\r\n") {
INFO_LOG(Log::HTTP, "Upload of '%s' complete, continuing to next part.", filename.c_str());
return MultiPartResult::MoveToNext;
} else {
WARN_LOG(Log::HTTP, "Unexpected upload ending: '%s'. Bailing.", ending.c_str());
return MultiPartResult::RequestError;
}
}
// Handles a POST to upload a file.
// This uses the HTTP multipart protocol, which is arcane and complicated, unfortunately.
static void HandleUploadPost(const http::ServerRequest &request) {
AndroidJNIThreadContext jniContext;
// Do some sanity checks.
if (request.Method() != http::RequestHeader::POST) {
ERROR_LOG(Log::HTTP, "Wrong method");
return;
}
Path uploadPath;
{
std::lock_guard<std::mutex> guard(g_webServerLock);
uploadPath = g_uploadPath;
}
if (uploadPath.empty()) {
ERROR_LOG(Log::HTTP, "HandleUploadPost should not happen while uploadPath is empty");
return;
}
// Now start handling things.
std::string contentType;
if (!request.GetHeader("content-type", &contentType)) {
return;
}
size_t bpos = contentType.find("boundary=");
if (bpos == std::string::npos) {
return;
}
const std::string boundary = contentType.substr(bpos + strlen("boundary="));
// The total length of the entire multipart thing. This is just above the full size of the upload, so let's use it for progress.
u64 contentLength = request.Header().content_length;
if (contentLength == 0) {
WARN_LOG(Log::HTTP, "Bad content length");
return;
}
const int sessionId = g_uploadSessionId.fetch_add(1);
ProgressTracker progress(sessionId, contentLength);
while (true) {
MultiPartResult result = HandleMultipartPart(request, boundary, uploadPath, progress);
switch (result) {
case MultiPartResult::Done:
goto exitLoop;
case MultiPartResult::RequestError:
request.WriteHttpResponseHeader("1.0", 400, -1, "text/plain"); // Bad request
return;
case MultiPartResult::LocalError:
request.WriteHttpResponseHeader("1.0", 500, -1, "text/plain"); // Server error
return;
case MultiPartResult::MoveToNext:
// Else just continue to the next part.
break;
}
}
exitLoop:
// Now the buffer should be empty.
if (!request.In()->Empty()) {
size_t remaining = request.In()->ValidAmount();
WARN_LOG(Log::HTTP, "We didn't fully drain the request (%d bytes still buffered)", (int)remaining);
// std::string extraBytes;
// extraBytes.resize(remaining);
// request.In()->TakeExact(extraBytes.data(), remaining);
}
request.WriteHttpResponseHeader("1.0", 200, -1, "text/plain");
}
void WebServerSetUploadPath(const Path &path) {
std::lock_guard<std::mutex> guard(g_webServerLock);
g_uploadPath = path;
}
static void WebServerThread() {
SetCurrentThreadName("HTTPServer");
AndroidJNIThreadContext context; // Destructor detaches.
auto http = new http::Server(new NewThreadExecutor());
http->RegisterHandler("/", &HandleListing);
// This lists all the (current) recent ISOs. It also handles the debugger, which is very ugly.
http->SetFallbackHandler(&HandleFallback);
http->RegisterHandler("/debugger", &ForwardDebuggerRequest);
http->RegisterHandler("/upload_file", &HandleUploadPost);
if (!http->Listen(g_Config.iRemoteISOPort, "debugger-webserver")) {
if (!http->Listen(0, "debugger-webserver")) {
ERROR_LOG(Log::FileSystem, "Unable to listen on any port (debugger - webserver)");
UpdateStatus(ServerStatus::FINISHED);
return;
}
}
UpdateStatus(ServerStatus::RUNNING);
g_Config.iRemoteISOPort = http->Port();
RegisterServer(http->Port());
double lastRegister = time_now_d();
INFO_LOG(Log::HTTP, "Entering web server loop. Listening on port %d", g_Config.iRemoteISOPort);
while (RetrieveStatus() == ServerStatus::RUNNING) {
constexpr double webServerSliceSeconds = 0.2f;
http->RunSlice(webServerSliceSeconds);
double now = time_now_d();
if (now > lastRegister + 540.0) {
RegisterServer(http->Port());
lastRegister = now;
}
}
INFO_LOG(Log::HTTP, "Leaving web server loop.");
http->Stop();
StopAllDebuggers();
delete http;
UpdateStatus(ServerStatus::FINISHED);
}
// Only adds flags.
bool StartWebServer(WebServerFlags flags) {
std::lock_guard<std::mutex> guard(serverStatusLock);
switch (serverStatus) {
case ServerStatus::RUNNING:
if (((int)serverFlags & (int)flags) == (int)flags) {
// Already running with these flags.
return false;
}
serverFlags |= flags;
return true;
case ServerStatus::FINISHED:
serverThread.join();
[[fallthrough]]; // Intentional fallthrough.
case ServerStatus::STOPPED:
serverStatus = ServerStatus::STARTING;
serverFlags = flags;
serverThread = std::thread(&WebServerThread);
return true;
default:
return false;
}
}
// Only removes flags.
bool StopWebServer(WebServerFlags flags) {
std::lock_guard<std::mutex> guard(serverStatusLock);
if (serverStatus != ServerStatus::RUNNING) {
return false;
}
serverFlags &= ~flags;
if (serverFlags == WebServerFlags::NONE) {
serverStatus = ServerStatus::STOPPING;
}
return true;
}
bool WebServerStopping(WebServerFlags flags) {
std::lock_guard<std::mutex> guard(serverStatusLock);
return serverStatus == ServerStatus::STOPPING;
}
bool WebServerStopped(WebServerFlags flags) {
std::lock_guard<std::mutex> guard(serverStatusLock);
if (serverStatus == ServerStatus::RUNNING) {
return !(serverFlags & flags);
}
return serverStatus == ServerStatus::STOPPED || serverStatus == ServerStatus::FINISHED;
}
void ShutdownWebServer() {
StopWebServer(WebServerFlags::ALL);
if (serverStatus != ServerStatus::STOPPED)
serverThread.join();
serverStatus = ServerStatus::STOPPED;
}
bool WebServerRunning(WebServerFlags flags) {
return RetrieveStatus() == ServerStatus::RUNNING && (serverFlags & flags) != 0;
}
int WebServerPort() {
return g_Config.iRemoteISOPort;
}