mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 09:39:33 +01:00
Merge pull request #22945 from hrydgard/net-hardening
Web server, HTTP client, inet: Check Origin and bound what the network can make us do
This commit is contained in:
21 files changed
+277
-27
No files matched your search
@@ -13,6 +13,12 @@
|
||||
namespace fd_util {
|
||||
|
||||
bool WaitUntilReady(int fd, double timeout, bool for_write) {
|
||||
#if !PPSSPP_PLATFORM(WINDOWS)
|
||||
// FD_SET on an fd past the end of the set writes off the end of it, on the stack.
|
||||
if (fd < 0 || fd >= FD_SETSIZE) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
struct timeval tv;
|
||||
tv.tv_sec = (long)floor(timeout);
|
||||
tv.tv_usec = (long)((timeout - floor(timeout)) * 1000000.0);
|
||||
|
||||
@@ -499,8 +499,13 @@ int Client::ReadResponseEntity(net::Buffer *readbuf, const std::vector<std::stri
|
||||
// Just sanity checking...
|
||||
contentLength = 0;
|
||||
}
|
||||
if ((size_t)contentLength > maxResponseSize_) {
|
||||
ERROR_LOG(Log::HTTP, "Response too large: %d bytes", contentLength);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!readbuf->ReadAllWithProgress(sock(), contentLength, progress))
|
||||
// readbuf may already hold the start of the body, read along with the headers.
|
||||
if (readbuf->size() > maxResponseSize_ || !readbuf->ReadAllWithProgress(sock(), contentLength, progress, maxResponseSize_ - readbuf->size()))
|
||||
return -1;
|
||||
|
||||
// output now contains the rest of the reply. Dechunk it.
|
||||
@@ -521,7 +526,7 @@ int Client::ReadResponseEntity(net::Buffer *readbuf, const std::vector<std::stri
|
||||
if (gzip) {
|
||||
std::string compressed, decompressed;
|
||||
output->TakeAll(&compressed);
|
||||
bool result = decompress_string(compressed, &decompressed);
|
||||
bool result = decompress_string(compressed, &decompressed, maxResponseSize_);
|
||||
if (!result) {
|
||||
ERROR_LOG(Log::HTTP, "Error decompressing using zlib");
|
||||
if (progress) {
|
||||
@@ -581,6 +586,7 @@ int HTTPRequest::Perform(const std::string &url) {
|
||||
if (!userAgent_.empty()) {
|
||||
client.SetUserAgent(userAgent_);
|
||||
}
|
||||
client.SetMaxResponseSize(outfile_.empty() ? MAX_RESPONSE_IN_MEMORY : MAX_RESPONSE_TO_FILE);
|
||||
|
||||
if (!client.Resolve(fileUrl.Host().c_str(), fileUrl.Port())) {
|
||||
ERROR_LOG(Log::HTTP, "Failed resolving %s", url.c_str());
|
||||
@@ -625,6 +631,7 @@ void HTTPRequest::Do() {
|
||||
resultCode_ = 0;
|
||||
|
||||
std::string downloadURL = url_;
|
||||
int redirects = 0;
|
||||
while (resultCode_ == 0) {
|
||||
// This is where the new request is performed.
|
||||
int resultCode = Perform(downloadURL);
|
||||
@@ -641,6 +648,9 @@ void HTTPRequest::Do() {
|
||||
} else if (redirectURL == downloadURL || redirectURL == url_) {
|
||||
// Simple loop detected, bail out.
|
||||
resultCode_ = resultCode;
|
||||
} else if (++redirects > MAX_REDIRECTS) {
|
||||
ERROR_LOG(Log::HTTP, "Too many redirects requesting %s", url_.c_str());
|
||||
resultCode_ = resultCode;
|
||||
}
|
||||
|
||||
// Perform the next GET.
|
||||
|
||||
@@ -98,10 +98,16 @@ public:
|
||||
httpVersion_ = version;
|
||||
}
|
||||
|
||||
// The most body ReadResponseEntity accepts, before and after decompression.
|
||||
void SetMaxResponseSize(size_t size) {
|
||||
maxResponseSize_ = size;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string userAgent_;
|
||||
const char* httpVersion_;
|
||||
double headerTimeout_ = 900.0;
|
||||
size_t maxResponseSize_ = MAX_RESPONSE_IN_MEMORY;
|
||||
};
|
||||
|
||||
// Really an asynchronous request.
|
||||
|
||||
@@ -9,9 +9,19 @@
|
||||
#include "Common/Log.h"
|
||||
#include "Common/File/FileDescriptor.h"
|
||||
#include "Common/StringUtils.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
|
||||
namespace http {
|
||||
|
||||
// Limits on a request's header block, so a client can't hold a connection or grow memory forever.
|
||||
static constexpr int MAX_HEADER_LINES = 100;
|
||||
static constexpr size_t MAX_HEADER_BYTES = 64 * 1024;
|
||||
static constexpr double MAX_HEADER_SECONDS = 10.0;
|
||||
|
||||
static bool KeyIs(const char *key, int key_len, const char *name) {
|
||||
return key_len == (int)strlen(name) && !strncasecmp(key, name, key_len);
|
||||
}
|
||||
|
||||
RequestHeader::RequestHeader() {
|
||||
}
|
||||
|
||||
@@ -58,13 +68,13 @@ int RequestHeader::ParseHttpHeader(const char *buffer) {
|
||||
if (first_header_) {
|
||||
// Step 1: Method
|
||||
first_header_ = false;
|
||||
if (!memcmp(buffer, "GET ", 4)) {
|
||||
if (!strncmp(buffer, "GET ", 4)) {
|
||||
method = GET;
|
||||
buffer += 4;
|
||||
} else if (!memcmp(buffer, "HEAD ", 5)) {
|
||||
} else if (!strncmp(buffer, "HEAD ", 5)) {
|
||||
method = HEAD;
|
||||
buffer += 5;
|
||||
} else if (!memcmp(buffer, "POST ", 5)) {
|
||||
} else if (!strncmp(buffer, "POST ", 5)) {
|
||||
method = POST;
|
||||
buffer += 5;
|
||||
} else {
|
||||
@@ -133,14 +143,16 @@ int RequestHeader::ParseHttpHeader(const char *buffer) {
|
||||
SkipSpace(&buffer);
|
||||
int value_len = (int)strlen(buffer);
|
||||
|
||||
if (!strncasecmp(key, "User-Agent", key_len)) {
|
||||
if (KeyIs(key, key_len, "User-Agent")) {
|
||||
delete[] user_agent;
|
||||
user_agent = new char[value_len + 1];
|
||||
memcpy(user_agent, buffer, value_len + 1);
|
||||
VERBOSE_LOG(Log::HTTP, "user-agent: %s", user_agent);
|
||||
} else if (!strncasecmp(key, "Referer", key_len)) {
|
||||
} else if (KeyIs(key, key_len, "Referer")) {
|
||||
delete[] referer;
|
||||
referer = new char[value_len + 1];
|
||||
memcpy(referer, buffer, value_len + 1);
|
||||
} else if (!strncasecmp(key, "Content-Length", key_len)) {
|
||||
} else if (KeyIs(key, key_len, "Content-Length")) {
|
||||
content_length = atoi(buffer);
|
||||
VERBOSE_LOG(Log::HTTP, "Content-Length: %i", (int)content_length);
|
||||
} else {
|
||||
@@ -154,12 +166,21 @@ int RequestHeader::ParseHttpHeader(const char *buffer) {
|
||||
|
||||
void RequestHeader::ParseHeaders(net::InputSink *sink) {
|
||||
int line_count = 0;
|
||||
size_t total_bytes = 0;
|
||||
const double deadline = time_now_d() + MAX_HEADER_SECONDS;
|
||||
std::string line;
|
||||
while (sink->ReadLine(line)) {
|
||||
if (line.length() == 0) {
|
||||
// Blank line, this means end of headers.
|
||||
break;
|
||||
}
|
||||
total_bytes += line.length();
|
||||
if (line_count >= MAX_HEADER_LINES || total_bytes > MAX_HEADER_BYTES || time_now_d() > deadline) {
|
||||
WARN_LOG(Log::HTTP, "Request headers too long, dropping the request");
|
||||
status = 431;
|
||||
ok = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ParseHttpHeader(line.c_str());
|
||||
line_count++;
|
||||
|
||||
@@ -35,6 +35,7 @@ struct NaettBodySink {
|
||||
std::string postData;
|
||||
// Written by us, read by the transfer thread.
|
||||
std::atomic<bool> cancelled{false};
|
||||
size_t maxBytes = MAX_RESPONSE_IN_MEMORY;
|
||||
};
|
||||
|
||||
// Sinks belonging to requests that hadn't finished when they were torn down, which only happens
|
||||
@@ -62,6 +63,11 @@ int HTTPSRequest::WriteBodyThunk(const void *source, int bytes, void *userData)
|
||||
if (bytes <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (sink->buffer.size() + (size_t)bytes > sink->maxBytes) {
|
||||
// Fails the request, like cancelling does.
|
||||
ERROR_LOG(Log::HTTP, "Response larger than %lld bytes, giving up", (long long)sink->maxBytes);
|
||||
return 0;
|
||||
}
|
||||
char *dest = sink->buffer.Append((size_t)bytes);
|
||||
memcpy(dest, source, bytes);
|
||||
return bytes;
|
||||
@@ -84,6 +90,7 @@ void HTTPSRequest::Start() {
|
||||
// In case someone managed to cancel us between construction and here.
|
||||
sink_->cancelled = cancelled_;
|
||||
sink_->postData = postData_;
|
||||
sink_->maxBytes = outfile_.empty() ? MAX_RESPONSE_IN_MEMORY : MAX_RESPONSE_TO_FILE;
|
||||
|
||||
std::vector<naettOption *> options;
|
||||
options.push_back(naettMethod(method_ == RequestMethod::GET ? "GET" : "POST"));
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
|
||||
namespace http {
|
||||
|
||||
// Caps on a response body, so a server can't make us buffer until we run out of memory. Downloads
|
||||
// to a file are buffered in memory too, so they get a cap of their own.
|
||||
constexpr size_t MAX_RESPONSE_IN_MEMORY = 64 * 1024 * 1024;
|
||||
constexpr size_t MAX_RESPONSE_TO_FILE = 1024 * 1024 * 1024;
|
||||
// Redirects we follow before giving up.
|
||||
constexpr int MAX_REDIRECTS = 10;
|
||||
|
||||
enum class RequestMethod {
|
||||
GET,
|
||||
POST,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#else
|
||||
|
||||
#include <sys/select.h> /* FD_SETSIZE */
|
||||
#include <sys/socket.h> /* socket definitions */
|
||||
#include <sys/types.h> /* socket types */
|
||||
#include <sys/wait.h> /* for waitpid() */
|
||||
@@ -43,10 +44,14 @@
|
||||
#include "Common/Log.h"
|
||||
|
||||
|
||||
void NewThreadExecutor::Run(std::function<void()> func) {
|
||||
bool NewThreadExecutor::Run(std::function<void()> func) {
|
||||
// Every connection gets a thread, and we only ever joined them at shutdown - so a server that
|
||||
// had served N connections was still holding N joinable std::threads. Reap the finished ones.
|
||||
Prune();
|
||||
if (workers_.size() >= MAX_WORKERS) {
|
||||
// Otherwise anyone who can connect gets a thread per connection, until creating one throws.
|
||||
return false;
|
||||
}
|
||||
|
||||
auto done = std::make_shared<std::atomic<bool>>(false);
|
||||
Worker worker;
|
||||
@@ -56,6 +61,7 @@ void NewThreadExecutor::Run(std::function<void()> func) {
|
||||
done->store(true, std::memory_order_release);
|
||||
});
|
||||
workers_.push_back(std::move(worker));
|
||||
return true;
|
||||
}
|
||||
|
||||
void NewThreadExecutor::Prune() {
|
||||
@@ -328,7 +334,18 @@ bool Server::RunSlice(double timeout) {
|
||||
socklen_t client_addr_size = sizeof(client_addr);
|
||||
int conn_fd = accept(listenerSock_, &client_addr.sa, &client_addr_size);
|
||||
if (conn_fd >= 0) {
|
||||
executor_->Run(std::bind(&Server::HandleConnection, this, conn_fd));
|
||||
#if !PPSSPP_PLATFORM(WINDOWS)
|
||||
// The sinks and the WebSocket server wait on the socket with select().
|
||||
if (conn_fd >= FD_SETSIZE) {
|
||||
WARN_LOG(Log::HTTP, "Too many open files, dropping connection");
|
||||
closesocket(conn_fd);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (!executor_->Run(std::bind(&Server::HandleConnection, this, conn_fd))) {
|
||||
WARN_LOG(Log::HTTP, "Too many connections, dropping one");
|
||||
closesocket(conn_fd);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
class NewThreadExecutor {
|
||||
public:
|
||||
~NewThreadExecutor();
|
||||
void Run(std::function<void()> func);
|
||||
// Returns false, without running func, when MAX_WORKERS threads are already busy.
|
||||
bool Run(std::function<void()> func);
|
||||
|
||||
static constexpr size_t MAX_WORKERS = 64;
|
||||
|
||||
private:
|
||||
// Reap threads that have finished. Only called from the thread that calls Run().
|
||||
|
||||
@@ -58,7 +58,7 @@ bool Buffer::FlushSocket(uintptr_t sock, double timeout, bool *cancelled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Buffer::ReadAllWithProgress(int fd, int knownSize, RequestProgress *progress) {
|
||||
bool Buffer::ReadAllWithProgress(int fd, int knownSize, RequestProgress *progress, size_t maxBytes) {
|
||||
static constexpr float CANCEL_INTERVAL = 0.25f;
|
||||
std::vector<char> buf;
|
||||
// We're non-blocking and reading from an OS buffer, so try to read as much as we can at a time.
|
||||
@@ -71,7 +71,7 @@ bool Buffer::ReadAllWithProgress(int fd, int knownSize, RequestProgress *progres
|
||||
}
|
||||
|
||||
double st = time_now_d();
|
||||
int total = 0;
|
||||
int64_t total = 0;
|
||||
while (true) {
|
||||
bool ready = false;
|
||||
|
||||
@@ -95,6 +95,10 @@ bool Buffer::ReadAllWithProgress(int fd, int knownSize, RequestProgress *progres
|
||||
// Just try again on a would block error, not a real error.
|
||||
continue;
|
||||
}
|
||||
if ((uint64_t)total + retval > maxBytes) {
|
||||
ERROR_LOG(Log::IO, "Response larger than %lld bytes, giving up", (long long)maxBytes);
|
||||
return false;
|
||||
}
|
||||
char *p = Append((size_t)retval);
|
||||
memcpy(p, &buf[0], retval);
|
||||
total += retval;
|
||||
|
||||
@@ -24,7 +24,8 @@ public:
|
||||
bool FlushSocket(uintptr_t sock, double timeout, bool *cancelled = nullptr);
|
||||
|
||||
// If you know the size of the file to read, pass it in knownSize, for best performance and for progress reporting.
|
||||
bool ReadAllWithProgress(int fd, int knownSize, RequestProgress *progress);
|
||||
// Fails once more than maxBytes have arrived.
|
||||
bool ReadAllWithProgress(int fd, int knownSize, RequestProgress *progress, size_t maxBytes);
|
||||
|
||||
// < 0: error
|
||||
// >= 0: number of bytes read
|
||||
|
||||
+2
-1
@@ -27,6 +27,8 @@ public:
|
||||
|
||||
bool Empty() const;
|
||||
bool TryFill();
|
||||
// Waits (up to a few seconds) for more data to arrive. False on timeout, error or end of stream.
|
||||
bool Block();
|
||||
bool HasError() const { return hasError_; }
|
||||
// True once the peer has closed its end. Sticky - no more data can ever arrive, which is not
|
||||
// the same as "nothing right now", and callers that wait for more need to tell them apart.
|
||||
@@ -44,7 +46,6 @@ public:
|
||||
private:
|
||||
std::pair<std::string_view, std::string_view> BufferParts() const;
|
||||
void Fill();
|
||||
bool Block();
|
||||
void AccountDrain(size_t bytes);
|
||||
size_t FindNewline() const;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "Common/Crypto/sha1.h"
|
||||
#include "Common/Log.h"
|
||||
#include "Common/StringUtils.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
|
||||
static const char *const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
@@ -19,6 +20,10 @@ static const char *const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
// claiming an enormous frame length before sending any actual data.
|
||||
// TODO: Make configurable?
|
||||
static constexpr uint64_t MAX_WS_MESSAGE_SIZE = 128 * 1024 * 1024;
|
||||
// How much we queue for a client that isn't reading, before giving up on it.
|
||||
static constexpr size_t MAX_WS_OUT_BACKLOG = 128 * 1024 * 1024;
|
||||
// How long we wait for our close frame to go out.
|
||||
static constexpr double WS_CLOSE_TIMEOUT_SECONDS = 2.0;
|
||||
|
||||
namespace net {
|
||||
|
||||
@@ -214,6 +219,7 @@ void WebSocketServer::Close(WebSocketClose reason) {
|
||||
}
|
||||
|
||||
sentClose_ = true;
|
||||
sentCloseTime_ = time_now_d();
|
||||
}
|
||||
|
||||
// The connection is gone or unusable - there's nothing left to send and no point waiting for
|
||||
@@ -247,6 +253,11 @@ bool WebSocketServer::Process(float timeout) {
|
||||
open_ = false;
|
||||
return false;
|
||||
}
|
||||
if (sentClose_ && time_now_d() - sentCloseTime_ > WS_CLOSE_TIMEOUT_SECONDS) {
|
||||
// The peer isn't reading, so the close frame is never going out.
|
||||
CloseAbnormally();
|
||||
return false;
|
||||
}
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = floor(timeout);
|
||||
@@ -437,11 +448,14 @@ bool WebSocketServer::ReadFrame() {
|
||||
}
|
||||
|
||||
bool WebSocketServer::ReadPending() {
|
||||
// Grow by at most what one read can return, not by the length the frame claims: a client could
|
||||
// otherwise make us commit (and zero, on every lap) MAX_WS_MESSAGE_SIZE without sending it.
|
||||
size_t pos = pendingBuf_.size();
|
||||
pendingBuf_.resize(pendingBuf_.size() + pendingLeft_);
|
||||
size_t chunk = (size_t)std::min(pendingLeft_, (uint64_t)InputSink::BUFFER_SIZE);
|
||||
pendingBuf_.resize(pos + chunk);
|
||||
|
||||
// Read what we can.
|
||||
size_t readBytes = in_->TakeAtMost((char *)&pendingBuf_[pos], pendingLeft_);
|
||||
size_t readBytes = in_->TakeAtMost((char *)&pendingBuf_[pos], chunk);
|
||||
for (size_t i = 0; i < readBytes; ++i) {
|
||||
pendingBuf_[pos + i] ^= pendingMask_[i & 3];
|
||||
}
|
||||
@@ -599,6 +613,9 @@ void WebSocketServer::SendHeader(bool fin, int opcode, size_t sz) {
|
||||
}
|
||||
|
||||
void WebSocketServer::SendBytes(const void *p, size_t sz) {
|
||||
if (outputOverflowed_) {
|
||||
return;
|
||||
}
|
||||
const char *data = (const char *)p;
|
||||
if (OutBufPending() == 0) {
|
||||
size_t pushed = out_->PushAtMost(data, sz);
|
||||
@@ -607,6 +624,18 @@ void WebSocketServer::SendBytes(const void *p, size_t sz) {
|
||||
}
|
||||
|
||||
if (sz != 0) {
|
||||
if (OutBufPending() + sz > MAX_WS_OUT_BACKLOG) {
|
||||
// The client stopped reading. Drop the connection rather than buffer without limit.
|
||||
ERROR_LOG(Log::IO, "WebSocket client isn't reading, dropping it");
|
||||
outputOverflowed_ = true;
|
||||
closeReason_ = WebSocketClose::ABNORMAL;
|
||||
sentClose_ = true;
|
||||
sentCloseTime_ = time_now_d();
|
||||
outBuf_.clear();
|
||||
outBufOffset_ = 0;
|
||||
out_->Discard();
|
||||
return;
|
||||
}
|
||||
size_t pos = outBuf_.size();
|
||||
outBuf_.resize(pos + sz);
|
||||
memcpy(&outBuf_[pos], data, sz);
|
||||
|
||||
@@ -90,6 +90,10 @@ protected:
|
||||
|
||||
bool open_ = true;
|
||||
bool sentClose_ = false;
|
||||
// When sentClose_ was set. A peer that never reads would otherwise keep us waiting forever.
|
||||
double sentCloseTime_ = 0.0;
|
||||
// Set when the peer stopped reading and the backlog hit its cap. Nothing more gets queued.
|
||||
bool outputOverflowed_ = false;
|
||||
int fragmentOpcode_ = -1;
|
||||
size_t fd_ = 0;
|
||||
InputSink *in_ = nullptr;
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/StringUtils.h"
|
||||
#include "Common/Thread/ThreadUtil.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
#include "Core/Core.h"
|
||||
@@ -226,9 +228,63 @@ void WebSocketDebuggerTick() {
|
||||
}
|
||||
}
|
||||
|
||||
// Browsers let any page open a WebSocket to localhost, and the debugger can write files and memory.
|
||||
// So only accept pages from this machine, from this server (the bundled UI), or the official web
|
||||
// debugger. Non-browser clients don't send an Origin.
|
||||
static bool IsAllowedOrigin(const http::ServerRequest &request) {
|
||||
std::string origin;
|
||||
if (!request.GetHeader("origin", &origin)) {
|
||||
return true;
|
||||
}
|
||||
auto toLower = [](std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return (char)tolower(c); });
|
||||
return s;
|
||||
};
|
||||
origin = toLower(origin);
|
||||
|
||||
size_t schemeEnd = origin.find("://");
|
||||
if (schemeEnd == std::string::npos) {
|
||||
// Includes "null", sent by file:// pages and sandboxed frames.
|
||||
return false;
|
||||
}
|
||||
const std::string scheme = origin.substr(0, schemeEnd);
|
||||
if (scheme != "http" && scheme != "https") {
|
||||
return false;
|
||||
}
|
||||
const std::string hostPort = origin.substr(schemeEnd + 3);
|
||||
if (hostPort == "ppsspp-debugger.unknownbrackets.org") {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string hostname = hostPort;
|
||||
if (startsWith(hostname, "[")) {
|
||||
hostname = hostname.substr(0, hostname.find(']') + 1);
|
||||
} else {
|
||||
hostname = hostname.substr(0, hostname.find(':'));
|
||||
}
|
||||
if (hostname == "localhost" || hostname == "127.0.0.1" || hostname == "[::1]") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A name matching Host proves nothing: an attacker's domain can be made to resolve to us (DNS
|
||||
// rebinding). So only an IP address, the way the bundled UI is reached over the LAN.
|
||||
const bool isIP = startsWith(hostname, "[") || (!hostname.empty() && hostname.find_first_not_of("0123456789.") == std::string::npos);
|
||||
std::string host;
|
||||
return isIP && request.GetHeader("host", &host) && toLower(host) == hostPort;
|
||||
}
|
||||
|
||||
void HandleDebuggerRequest(const http::ServerRequest &request) {
|
||||
SetCurrentThreadName("WebSocketDebugger");
|
||||
|
||||
if (!IsAllowedOrigin(request)) {
|
||||
std::string origin;
|
||||
request.GetHeader("origin", &origin);
|
||||
WARN_LOG(Log::Debugger, "Rejected debugger connection from origin '%s'", origin.c_str());
|
||||
request.WriteHttpResponseHeader("1.1", 403, -1, "text/plain");
|
||||
request.Out()->Push("Origin not allowed.");
|
||||
return;
|
||||
}
|
||||
|
||||
net::WebSocketServer *ws = net::WebSocketServer::CreateAsUpgrade(request, "debugger.ppsspp.org");
|
||||
if (!ws) {
|
||||
return;
|
||||
|
||||
@@ -244,6 +244,8 @@ size_t HTTPFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags f
|
||||
|
||||
// TODO: Would be nice to read directly.
|
||||
net::Buffer output;
|
||||
// We only use what we asked for, so don't let the server send more than that.
|
||||
client_.SetMaxResponseSize((size_t)(absoluteEnd - absolutePos));
|
||||
int res = client_.ReadResponseEntity(&readbuf, responseHeaders, &output, &progress_);
|
||||
if (res != 0) {
|
||||
ERROR_LOG(Log::Loader, "Unable to read HTTP response entity: %d", res);
|
||||
|
||||
@@ -16,7 +16,8 @@ InetSocket *SocketManager::CreateSocket(int *index, int *returned_errno, SocketS
|
||||
int hostProtocol = convertSocketProtoPSP2Host(protocol);
|
||||
|
||||
SOCKET hostSock = ::socket(hostDomain, hostType, hostProtocol);
|
||||
if (hostSock < 0) {
|
||||
// SOCKET is unsigned on Windows, so not < 0.
|
||||
if (hostSock == INVALID_SOCKET) {
|
||||
*returned_errno = socket_errno;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -305,6 +305,10 @@ bool LoadDNSForGameID(std::string_view gameID, std::string_view jsonStr, InfraDN
|
||||
if (game.hasChild("domains", JSON_OBJECT)) {
|
||||
const JsonGet domains = game.getDict("domains");
|
||||
for (const auto &iter : domains.value_) {
|
||||
// toString() on anything else isn't a string pointer.
|
||||
if (iter->value.getTag() != JSON_STRING) {
|
||||
continue;
|
||||
}
|
||||
std::string domain = std::string(iter->key);
|
||||
std::string ipAddr = std::string(iter->value.toString());
|
||||
dns->fixedDNS[domain] = ipAddr;
|
||||
|
||||
@@ -922,6 +922,46 @@ static bool StartInetWait(InetWaitOp op, const u32 *args, int numArgs, s64 timeo
|
||||
return true;
|
||||
}
|
||||
|
||||
// The entry points check the arguments before a wait starts, but a wait can also come from a
|
||||
// savestate. Same checks, failing with EFAULT (errno set) like the entry points would.
|
||||
static bool InetWaitArgsValid(const InetWait &wait, SceUID threadID) {
|
||||
const u32 *args = wait.args;
|
||||
const char *func = "inet wait";
|
||||
auto optional = [&](u32 ptr, u32 size) {
|
||||
return ptr == 0 || InetValidRange(ptr, size, func, threadID);
|
||||
};
|
||||
switch (wait.op) {
|
||||
case InetWaitOp::Select:
|
||||
if ((int)args[0] < 0 || (int)args[0] > PSP_NET_INET_FD_SETSIZE) {
|
||||
SetInetErrno(ERROR_INET_EINVAL, func, threadID);
|
||||
return false;
|
||||
}
|
||||
return optional(args[1], sizeof(SceNetInetFdSet)) && optional(args[2], sizeof(SceNetInetFdSet)) && optional(args[3], sizeof(SceNetInetFdSet));
|
||||
case InetWaitOp::Poll:
|
||||
if (args[1] > PSP_NET_INET_FD_SETSIZE) {
|
||||
SetInetErrno(ERROR_INET_EINVAL, func, threadID);
|
||||
return false;
|
||||
}
|
||||
return InetValidRange(args[0], args[1] * (u32)sizeof(SceNetInetPollfd), func, threadID);
|
||||
case InetWaitOp::Recv:
|
||||
case InetWaitOp::Send:
|
||||
return InetValidRange(args[1], args[2], func, threadID);
|
||||
case InetWaitOp::RecvFrom:
|
||||
return InetValidRange(args[1], args[2], func, threadID) && optional(args[4], sizeof(SceNetInetSockaddr)) && optional(args[5], 4);
|
||||
case InetWaitOp::SendTo:
|
||||
return InetValidRange(args[1], args[2], func, threadID) && optional(args[4], sizeof(SceNetInetSockaddr));
|
||||
case InetWaitOp::Accept:
|
||||
return optional(args[1], sizeof(SceNetInetSockaddr)) && optional(args[2], 4);
|
||||
case InetWaitOp::Connect:
|
||||
case InetWaitOp::RecvMsg:
|
||||
// Connect has no pointers, and TryRecvMsg checks its msghdr on every attempt.
|
||||
return true;
|
||||
default:
|
||||
SetInetErrno(ERROR_INET_EINVAL, func, threadID);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void __NetInetWaitPoll(u64 userdata, int cyclesLate) {
|
||||
const SceUID threadID = (SceUID)userdata;
|
||||
auto it = g_inetWaits.find(threadID);
|
||||
@@ -936,6 +976,12 @@ static void __NetInetWaitPoll(u64 userdata, int cyclesLate) {
|
||||
}
|
||||
|
||||
const InetWait wait = it->second;
|
||||
if (!InetWaitArgsValid(wait, threadID)) {
|
||||
g_inetWaits.erase(it);
|
||||
__KernelResumeThreadFromWait(threadID, -1);
|
||||
__KernelReSchedule("inet wait failed");
|
||||
return;
|
||||
}
|
||||
const InetTry attempt = TryInetOp(wait.op, threadID, wait.args, false);
|
||||
const s64 now = CoreTiming::GetTicks(currentMIPS);
|
||||
if (attempt.wouldBlock && (wait.deadline < 0 || now < wait.deadline)) {
|
||||
@@ -1964,6 +2010,12 @@ static bool ReadInetMsghdr(u32 msghdrPtr, InetMsghdr *msg, std::vector<HostIovec
|
||||
SetInetErrno(ERROR_INET_EINVAL, func, threadID);
|
||||
return false;
|
||||
}
|
||||
// The BSD stack's limit (UIO_MAXIOV). Otherwise only guest memory bounds it, and a blocking
|
||||
// recvmsg rebuilds the list on every retry.
|
||||
if (msg->msg_iovlen > 1024) {
|
||||
SetInetErrno(ERROR_INET_EMSGSIZE, func, threadID);
|
||||
return false;
|
||||
}
|
||||
// In 64 bits, so a huge count can't wrap around to a small size.
|
||||
const u64 iovBytes = (u64)msg->msg_iovlen * sizeof(SceNetIovec);
|
||||
if ((msg->msg_name && !InetValidRange(msg->msg_name, sizeof(SceNetInetSockaddr), func, threadID)) ||
|
||||
|
||||
@@ -102,7 +102,9 @@ static int CheckNtoAArgs(u32 hostnamePtr, u32 inAddrPtr) {
|
||||
|
||||
// Note: timeouts are in seconds
|
||||
static int NetResolver_StartNtoA(NetResolver *resolver, u32 hostnamePtr, u32 inAddrPtr, int timeout, int retry) {
|
||||
std::string hostname = std::string(safe_string(Memory::GetCharPointer(hostnamePtr)));
|
||||
// CheckNtoAArgs checked the address. The name needn't be terminated before the end of memory.
|
||||
const char *hostnameChars = Memory::GetCharPointerUnchecked(hostnamePtr);
|
||||
std::string hostname(hostnameChars, strnlen(hostnameChars, Memory::MaxSizeAtAddress(hostnamePtr)));
|
||||
|
||||
// Process hostname with infra-DNS
|
||||
std::string processedHostname = ProcessHostnameWithInfraDNS(hostname);
|
||||
@@ -154,7 +156,10 @@ static int sceNetResolverStartNtoA(int resolverId, u32 hostnamePtr, u32 inAddrPt
|
||||
return hleLogError(Log::sceNet, error);
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < retry; ++attempt) {
|
||||
// Each attempt blocks the emulator for as long as the host's lookups take (seconds against a dead
|
||||
// DNS server), and the game's count isn't limited, so a couple will do.
|
||||
const int attempts = std::min(retry, 2);
|
||||
for (int attempt = 0; attempt < attempts; ++attempt) {
|
||||
const int status = NetResolver_StartNtoA(&iter->second, hostnamePtr, inAddrPtr, timeout, retry);
|
||||
if (status >= 0) {
|
||||
return hleDelayResult(hleLogDebugOrError(Log::sceNet, status), "resolver lookup", RESOLVER_LOOKUP_US);
|
||||
|
||||
+9
-5
@@ -310,7 +310,10 @@ static void DiscHandler(const http::ServerRequest &request, const Path &filename
|
||||
s64 chunklen = std::min(len - pos, (s64)CHUNK_SIZE);
|
||||
if (fread(buf, chunklen, 1, fp) != 1)
|
||||
break;
|
||||
request.Out()->Push(buf, chunklen);
|
||||
if (!request.Out()->Push(buf, chunklen)) {
|
||||
// The client stopped reading or went away. Each failed Push already waited a while.
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
delete[] buf;
|
||||
@@ -679,10 +682,11 @@ static MultiPartResult HandleMultipartPart(const http::ServerRequest &request, s
|
||||
}
|
||||
progress.AddBytes(readBytes);
|
||||
bytesTransferred += readBytes;
|
||||
if (readBytes == 0 && !terminatorFound && (request.In()->AtEnd() || request.In()->HasError())) {
|
||||
// Peer went away mid-upload (a cancelled browser upload, say). Without this the loop
|
||||
// spins forever, which also blocks web server shutdown since it joins its threads.
|
||||
ERROR_LOG(Log::HTTP, "Connection closed during upload of '%s'", filename.c_str());
|
||||
// ReadBinaryUntilTerminator doesn't wait, so wait here for more data when there was none.
|
||||
if (readBytes == 0 && !terminatorFound && !request.In()->Block()) {
|
||||
// Peer went away mid-upload (a cancelled browser upload, say), or stopped sending. Without
|
||||
// this the loop spins forever, which also blocks web server shutdown since it joins its threads.
|
||||
ERROR_LOG(Log::HTTP, "Connection closed or timed out during upload of '%s'", filename.c_str());
|
||||
if (fp) {
|
||||
fclose(fp);
|
||||
File::Delete(destPath);
|
||||
|
||||
@@ -34,8 +34,18 @@ This doc is a local reference, the user-facing documentation is on the website.
|
||||
to `/debugger` without a websocket Upgrade just redirects to the bundled
|
||||
web UI at `/debugger/index.html`).
|
||||
- Messages are JSON, both directions, always shaped as `{"event": "NAME", ...}`.
|
||||
- One WebSocket connection = one client; PPSSPP does not limit the number of
|
||||
simultaneous debugger connections.
|
||||
- One WebSocket connection = one client. The web server as a whole handles at
|
||||
most 64 connections at a time (`NewThreadExecutor::MAX_WORKERS`), debugger
|
||||
connections included.
|
||||
- Browser pages are checked by their `Origin` header, since any page can open a
|
||||
WebSocket to localhost and the debugger can write memory and files. Allowed:
|
||||
no `Origin` at all (native clients, wsdbg, scripts), pages served from
|
||||
`localhost`, `127.0.0.1` or `[::1]` on any port, pages served by this server
|
||||
itself by IP address (the `Origin` matches `Host`, as for the bundled UI; a
|
||||
hostname doesn't count, since DNS rebinding can point one at us), and
|
||||
`ppsspp-debugger.unknownbrackets.org`. Anything else gets a 403.
|
||||
- A client that stops reading gets dropped once 128 MB of responses are queued
|
||||
for it.
|
||||
- The debugger only actually does anything while `WebServerFlags::DEBUGGER`
|
||||
is enabled (see "Enabling it" below) - the HTTP server itself may also be
|
||||
running for other reasons (Remote ISO, upload).
|
||||
|
||||
Reference in new issue
Block a user