mirror of
https://github.com/DaedalusX64/daedalus.git
synced 2025-04-02 10:21:48 -04:00
503 lines
12 KiB
C++
503 lines
12 KiB
C++
#include "BuildOptions.h"
|
|
#include "Base/Types.h"
|
|
|
|
#ifdef DAEDALUS_DEBUG_DISPLAYLIST
|
|
#include "SysPosix/Debug/WebDebug.h"
|
|
#include "SysPosix/Debug/WebDebugTemplate.h"
|
|
#include "third_party/webby/webby.h"
|
|
|
|
#ifdef DAEDALUS_W32
|
|
#include <winsock2.h>
|
|
#endif
|
|
|
|
#ifdef DAEDALUS_POSIX
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include <stdarg.h>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <filesystem>
|
|
|
|
#include "Debug/DBGConsole.h"
|
|
#include "Base/MathUtil.h"
|
|
#include "System/IO.h"
|
|
#include "Base/Macros.h"
|
|
#include "Utility/StringUtil.h"
|
|
#include "System/Thread.h"
|
|
|
|
|
|
enum
|
|
{
|
|
MAX_WSCONN = 8
|
|
};
|
|
|
|
struct WebDebugHandlerEntry
|
|
{
|
|
const char * Request;
|
|
WebDebugHandler Handler;
|
|
void * Arg;
|
|
};
|
|
|
|
struct StaticResource
|
|
{
|
|
std::string Resource;
|
|
std::string FullPath;
|
|
};
|
|
|
|
static void * gServerMemory = NULL;
|
|
static struct WebbyServer * gServer = NULL;
|
|
static volatile bool gKeepRunning = false;
|
|
static ThreadHandle gThread = kInvalidThreadHandle;
|
|
|
|
static int ws_connection_count;
|
|
static struct WebbyConnection * ws_connections[MAX_WSCONN];
|
|
static std::vector<WebDebugHandlerEntry> gHandlers;
|
|
static std::vector<StaticResource> gStaticResources;
|
|
|
|
const char * const kApplicationJavascript = "application/javascript";
|
|
const char * const kApplicationJSON = "application/json";
|
|
const char * const kImagePng = "image/png";
|
|
const char * const kTextCSS = "text/css";
|
|
const char * const kTextHTML = "text/html";
|
|
const char * const kTextPlain = "text/plain";
|
|
|
|
void WebDebug_Register(const char * request, WebDebugHandler handler, void * arg)
|
|
{
|
|
WebDebugHandlerEntry entry = { request, handler, arg };
|
|
gHandlers.push_back(entry);
|
|
}
|
|
|
|
static void test_log(const char* text)
|
|
{
|
|
printf("[debug] %s\n", text);
|
|
fflush(stdout);
|
|
}
|
|
|
|
WebDebugConnection::WebDebugConnection(WebbyConnection * connection)
|
|
: mConnection(connection)
|
|
, mState(kUnresponded)
|
|
, mBytesExpected(0)
|
|
, mBytesWritten(0)
|
|
{
|
|
if (const char * params = connection->request.query_params)
|
|
{
|
|
std::vector<ConstStringRef> args;
|
|
Split(params, '&', &args);
|
|
|
|
mQueryParams.reserve(args.size());
|
|
for (size_t i = 0; i < args.size(); ++i)
|
|
{
|
|
Param param;
|
|
SplitAt(args[i], '=', ¶m.Key, ¶m.Value);
|
|
|
|
mQueryParams.push_back(param);
|
|
}
|
|
}
|
|
}
|
|
|
|
const char * WebDebugConnection::GetQueryString() const
|
|
{
|
|
return mConnection->request.query_params;
|
|
}
|
|
|
|
void WebDebugConnection::BeginResponse(int code, int content_length, const char * content_type)
|
|
{
|
|
DAEDALUS_ASSERT(mState == kUnresponded, "Already responded to this request");
|
|
|
|
mBytesExpected = content_length >= 0 ? content_length : 0;
|
|
|
|
WebbyHeader headers[] = {
|
|
{ "Content-Type", content_type },
|
|
};
|
|
|
|
WebbyBeginResponse(mConnection, code, content_length, headers, ARRAYSIZE(headers));
|
|
mState = kResponding;
|
|
}
|
|
|
|
size_t WebDebugConnection::Write(const void * p, size_t len)
|
|
{
|
|
mBytesWritten += len;
|
|
return WebbyWrite(mConnection, p, len);
|
|
}
|
|
|
|
void WebDebugConnection::Flush()
|
|
{
|
|
// Ignore this, it's not useful for network connections.
|
|
}
|
|
|
|
void WebDebugConnection::WriteString(const char * str)
|
|
{
|
|
DAEDALUS_ASSERT(mState == kResponding, "Should be in Responding state");
|
|
|
|
Write(str, strlen(str));
|
|
}
|
|
|
|
void WebDebugConnection::WriteF(const char * format, ...)
|
|
{
|
|
static const u32 kBufferLen = 1024;
|
|
char buffer[kBufferLen];
|
|
|
|
va_list va;
|
|
va_start(va, format);
|
|
vsnprintf( buffer, kBufferLen, format, va );
|
|
|
|
// This should be guaranteed...
|
|
buffer[kBufferLen-1] = 0;
|
|
va_end(va);
|
|
|
|
Write(buffer, strlen(buffer));
|
|
}
|
|
|
|
void WebDebugConnection::EndResponse()
|
|
{
|
|
DAEDALUS_ASSERT(mState == kResponding, "Should be in Responding state");
|
|
DAEDALUS_ASSERT(mBytesExpected == 0 || mBytesWritten == mBytesExpected,
|
|
"Promised %d bytes, but only wrote %d",
|
|
mBytesExpected, mBytesWritten);
|
|
|
|
WebbyEndResponse(mConnection);
|
|
mState = kResponded;
|
|
}
|
|
|
|
void Generate404(WebDebugConnection * connection, const char * request)
|
|
{
|
|
connection->BeginResponse(404, -1, kTextHTML );
|
|
|
|
WriteStandardHeader(connection, "404 - Page Not Found");
|
|
|
|
connection->WriteString(
|
|
"<div class=\"container\">\n"
|
|
" <div class=\"row\">\n"
|
|
" <div class=\"span12\">\n"
|
|
);
|
|
connection->WriteString("<h1>404 - Page Not Found</h1>\n");
|
|
connection->WriteF("<div>%s was not found. Boo.</div>", request);
|
|
connection->WriteString(
|
|
" </div>\n"
|
|
" </div>\n"
|
|
"</div>\n"
|
|
);
|
|
|
|
WriteStandardFooter(connection);
|
|
connection->EndResponse();
|
|
}
|
|
|
|
void Generate500(WebDebugConnection * connection, const char * message)
|
|
{
|
|
connection->BeginResponse(500, -1, kTextHTML );
|
|
|
|
WriteStandardHeader(connection, "404 - Page Not Found");
|
|
|
|
connection->WriteString(
|
|
"<div class=\"container\">\n"
|
|
" <div class=\"row\">\n"
|
|
" <div class=\"span12\">\n"
|
|
);
|
|
connection->WriteString("<h1>500 - Epic Fail</h1>\n");
|
|
connection->WriteF("<div>%s. Sad Panda :(</div>", message);
|
|
connection->WriteString(
|
|
" </div>\n"
|
|
" </div>\n"
|
|
"</div>\n"
|
|
);
|
|
|
|
WriteStandardFooter(connection);
|
|
connection->EndResponse();
|
|
}
|
|
|
|
static const char * GetContentTypeForFilename(const char * filename)
|
|
{
|
|
const char * ext = strrchr(filename, '.');
|
|
if (ext)
|
|
{
|
|
if (strcmp(ext, ".js") == 0) return kApplicationJavascript;
|
|
if (strcmp(ext, ".css") == 0) return kTextCSS;
|
|
if (strcmp(ext, ".html") == 0) return kTextHTML;
|
|
if (strcmp(ext, ".png") == 0) return kImagePng;
|
|
}
|
|
|
|
DBGConsole_Msg(0, "Unknown filetype [C%s]", filename);
|
|
return kTextPlain;
|
|
}
|
|
|
|
static void ServeFile(WebDebugConnection * connection, const char * filename)
|
|
{
|
|
FILE * fh = fopen(filename, "rb");
|
|
if (!fh)
|
|
{
|
|
Generate404(connection, filename);
|
|
return;
|
|
}
|
|
|
|
connection->BeginResponse(200, -1, GetContentTypeForFilename(filename));
|
|
|
|
static const size_t kBufSize = 1024;
|
|
size_t len_read;
|
|
char buf[kBufSize];
|
|
do
|
|
{
|
|
len_read = fread(buf, 1, kBufSize, fh);
|
|
if (len_read > 0)
|
|
connection->Write(buf, len_read);
|
|
}
|
|
while (len_read == kBufSize);
|
|
|
|
connection->EndResponse();
|
|
fclose(fh);
|
|
}
|
|
|
|
bool ServeResource(WebDebugConnection * connection, const char * resource_path)
|
|
{
|
|
for (size_t i = 0; i < gStaticResources.size(); ++i)
|
|
{
|
|
const StaticResource & resource = gStaticResources[i];
|
|
|
|
if (strcmp(resource_path, resource.Resource.c_str()) == 0)
|
|
{
|
|
ServeFile(connection, resource.FullPath.c_str());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int WebDebugDispatch(struct WebbyConnection *connection)
|
|
{
|
|
WebDebugConnection dbg_connection(connection);
|
|
|
|
// Check dynamic handlers.
|
|
for (size_t i = 0; i < gHandlers.size(); ++i)
|
|
{
|
|
const WebDebugHandlerEntry & entry = gHandlers[i];
|
|
if (strcmp(connection->request.uri, entry.Request) == 0)
|
|
{
|
|
entry.Handler(entry.Arg, &dbg_connection);
|
|
|
|
DAEDALUS_ASSERT(dbg_connection.GetState() == WebDebugConnection::kResponded, "Failed to handle the response");
|
|
|
|
// Return success if we handled the connection.
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Check static resources.
|
|
if (ServeResource(&dbg_connection, connection->request.uri))
|
|
return 0;
|
|
|
|
DBGConsole_Msg(0, "404 [R%s]", connection->request.uri);
|
|
Generate404(&dbg_connection, connection->request.uri);
|
|
return 1;
|
|
}
|
|
|
|
static int test_ws_connect(struct WebbyConnection *connection)
|
|
{
|
|
/* Allow websocket upgrades on /wstest */
|
|
if (0 == strcmp(connection->request.uri, "/wstest") && ws_connection_count < MAX_WSCONN)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
|
|
static void test_ws_connected(struct WebbyConnection *connection)
|
|
{
|
|
printf("WebSocket connected\n");
|
|
ws_connections[ws_connection_count++] = connection;
|
|
}
|
|
|
|
static void test_ws_closed(struct WebbyConnection *connection)
|
|
{
|
|
int i;
|
|
printf("WebSocket closed\n");
|
|
|
|
for (i = 0; i < ws_connection_count; i++)
|
|
{
|
|
if (ws_connections[i] == connection)
|
|
{
|
|
int remain = ws_connection_count - i;
|
|
memmove(ws_connections + i, ws_connections + i + 1, remain * sizeof(struct WebbyConnection *));
|
|
--ws_connection_count;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int test_ws_frame(struct WebbyConnection *connection, const struct WebbyWsFrame *frame)
|
|
{
|
|
int i = 0;
|
|
|
|
printf("WebSocket frame incoming\n");
|
|
printf(" Frame OpCode: %d\n", frame->opcode);
|
|
printf(" Final frame?: %s\n", (frame->flags & WEBBY_WSF_FIN) ? "yes" : "no");
|
|
printf(" Masked? : %s\n", (frame->flags & WEBBY_WSF_MASKED) ? "yes" : "no");
|
|
printf(" Data Length : %d\n", (int) frame->payload_length);
|
|
|
|
while (i < frame->payload_length)
|
|
{
|
|
unsigned char buffer[16];
|
|
int remain = frame->payload_length - i;
|
|
size_t read_size = remain > sizeof buffer ? sizeof buffer : (size_t) remain;
|
|
size_t k;
|
|
|
|
printf("%08x ", (int) i);
|
|
|
|
if (0 != WebbyRead(connection, buffer, read_size))
|
|
break;
|
|
|
|
for (k = 0; k < read_size; ++k)
|
|
printf("%02x ", buffer[k]);
|
|
|
|
for (k = read_size; k < 16; ++k)
|
|
printf(" ");
|
|
|
|
printf(" | ");
|
|
|
|
for (k = 0; k < read_size; ++k)
|
|
printf("%c", isprint(buffer[k]) ? buffer[k] : '?');
|
|
|
|
printf("\n");
|
|
|
|
i += read_size;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u32 DAEDALUS_THREAD_CALL_TYPE WebDebugThread(void * arg)
|
|
{
|
|
WebbyServer * server = static_cast<WebbyServer*>(arg);
|
|
int frame_counter = 0;
|
|
|
|
while (gKeepRunning)
|
|
{
|
|
WebbyServerUpdate(server);
|
|
|
|
/* Push some test data over websockets */
|
|
// if (0 == (frame_counter & 0x7f))
|
|
// {
|
|
// for (int i = 0; i < ws_connection_count; ++i)
|
|
// {
|
|
// WebbyBeginSocketFrame(ws_connections[i], WEBBY_WS_OP_TEXT_FRAME);
|
|
// WebbyPrintf(ws_connections[i], "Hello world over websockets!\n");
|
|
// WebbyEndSocketFrame(ws_connections[i]);
|
|
// }
|
|
// }
|
|
|
|
ThreadSleepMs(10);
|
|
|
|
++frame_counter;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void AddStaticContent(const char * dir, const char * root)
|
|
{
|
|
IO::FindHandleT find_handle;
|
|
IO::FindDataT find_data;
|
|
|
|
if (IO::FindFileOpen(dir, &find_handle, find_data))
|
|
{
|
|
do
|
|
{
|
|
IO::Filename full_path;
|
|
IO::Path::Combine(full_path, dir, find_data.Name);
|
|
|
|
std::string resource_path = root;
|
|
resource_path += '/';
|
|
resource_path += find_data.Name;
|
|
|
|
if (std::filesystem::is_directory(full_path))
|
|
{
|
|
AddStaticContent(full_path, resource_path.c_str());
|
|
}
|
|
else if (std::filesystem::exists(full_path))
|
|
{
|
|
StaticResource resource;
|
|
resource.Resource = resource_path;
|
|
resource.FullPath = full_path;
|
|
|
|
DBGConsole_Msg(0, " adding [M%s] -> [C%s]",
|
|
resource.Resource.c_str(), resource.FullPath.c_str());
|
|
|
|
gStaticResources.push_back(resource);
|
|
}
|
|
}
|
|
while (IO::FindFileNext(find_handle, find_data));
|
|
|
|
IO::FindFileClose(find_handle);
|
|
}
|
|
}
|
|
|
|
bool WebDebug_Init()
|
|
{
|
|
#if defined(DAEDALUS_W32)
|
|
{
|
|
WORD wsa_version = MAKEWORD(2, 2);
|
|
WSADATA wsa_data;
|
|
if (0 != WSAStartup( wsa_version, &wsa_data ))
|
|
{
|
|
fprintf(stderr, "WSAStartup failed\n");
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
struct WebbyServerConfig config;
|
|
memset(&config, 0, sizeof config);
|
|
config.bind_address = "127.0.0.1";
|
|
config.listening_port = 8081;
|
|
config.flags = WEBBY_SERVER_WEBSOCKETS;
|
|
config.connection_max = 16; // Chrome and Firefox open lots of connections simultaneously.
|
|
config.request_buffer_size = 2048;
|
|
config.io_buffer_size = 8192;
|
|
config.dispatch = &WebDebugDispatch;
|
|
config.log = &test_log;
|
|
config.ws_connect = &test_ws_connect;
|
|
config.ws_connected = &test_ws_connected;
|
|
config.ws_closed = &test_ws_closed;
|
|
config.ws_frame = &test_ws_frame;
|
|
|
|
if (0)
|
|
config.flags = WEBBY_SERVER_LOG_DEBUG;
|
|
|
|
int memory_size = WebbyServerMemoryNeeded(&config);
|
|
gServerMemory = malloc(memory_size);
|
|
gServer = WebbyServerInit(&config, gServerMemory, memory_size);
|
|
|
|
if (!gServer)
|
|
{
|
|
fprintf(stderr, "failed to init server\n");
|
|
return false;
|
|
}
|
|
|
|
IO::Filename data_path;
|
|
std::filesystem::path gDaedalusExePath = std::filesystem::current_path();
|
|
IO::Path::Combine(data_path, gDaedalusExePath.c_str(), "Web");
|
|
DBGConsole_Msg(0, "Looking for static resource in [C%s]", data_path);
|
|
AddStaticContent(data_path, "");
|
|
|
|
gKeepRunning = true;
|
|
gThread = CreateThread( "WebDebug", &WebDebugThread, gServer );
|
|
|
|
return true;
|
|
}
|
|
|
|
void WebDebug_Fini()
|
|
{
|
|
if (gThread != kInvalidThreadHandle)
|
|
{
|
|
gKeepRunning = false;
|
|
JoinThread(gThread, -1);
|
|
gThread = kInvalidThreadHandle;
|
|
}
|
|
|
|
WebbyServerShutdown(gServer);
|
|
free(gServerMemory);
|
|
|
|
#if defined(DAEDALUS_W32)
|
|
WSACleanup();
|
|
#endif
|
|
}
|
|
#endif //DAEDALUS_DEBUG_DISPLAYLIST
|