Add archive helpers and harden task runtime
This commit is contained in:
+348
-162
@@ -44,10 +44,42 @@
|
||||
#include <netinet/in.h> // sockaddr_in, INADDR_*
|
||||
#include <sys/select.h> // select, fd_set, FD_*, timeval
|
||||
#include <sys/socket.h> // socket, bind, accept, listen, sockaddr, AF_*, SOCK_*
|
||||
#include <sys/stat.h> // mkdir
|
||||
#include <sys/un.h> // sockaddr_un
|
||||
|
||||
#include "../fastcgi_devkit/fastcgi.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct TransportLimits {
|
||||
u64 max_client_connections = 256;
|
||||
u64 max_http_header_bytes = 16 * 1024;
|
||||
u64 max_http_body_bytes = 1024 * 1024;
|
||||
u64 max_websocket_frame_bytes = 1024 * 1024;
|
||||
u64 max_websocket_message_bytes = 1024 * 1024;
|
||||
u64 max_websocket_output_bytes = 4 * 1024 * 1024;
|
||||
f64 http_request_timeout_seconds = 15.0;
|
||||
f64 connection_idle_timeout_seconds = 120.0;
|
||||
|
||||
u64 max_http_buffer_bytes() const
|
||||
{
|
||||
return(max_http_header_bytes + max_http_body_bytes);
|
||||
}
|
||||
|
||||
u64 max_websocket_buffer_bytes() const
|
||||
{
|
||||
return(max_websocket_message_bytes + 16 * 1024);
|
||||
}
|
||||
};
|
||||
|
||||
const TransportLimits& transport_limits()
|
||||
{
|
||||
static const TransportLimits limits;
|
||||
return(limits);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static String
|
||||
make_http_text_response(String status_line, String body, String extra_headers = "")
|
||||
{
|
||||
@@ -75,6 +107,27 @@ is_valid_close_code(u16 status_code)
|
||||
return(false);
|
||||
}
|
||||
|
||||
static void
|
||||
ensure_parent_directories(const std::string& path)
|
||||
{
|
||||
std::string::size_type slash = path.rfind('/');
|
||||
if(slash == std::string::npos || slash == 0)
|
||||
return;
|
||||
|
||||
std::string current;
|
||||
std::string directory = path.substr(0, slash);
|
||||
for(std::string::size_type i = 1; i <= directory.length(); ++i)
|
||||
{
|
||||
if(i != directory.length() && directory[i] != '/')
|
||||
continue;
|
||||
current = directory.substr(0, i);
|
||||
if(current == "")
|
||||
continue;
|
||||
if(::mkdir(current.c_str(), 0775) == -1 && errno != EEXIST)
|
||||
throw std::runtime_error("mkdir() failed for Unix socket directory");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
set_socket_nonblocking(int socket_handle)
|
||||
{
|
||||
@@ -132,6 +185,15 @@ FastCGIServer::listen_http(unsigned tcp_port)
|
||||
return server_socket;
|
||||
}
|
||||
|
||||
int
|
||||
FastCGIServer::listen_cli(const std::string& local_path)
|
||||
{
|
||||
int server_socket = listen(local_path);
|
||||
server_socket_types[server_socket] = 'C';
|
||||
printf("(P) CLI command socket ready at %s\n", local_path.c_str());
|
||||
return server_socket;
|
||||
}
|
||||
|
||||
int
|
||||
FastCGIServer::listen(unsigned tcp_port)
|
||||
{
|
||||
@@ -188,6 +250,7 @@ FastCGIServer::listen(const std::string& local_path)
|
||||
|
||||
std::memcpy(sa.sun_path, local_path.data(), size);
|
||||
|
||||
ensure_parent_directories(local_path);
|
||||
file_unlink(local_path);
|
||||
try {
|
||||
if (bind(server_socket, (struct sockaddr*)&sa,
|
||||
@@ -250,9 +313,95 @@ FastCGIServer::close_http_listeners()
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
FastCGIServer::is_http_like_type(char type)
|
||||
{
|
||||
return(type == 'H' || type == 'C');
|
||||
}
|
||||
|
||||
FastCGIServer::Connection*
|
||||
FastCGIServer::open_client_connection(int server_socket, int client_socket)
|
||||
{
|
||||
set_socket_nonblocking(client_socket);
|
||||
printf("Opening socket %i\n", client_socket);
|
||||
Connection* connection = new Connection();
|
||||
connection->client_socket = client_socket;
|
||||
connection->server_socket = server_socket;
|
||||
connection->type = server_socket_types[server_socket];
|
||||
connection->opened_at = time_precise();
|
||||
connection->last_activity_at = connection->opened_at;
|
||||
client_sockets[client_socket] = connection;
|
||||
|
||||
if(is_http_like_type(connection->type))
|
||||
{
|
||||
FastCGIRequest* new_request = new FastCGIRequest();
|
||||
new_request->resources.client_socket = client_socket;
|
||||
new_request->resources.server_socket = server_socket;
|
||||
new_request->stats.time_init = connection->opened_at;
|
||||
connection->requests[client_socket] = new_request;
|
||||
}
|
||||
|
||||
return(connection);
|
||||
}
|
||||
|
||||
bool
|
||||
FastCGIServer::reject_http_connection(Connection& connection, String status_line, String body, String extra_headers)
|
||||
{
|
||||
connection.output_buffer += make_http_text_response(status_line, body, extra_headers);
|
||||
connection.close_socket = true;
|
||||
return(false);
|
||||
}
|
||||
|
||||
void
|
||||
FastCGIServer::enforce_connection_timeouts(Connection& connection)
|
||||
{
|
||||
if(connection.close_socket)
|
||||
return;
|
||||
|
||||
const TransportLimits& limits = transport_limits();
|
||||
f64 now = time_precise();
|
||||
if(is_http_like_type(connection.type) && !connection.is_websocket && !connection.requests.empty())
|
||||
{
|
||||
FastCGIRequest* pending_request = connection.requests.begin()->second;
|
||||
if(!pending_request->flags.input_closed && now - connection.opened_at > limits.http_request_timeout_seconds)
|
||||
{
|
||||
reject_http_connection(connection, "HTTP/1.1 408 Request Timeout", "request timed out\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(now - connection.last_activity_at > limits.connection_idle_timeout_seconds)
|
||||
connection.close_socket = true;
|
||||
}
|
||||
|
||||
bool
|
||||
FastCGIServer::queue_websocket_frame(Connection& connection, String frame)
|
||||
{
|
||||
if(connection.output_buffer.length() + frame.length() > transport_limits().max_websocket_output_bytes)
|
||||
{
|
||||
fail_websocket_connection(connection, 1013, "websocket output queue is full");
|
||||
return(false);
|
||||
}
|
||||
connection.output_buffer += frame;
|
||||
return(true);
|
||||
}
|
||||
|
||||
bool
|
||||
FastCGIServer::queue_websocket_payload(Connection& connection, String message, bool binary)
|
||||
{
|
||||
if(message.length() > transport_limits().max_websocket_message_bytes)
|
||||
return(false);
|
||||
return(queue_websocket_frame(connection, ws_encode_frame(message, binary ? 0x2 : 0x1)));
|
||||
}
|
||||
|
||||
void
|
||||
FastCGIServer::close_websocket_connection(Connection& connection, u16 status_code, String reason)
|
||||
{
|
||||
if(!is_valid_close_code(status_code))
|
||||
status_code = 1002;
|
||||
if(reason.length() > 123)
|
||||
reason = reason.substr(0, 123);
|
||||
if(!ws_is_valid_utf8(reason))
|
||||
reason = "";
|
||||
if(!connection.close_socket)
|
||||
connection.output_buffer += ws_close_frame(status_code, reason);
|
||||
connection.close_socket = true;
|
||||
@@ -261,6 +410,12 @@ FastCGIServer::close_websocket_connection(Connection& connection, u16 status_cod
|
||||
void
|
||||
FastCGIServer::fail_websocket_connection(Connection& connection, u16 status_code, String reason)
|
||||
{
|
||||
// Drop stale application frames on failure so slow peers cannot keep a large
|
||||
// queued buffer alive. Preserve an unsent 101 response, though: if the client
|
||||
// pipelined a bad WebSocket frame immediately after the HTTP upgrade, the
|
||||
// close frame must still follow the accepted upgrade response.
|
||||
if(!str_starts_with(connection.output_buffer, "HTTP/1.1 101 Switching Protocols\r\n"))
|
||||
connection.output_buffer = "";
|
||||
close_websocket_connection(connection, status_code, reason);
|
||||
}
|
||||
|
||||
@@ -339,20 +494,14 @@ FastCGIServer::process(int timeout_ms)
|
||||
throw std::runtime_error("accept() failed");
|
||||
}
|
||||
|
||||
set_socket_nonblocking(client_socket);
|
||||
printf("Opening socket %i\n", client_socket);
|
||||
client_sockets[client_socket] = new Connection();
|
||||
client_sockets[client_socket]->client_socket = client_socket;
|
||||
client_sockets[client_socket]->server_socket = socket_handle;
|
||||
client_sockets[client_socket]->type = server_socket_types[socket_handle];
|
||||
if(client_sockets[client_socket]->type == 'H')
|
||||
if(client_sockets.size() >= transport_limits().max_client_connections)
|
||||
{
|
||||
FastCGIRequest* new_request = new FastCGIRequest();
|
||||
new_request->resources.client_socket = client_socket;
|
||||
new_request->resources.server_socket = socket_handle;
|
||||
new_request->stats.time_init = time_precise();
|
||||
client_sockets[client_socket]->requests[client_socket] = new_request;
|
||||
printf("(!) rejecting socket %i: too many clients\n", client_socket);
|
||||
close(client_socket);
|
||||
continue;
|
||||
}
|
||||
|
||||
open_client_connection(socket_handle, client_socket);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +510,7 @@ FastCGIServer::process(int timeout_ms)
|
||||
{
|
||||
int read_socket = it->first;
|
||||
Connection* connection = it->second;
|
||||
enforce_connection_timeouts(*connection);
|
||||
|
||||
if(FD_ISSET(read_socket, &fs_read))
|
||||
{
|
||||
@@ -378,15 +528,12 @@ FastCGIServer::process(int timeout_ms)
|
||||
{
|
||||
connection->close_socket = true;
|
||||
}
|
||||
else if(connection->type == 'H' && connection->input_buffer != "")
|
||||
else if(is_http_like_type(connection->type) && connection->input_buffer != "")
|
||||
{
|
||||
process_http_request(
|
||||
*client_sockets[connection->client_socket]->requests[connection->client_socket],
|
||||
connection->input_buffer
|
||||
);
|
||||
if(connection->close_socket || !connection->output_buffer.empty())
|
||||
process_http_like_connection_input(*connection);
|
||||
if(!connection->is_websocket && (connection->close_socket || !connection->output_buffer.empty()))
|
||||
connection->input_buffer = "";
|
||||
else
|
||||
else if(!connection->is_websocket)
|
||||
connection->close_socket = true;
|
||||
}
|
||||
else
|
||||
@@ -396,22 +543,16 @@ FastCGIServer::process(int timeout_ms)
|
||||
}
|
||||
else
|
||||
{
|
||||
connection->last_activity_at = time_precise();
|
||||
connection->input_buffer.append(buffer, read_result);
|
||||
if(connection->type == 'H')
|
||||
if(is_http_like_type(connection->type))
|
||||
{
|
||||
if(connection->is_websocket)
|
||||
{
|
||||
process_websocket_input(*connection);
|
||||
}
|
||||
if(!connection->is_websocket && connection->input_buffer.length() > transport_limits().max_http_buffer_bytes())
|
||||
reject_http_connection(*connection, "HTTP/1.1 413 Payload Too Large", "request is too large\n");
|
||||
else
|
||||
{
|
||||
process_http_request(
|
||||
*client_sockets[connection->client_socket]->requests[connection->client_socket],
|
||||
connection->input_buffer
|
||||
);
|
||||
if(connection->close_socket || !connection->output_buffer.empty())
|
||||
connection->input_buffer = "";
|
||||
}
|
||||
process_http_like_connection_input(*connection);
|
||||
if(!connection->is_websocket && (connection->close_socket || !connection->output_buffer.empty()))
|
||||
connection->input_buffer = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -420,6 +561,9 @@ FastCGIServer::process(int timeout_ms)
|
||||
}
|
||||
}
|
||||
|
||||
if(connection->is_websocket && connection->output_buffer.length() > transport_limits().max_websocket_output_bytes)
|
||||
fail_websocket_connection(*connection, 1013, "websocket output queue is full");
|
||||
|
||||
if(!connection->output_buffer.empty() && FD_ISSET(read_socket, &fs_write))
|
||||
{
|
||||
if(connection->type == 'F')
|
||||
@@ -428,6 +572,9 @@ FastCGIServer::process(int timeout_ms)
|
||||
goto close_socket;
|
||||
}
|
||||
|
||||
if(connection->is_websocket && connection->output_buffer.empty() && !connection->input_buffer.empty() && !connection->close_socket)
|
||||
process_websocket_input(*connection);
|
||||
|
||||
if(connection->close_socket && connection->output_buffer.empty())
|
||||
{
|
||||
close_socket:
|
||||
@@ -452,141 +599,160 @@ FastCGIServer::process(int timeout_ms)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
FastCGIServer::process_http_request(FastCGIRequest& request, String& data)
|
||||
bool
|
||||
FastCGIServer::parse_http_message(FastCGIRequest& request, String& data)
|
||||
{
|
||||
Connection* connection = client_sockets[request.resources.client_socket];
|
||||
const TransportLimits& limits = transport_limits();
|
||||
auto header_end = data.find("\r\n\r\n");
|
||||
if(header_end == String::npos)
|
||||
return;
|
||||
{
|
||||
if(data.length() > limits.max_http_header_bytes)
|
||||
reject_http_connection(*connection, "HTTP/1.1 431 Request Header Fields Too Large", "request headers are too large\n");
|
||||
return(false);
|
||||
}
|
||||
|
||||
if(header_end > limits.max_http_header_bytes)
|
||||
return(reject_http_connection(*connection, "HTTP/1.1 431 Request Header Fields Too Large", "request headers are too large\n"));
|
||||
|
||||
if(request.params.size() == 0)
|
||||
{
|
||||
request.params = split_http_headers(data.substr(0, header_end));
|
||||
request.flags.params_closed = true;
|
||||
if(request.params["HTTP_SCRIPT_FILENAME"] != "")
|
||||
request.params["SCRIPT_FILENAME"] = request.params["HTTP_SCRIPT_FILENAME"];
|
||||
else if(request.params["SCRIPT_FILENAME"] == "" && request.params["DOCUMENT_URI"] != "")
|
||||
{
|
||||
String document_root = first(request.params["DOCUMENT_ROOT"], cwd_get());
|
||||
if(document_root.length() > 1 && document_root[document_root.length()-1] == '/')
|
||||
document_root.resize(document_root.length()-1);
|
||||
request.params["DOCUMENT_ROOT"] = document_root;
|
||||
request.params["SCRIPT_FILENAME"] = document_root + request.params["DOCUMENT_URI"];
|
||||
}
|
||||
}
|
||||
|
||||
u64 content_length = int_val(first(request.params["CONTENT_LENGTH"], "0"));
|
||||
if(content_length > limits.max_http_body_bytes)
|
||||
return(reject_http_connection(*connection, "HTTP/1.1 413 Payload Too Large", "request body is too large\n"));
|
||||
|
||||
u64 request_size = header_end + 4 + content_length;
|
||||
if(request_size > limits.max_http_buffer_bytes())
|
||||
return(reject_http_connection(*connection, "HTTP/1.1 413 Payload Too Large", "request is too large\n"));
|
||||
if(data.length() < request_size)
|
||||
return;
|
||||
return(false);
|
||||
|
||||
request.in = data.substr(header_end + 4, content_length);
|
||||
request.flags.input_closed = true;
|
||||
data.erase(0, request_size);
|
||||
return(true);
|
||||
}
|
||||
|
||||
void
|
||||
FastCGIServer::process_http_like_connection_input(Connection& connection)
|
||||
{
|
||||
if(connection.type == 'H' && connection.is_websocket)
|
||||
{
|
||||
process_websocket_input(connection);
|
||||
return;
|
||||
}
|
||||
|
||||
FastCGIRequest& request = *connection.requests[connection.client_socket];
|
||||
if(connection.type == 'C')
|
||||
process_cli_request(request, connection.input_buffer);
|
||||
else
|
||||
process_http_request(request, connection.input_buffer);
|
||||
}
|
||||
|
||||
void
|
||||
FastCGIServer::process_cli_request(FastCGIRequest& request, String& data)
|
||||
{
|
||||
if(!parse_http_message(request, data))
|
||||
return;
|
||||
|
||||
Connection* connection = client_sockets[request.resources.client_socket];
|
||||
if(!on_cli_complete)
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 500 Internal Server Error",
|
||||
"UCE CLI dispatcher is not configured\n"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
on_cli_complete(request);
|
||||
assemble_output_buffer(request, connection);
|
||||
}
|
||||
connection->close_socket = true;
|
||||
}
|
||||
|
||||
bool
|
||||
FastCGIServer::validate_websocket_upgrade(FastCGIRequest& request, Connection& connection)
|
||||
{
|
||||
String request_method = trim(request.params["REQUEST_METHOD"]);
|
||||
String request_protocol = trim(request.params["SERVER_PROTOCOL"]);
|
||||
String websocket_connection = to_lower(request.params["HTTP_CONNECTION"]);
|
||||
String websocket_host = trim(request.params["HTTP_HOST"]);
|
||||
String websocket_key = trim(request.params["HTTP_SEC_WEBSOCKET_KEY"]);
|
||||
String websocket_version = trim(request.params["HTTP_SEC_WEBSOCKET_VERSION"]);
|
||||
|
||||
if(request_method != "GET")
|
||||
return(reject_http_connection(connection, "HTTP/1.1 405 Method Not Allowed", "websocket upgrades require GET"));
|
||||
if(request_protocol != "HTTP/1.1")
|
||||
return(reject_http_connection(connection, "HTTP/1.1 400 Bad Request", "websocket upgrades require HTTP/1.1"));
|
||||
if(websocket_host == "")
|
||||
return(reject_http_connection(connection, "HTTP/1.1 400 Bad Request", "missing Host header"));
|
||||
if(websocket_connection.find("upgrade") == String::npos)
|
||||
return(reject_http_connection(connection, "HTTP/1.1 400 Bad Request", "missing Connection: Upgrade header"));
|
||||
if(websocket_key == "")
|
||||
return(reject_http_connection(connection, "HTTP/1.1 400 Bad Request", "missing Sec-WebSocket-Key header"));
|
||||
if(!ws_is_valid_client_key(websocket_key))
|
||||
return(reject_http_connection(connection, "HTTP/1.1 400 Bad Request", "invalid Sec-WebSocket-Key header"));
|
||||
if(websocket_version == "")
|
||||
return(reject_http_connection(connection, "HTTP/1.1 400 Bad Request", "missing Sec-WebSocket-Version header"));
|
||||
if(websocket_version != "13")
|
||||
return(reject_http_connection(connection, "HTTP/1.1 426 Upgrade Required", "unsupported websocket version", "Sec-WebSocket-Version: 13\r\n"));
|
||||
|
||||
return(true);
|
||||
}
|
||||
|
||||
void
|
||||
FastCGIServer::begin_websocket_upgrade(FastCGIRequest& request, Connection& connection, String& data)
|
||||
{
|
||||
connection.is_websocket = true;
|
||||
connection.websocket_connection_id = std::to_string(getpid()) + ":" + std::to_string(connection.client_socket);
|
||||
connection.websocket_scope = first(
|
||||
request.params["SCRIPT_FILENAME"],
|
||||
request.params["DOCUMENT_URI"],
|
||||
request.params["REQUEST_URI"]
|
||||
);
|
||||
request.resources.is_websocket = true;
|
||||
request.resources.websocket_connection_id = connection.websocket_connection_id;
|
||||
request.resources.websocket_scope = connection.websocket_scope;
|
||||
request.resources.websocket_connection_state = &connection.websocket_state;
|
||||
request.connection.set_reference(&connection.websocket_state);
|
||||
|
||||
connection.output_buffer +=
|
||||
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Accept: " + ws_make_accept_key(trim(request.params["HTTP_SEC_WEBSOCKET_KEY"])) + "\r\n\r\n";
|
||||
|
||||
if(!data.empty())
|
||||
process_websocket_input(connection);
|
||||
}
|
||||
|
||||
void
|
||||
FastCGIServer::process_http_request(FastCGIRequest& request, String& data)
|
||||
{
|
||||
if(!parse_http_message(request, data))
|
||||
return;
|
||||
|
||||
if(request.params["HTTP_SCRIPT_FILENAME"] != "")
|
||||
request.params["SCRIPT_FILENAME"] = request.params["HTTP_SCRIPT_FILENAME"];
|
||||
else if(request.params["SCRIPT_FILENAME"] == "" && request.params["DOCUMENT_URI"] != "")
|
||||
{
|
||||
String document_root = first(request.params["DOCUMENT_ROOT"], cwd_get());
|
||||
if(document_root.length() > 1 && document_root[document_root.length()-1] == '/')
|
||||
document_root.resize(document_root.length()-1);
|
||||
request.params["DOCUMENT_ROOT"] = document_root;
|
||||
request.params["SCRIPT_FILENAME"] = document_root + request.params["DOCUMENT_URI"];
|
||||
}
|
||||
|
||||
if(to_lower(request.params["HTTP_UPGRADE"]) == "websocket")
|
||||
{
|
||||
Connection* connection = client_sockets[request.resources.client_socket];
|
||||
String request_method = trim(request.params["REQUEST_METHOD"]);
|
||||
String request_protocol = trim(request.params["SERVER_PROTOCOL"]);
|
||||
String websocket_connection = to_lower(request.params["HTTP_CONNECTION"]);
|
||||
String websocket_host = trim(request.params["HTTP_HOST"]);
|
||||
String websocket_key = trim(request.params["HTTP_SEC_WEBSOCKET_KEY"]);
|
||||
String websocket_version = trim(request.params["HTTP_SEC_WEBSOCKET_VERSION"]);
|
||||
if(request_method != "GET")
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 405 Method Not Allowed",
|
||||
"websocket upgrades require GET"
|
||||
);
|
||||
connection->close_socket = true;
|
||||
return;
|
||||
}
|
||||
if(request_protocol != "HTTP/1.1")
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"websocket upgrades require HTTP/1.1"
|
||||
);
|
||||
connection->close_socket = true;
|
||||
return;
|
||||
}
|
||||
if(websocket_host == "")
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"missing Host header"
|
||||
);
|
||||
connection->close_socket = true;
|
||||
return;
|
||||
}
|
||||
if(websocket_connection.find("upgrade") == String::npos)
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"missing Connection: Upgrade header"
|
||||
);
|
||||
connection->close_socket = true;
|
||||
return;
|
||||
}
|
||||
if(websocket_key == "")
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"missing Sec-WebSocket-Key header"
|
||||
);
|
||||
connection->close_socket = true;
|
||||
return;
|
||||
}
|
||||
if(!ws_is_valid_client_key(websocket_key))
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"invalid Sec-WebSocket-Key header"
|
||||
);
|
||||
connection->close_socket = true;
|
||||
return;
|
||||
}
|
||||
if(websocket_version == "")
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"missing Sec-WebSocket-Version header"
|
||||
);
|
||||
connection->close_socket = true;
|
||||
return;
|
||||
}
|
||||
if(websocket_version != "" && websocket_version != "13")
|
||||
{
|
||||
connection->output_buffer += make_http_text_response(
|
||||
"HTTP/1.1 426 Upgrade Required",
|
||||
"unsupported websocket version",
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
);
|
||||
connection->close_socket = true;
|
||||
return;
|
||||
}
|
||||
|
||||
connection->is_websocket = true;
|
||||
connection->websocket_connection_id = std::to_string(getpid()) + ":" + std::to_string(connection->client_socket);
|
||||
connection->websocket_scope = first(
|
||||
request.params["SCRIPT_FILENAME"],
|
||||
request.params["DOCUMENT_URI"],
|
||||
request.params["REQUEST_URI"]
|
||||
);
|
||||
request.resources.is_websocket = true;
|
||||
request.resources.websocket_connection_id = connection->websocket_connection_id;
|
||||
request.resources.websocket_scope = connection->websocket_scope;
|
||||
request.resources.websocket_connection_state = &connection->websocket_state;
|
||||
request.connection.set_reference(&connection->websocket_state);
|
||||
|
||||
connection->output_buffer +=
|
||||
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Accept: " + ws_make_accept_key(websocket_key) + "\r\n\r\n";
|
||||
|
||||
if(!data.empty())
|
||||
process_websocket_input(*connection);
|
||||
if(validate_websocket_upgrade(request, *connection))
|
||||
begin_websocket_upgrade(request, *connection, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -606,6 +772,13 @@ FastCGIServer::process_http_request(FastCGIRequest& request, String& data)
|
||||
void
|
||||
FastCGIServer::process_websocket_input(Connection& connection)
|
||||
{
|
||||
const TransportLimits& limits = transport_limits();
|
||||
if(connection.input_buffer.length() > limits.max_websocket_buffer_bytes())
|
||||
{
|
||||
fail_websocket_connection(connection, 1009, "websocket input buffer is too large");
|
||||
return;
|
||||
}
|
||||
|
||||
while(!connection.input_buffer.empty())
|
||||
{
|
||||
WSFrame frame;
|
||||
@@ -613,10 +786,13 @@ FastCGIServer::process_websocket_input(Connection& connection)
|
||||
if(!frame.parse(connection.input_buffer, error))
|
||||
{
|
||||
if(error != "")
|
||||
{
|
||||
connection.output_buffer += ws_close_frame(1002, error);
|
||||
connection.close_socket = true;
|
||||
}
|
||||
fail_websocket_connection(connection, 1002, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(frame.payload_length > limits.max_websocket_frame_bytes)
|
||||
{
|
||||
fail_websocket_connection(connection, 1009, "websocket frame is too large");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -645,6 +821,11 @@ FastCGIServer::process_websocket_input(Connection& connection)
|
||||
return;
|
||||
}
|
||||
|
||||
if(connection.websocket_fragment_buffer.length() + frame.payload.length() > limits.max_websocket_message_bytes)
|
||||
{
|
||||
fail_websocket_connection(connection, 1009, "websocket message is too large");
|
||||
return;
|
||||
}
|
||||
connection.websocket_fragment_buffer += frame.payload;
|
||||
if(!frame.is_final_fragment)
|
||||
break;
|
||||
@@ -672,6 +853,12 @@ FastCGIServer::process_websocket_input(Connection& connection)
|
||||
return;
|
||||
}
|
||||
|
||||
if(frame.payload.length() > limits.max_websocket_message_bytes)
|
||||
{
|
||||
fail_websocket_connection(connection, 1009, "websocket message is too large");
|
||||
return;
|
||||
}
|
||||
|
||||
if(frame.is_final_fragment)
|
||||
{
|
||||
if(frame.opcode == 0x1 && !ws_is_valid_utf8(frame.payload))
|
||||
@@ -720,7 +907,8 @@ FastCGIServer::process_websocket_input(Connection& connection)
|
||||
return;
|
||||
}
|
||||
case 0x9:
|
||||
connection.output_buffer += ws_encode_frame(frame.payload, 0xA);
|
||||
if(!queue_websocket_frame(connection, ws_encode_frame(frame.payload, 0xA)))
|
||||
return;
|
||||
break;
|
||||
case 0xA:
|
||||
break;
|
||||
@@ -734,15 +922,11 @@ FastCGIServer::process_websocket_input(Connection& connection)
|
||||
bool
|
||||
FastCGIServer::websocket_send_to(String connection_id, String message, bool binary)
|
||||
{
|
||||
u8 opcode = binary ? 0x2 : 0x1;
|
||||
for(auto& item : client_sockets)
|
||||
{
|
||||
Connection* connection = item.second;
|
||||
if(connection->is_websocket && connection->websocket_connection_id == connection_id)
|
||||
{
|
||||
connection->output_buffer += ws_encode_frame(message, opcode);
|
||||
return(true);
|
||||
}
|
||||
return(queue_websocket_payload(*connection, message, binary));
|
||||
}
|
||||
return(false);
|
||||
}
|
||||
@@ -751,7 +935,10 @@ u64
|
||||
FastCGIServer::websocket_broadcast(String scope, String message, bool binary)
|
||||
{
|
||||
u64 sent = 0;
|
||||
u8 opcode = binary ? 0x2 : 0x1;
|
||||
if(message.length() > transport_limits().max_websocket_message_bytes)
|
||||
return(sent);
|
||||
|
||||
String frame = ws_encode_frame(message, binary ? 0x2 : 0x1);
|
||||
for(auto& item : client_sockets)
|
||||
{
|
||||
Connection* connection = item.second;
|
||||
@@ -759,8 +946,8 @@ FastCGIServer::websocket_broadcast(String scope, String message, bool binary)
|
||||
continue;
|
||||
if(scope != "" && connection->websocket_scope != scope)
|
||||
continue;
|
||||
connection->output_buffer += ws_encode_frame(message, opcode);
|
||||
sent += 1;
|
||||
if(queue_websocket_frame(*connection, frame))
|
||||
sent += 1;
|
||||
}
|
||||
return(sent);
|
||||
}
|
||||
@@ -789,8 +976,7 @@ FastCGIServer::websocket_close(String connection_id, u16 status_code, String rea
|
||||
Connection* connection = item.second;
|
||||
if(connection->is_websocket && connection->websocket_connection_id == connection_id)
|
||||
{
|
||||
connection->output_buffer += ws_close_frame(status_code, reason);
|
||||
connection->close_socket = true;
|
||||
close_websocket_connection(*connection, status_code, reason);
|
||||
return(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,12 @@ public:
|
||||
std::function<int(FastCGIRequest&)> on_request = 0;
|
||||
std::function<int(FastCGIRequest&)> on_data = 0;
|
||||
std::function<int(FastCGIRequest&)> on_complete = 0;
|
||||
std::function<int(FastCGIRequest&)> on_cli_complete = 0;
|
||||
std::function<int(FastCGIRequest&, const String&, u8)> on_websocket_message = 0;
|
||||
|
||||
int listen(unsigned tcp_port);
|
||||
int listen_http(unsigned tcp_port);
|
||||
int listen_cli(const std::string& local_path);
|
||||
int listen(const std::string& local_path);
|
||||
|
||||
void process(int timeout_ms = -1); // timeout_ms<0 blocks forever
|
||||
@@ -75,7 +77,9 @@ public:
|
||||
DTree websocket_state;
|
||||
String websocket_fragment_buffer;
|
||||
u8 websocket_fragment_opcode = 0;
|
||||
char type = 'F'; // F = FastCGI, H = HttpServer
|
||||
f64 opened_at = 0;
|
||||
f64 last_activity_at = 0;
|
||||
char type = 'F'; // F = FastCGI, H = HttpServer/WebSocket, C = CLI-over-HTTP Unix socket
|
||||
};
|
||||
|
||||
typedef StringMap Pairs;
|
||||
@@ -88,7 +92,15 @@ public:
|
||||
|
||||
void close_http_listeners();
|
||||
void read_fgci(Connection&);
|
||||
static bool is_http_like_type(char type);
|
||||
Connection* open_client_connection(int server_socket, int client_socket);
|
||||
void enforce_connection_timeouts(Connection& connection);
|
||||
bool reject_http_connection(Connection& connection, String status_line, String body, String extra_headers = "");
|
||||
bool queue_websocket_frame(Connection& connection, String frame);
|
||||
bool queue_websocket_payload(Connection& connection, String message, bool binary = false);
|
||||
void process_websocket_input(Connection&);
|
||||
bool validate_websocket_upgrade(FastCGIRequest& request, Connection& connection);
|
||||
void begin_websocket_upgrade(FastCGIRequest& request, Connection& connection, String& data);
|
||||
void fail_websocket_connection(Connection& connection, u16 status_code, String reason = "");
|
||||
void close_websocket_connection(Connection& connection, u16 status_code = 1000, String reason = "");
|
||||
void dispatch_websocket_message(Connection& connection, RequestID request_id, String payload, u8 opcode);
|
||||
@@ -105,7 +117,10 @@ public:
|
||||
const std::string& input, unsigned char type);
|
||||
static void assemble_output_buffer(FastCGIRequest& request, Connection* connection = 0);
|
||||
int send_output_buffer(Connection& con);
|
||||
bool parse_http_message(FastCGIRequest& request, String& data);
|
||||
void process_http_request(FastCGIRequest& request, String& data);
|
||||
void process_cli_request(FastCGIRequest& request, String& data);
|
||||
void process_http_like_connection_input(Connection& connection);
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "cli.h"
|
||||
|
||||
DTree cli_input(Request& context)
|
||||
{
|
||||
DTree input;
|
||||
|
||||
for(auto& item : context.get)
|
||||
input[item.first] = item.second;
|
||||
for(auto& item : context.post)
|
||||
input[item.first] = item.second;
|
||||
|
||||
String content_type_info = context.params["CONTENT_TYPE"];
|
||||
String content_type = to_lower(trim(nibble(content_type_info, ";")));
|
||||
if(content_type == "application/json" || str_ends_with(content_type, "+json"))
|
||||
{
|
||||
String body = trim(context.in);
|
||||
if(body != "")
|
||||
{
|
||||
DTree parsed = json_decode(body);
|
||||
if(parsed.type == 'M')
|
||||
{
|
||||
for(auto& item : parsed._map)
|
||||
input[item.first] = item.second;
|
||||
}
|
||||
else
|
||||
{
|
||||
input["_"] = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return(input);
|
||||
}
|
||||
|
||||
String cli_arg(Request& context, String key, String default_value)
|
||||
{
|
||||
DTree input = cli_input(context);
|
||||
DTree* value = input.key(key);
|
||||
if(!value)
|
||||
return(default_value);
|
||||
String result = value->to_string();
|
||||
if(result == "")
|
||||
return(default_value);
|
||||
return(result);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
DTree cli_input(Request& context);
|
||||
String cli_arg(Request& context, String key, String default_value = "");
|
||||
+92
-28
@@ -13,6 +13,7 @@ const char* UCE_SETUP_SYMBOL = "__uce_set_current_request";
|
||||
const char* UCE_RENDER_SYMBOL = "__uce_render";
|
||||
const char* UCE_COMPONENT_SYMBOL = "__uce_component";
|
||||
const char* UCE_WEBSOCKET_SYMBOL = "__uce_websocket";
|
||||
const char* UCE_CLI_SYMBOL = "__uce_cli";
|
||||
const char* UCE_ONCE_SYMBOL = "__uce_once";
|
||||
const char* UCE_INIT_SYMBOL = "__uce_init";
|
||||
const u64 UCE_UNIT_ABI_VERSION = 1;
|
||||
@@ -690,6 +691,8 @@ void load_shared_unit(Request* context, SharedUnit* su)
|
||||
dlerror();
|
||||
su->on_websocket = (request_ref_handler)dlsym(su->so_handle, UCE_WEBSOCKET_SYMBOL);
|
||||
dlerror();
|
||||
su->on_cli = (request_ref_handler)dlsym(su->so_handle, UCE_CLI_SYMBOL);
|
||||
dlerror();
|
||||
su->on_once = (request_ref_handler)dlsym(su->so_handle, UCE_ONCE_SYMBOL);
|
||||
dlerror();
|
||||
su->on_init = (request_ref_handler)dlsym(su->so_handle, UCE_INIT_SYMBOL);
|
||||
@@ -1191,7 +1194,8 @@ enum class UnitCallMacroKind
|
||||
render,
|
||||
component,
|
||||
once,
|
||||
init
|
||||
init,
|
||||
cli
|
||||
};
|
||||
|
||||
struct UnitCallMacroTarget
|
||||
@@ -1234,6 +1238,7 @@ void compiler_unload_failed_shared_unit(SharedUnit* su)
|
||||
su->on_render = 0;
|
||||
su->on_component = 0;
|
||||
su->on_websocket = 0;
|
||||
su->on_cli = 0;
|
||||
su->on_once = 0;
|
||||
su->on_init = 0;
|
||||
}
|
||||
@@ -1358,6 +1363,11 @@ UnitCallMacroTarget unit_call_macro_target(String function_name)
|
||||
target.kind = UnitCallMacroKind::init;
|
||||
return(target);
|
||||
}
|
||||
if(function_name == "CLI")
|
||||
{
|
||||
target.kind = UnitCallMacroKind::cli;
|
||||
return(target);
|
||||
}
|
||||
|
||||
return(target);
|
||||
}
|
||||
@@ -1465,6 +1475,8 @@ String compiler_missing_request_handler_message(UnitCallMacroKind kind, String h
|
||||
}
|
||||
if(kind == UnitCallMacroKind::once)
|
||||
return("no ONCE() entry point");
|
||||
if(kind == UnitCallMacroKind::cli)
|
||||
return("no CLI() entry point");
|
||||
if(kind == UnitCallMacroKind::init)
|
||||
return("no INIT() entry point");
|
||||
return("request handler not found");
|
||||
@@ -1544,20 +1556,24 @@ request_ref_handler get_component_handler(SharedUnit* su, String render_name)
|
||||
return(handler);
|
||||
}
|
||||
|
||||
bool compiler_invoke_render(Request* context, String file_name, String render_name, String* error_out = 0)
|
||||
bool compiler_invoke_loaded_request_handler(
|
||||
Request* context,
|
||||
SharedUnit* su,
|
||||
request_ref_handler handler,
|
||||
UnitCallMacroKind kind,
|
||||
String handler_name,
|
||||
bool count_request,
|
||||
String runtime_error_status,
|
||||
String* error_out = 0
|
||||
)
|
||||
{
|
||||
auto su = compiler_load_shared_unit(context, file_name, "", false);
|
||||
if(!su)
|
||||
return(false);
|
||||
|
||||
if(!compiler_prepare_request_handler(context, su, error_out, true))
|
||||
return(false);
|
||||
|
||||
auto handler = get_page_render_handler(su, render_name);
|
||||
if(!handler)
|
||||
{
|
||||
if(error_out)
|
||||
*error_out = compiler_missing_request_handler_message(UnitCallMacroKind::render, render_name);
|
||||
*error_out = compiler_missing_request_handler_message(kind, handler_name);
|
||||
return(false);
|
||||
}
|
||||
|
||||
@@ -1565,37 +1581,46 @@ bool compiler_invoke_render(Request* context, String file_name, String render_na
|
||||
context,
|
||||
su,
|
||||
handler,
|
||||
compiler_is_request_entry_unit(context, su),
|
||||
"uncaught exception during render"
|
||||
count_request,
|
||||
runtime_error_status
|
||||
);
|
||||
return(true);
|
||||
}
|
||||
|
||||
bool compiler_invoke_render(Request* context, String file_name, String render_name, String* error_out = 0)
|
||||
{
|
||||
auto su = compiler_load_shared_unit(context, file_name, "", false);
|
||||
if(!su)
|
||||
return(false);
|
||||
|
||||
return(compiler_invoke_loaded_request_handler(
|
||||
context,
|
||||
su,
|
||||
get_page_render_handler(su, render_name),
|
||||
UnitCallMacroKind::render,
|
||||
render_name,
|
||||
compiler_is_request_entry_unit(context, su),
|
||||
"uncaught exception during render",
|
||||
error_out
|
||||
));
|
||||
}
|
||||
|
||||
bool compiler_invoke_component(Request* context, String file_name, String render_name, String* error_out = 0)
|
||||
{
|
||||
auto su = compiler_load_shared_unit(context, file_name, "", false);
|
||||
if(!su)
|
||||
return(false);
|
||||
|
||||
if(!compiler_prepare_request_handler(context, su, error_out, true))
|
||||
return(false);
|
||||
|
||||
auto handler = get_component_handler(su, render_name);
|
||||
if(!handler)
|
||||
{
|
||||
if(error_out)
|
||||
*error_out = compiler_missing_request_handler_message(UnitCallMacroKind::component, render_name);
|
||||
return(false);
|
||||
}
|
||||
|
||||
compiler_execute_request_handler(
|
||||
return(compiler_invoke_loaded_request_handler(
|
||||
context,
|
||||
su,
|
||||
handler,
|
||||
get_component_handler(su, render_name),
|
||||
UnitCallMacroKind::component,
|
||||
render_name,
|
||||
false,
|
||||
"uncaught exception during component render"
|
||||
);
|
||||
return(true);
|
||||
"uncaught exception during component render",
|
||||
error_out
|
||||
));
|
||||
}
|
||||
|
||||
void compiler_invoke(Request* context, String file_name)
|
||||
@@ -1610,6 +1635,33 @@ void compiler_invoke(Request* context, String file_name)
|
||||
}
|
||||
}
|
||||
|
||||
void compiler_invoke_cli(Request* context, String file_name)
|
||||
{
|
||||
auto su = compiler_load_shared_unit(context, file_name, "", false);
|
||||
if(!su)
|
||||
return;
|
||||
|
||||
String error_message = "";
|
||||
if(!compiler_invoke_loaded_request_handler(
|
||||
context,
|
||||
su,
|
||||
su->on_cli,
|
||||
UnitCallMacroKind::cli,
|
||||
"",
|
||||
compiler_is_request_entry_unit(context, su),
|
||||
"uncaught exception during cli handler",
|
||||
&error_message
|
||||
))
|
||||
{
|
||||
if(!su->on_cli)
|
||||
context->set_status(404, "CLI Entry Point Not Found");
|
||||
else
|
||||
context->set_status(500, "CLI Unit Error");
|
||||
if(error_message != "")
|
||||
print(error_message);
|
||||
}
|
||||
}
|
||||
|
||||
void compiler_invoke_websocket(Request* context, String file_name)
|
||||
{
|
||||
auto su = compiler_load_shared_unit(context, file_name, "", false);
|
||||
@@ -1767,19 +1819,31 @@ DTree* unit_call(String file_name, String function_name, DTree* call_param)
|
||||
}
|
||||
else
|
||||
{
|
||||
UnitInvocationScope invoke_scope(context, su);
|
||||
su->on_setup(context);
|
||||
request_ref_handler handler = 0;
|
||||
|
||||
if(macro_target.kind == UnitCallMacroKind::once)
|
||||
handler = su->on_once;
|
||||
else if(macro_target.kind == UnitCallMacroKind::init)
|
||||
handler = su->on_init;
|
||||
else if(macro_target.kind == UnitCallMacroKind::cli)
|
||||
handler = su->on_cli;
|
||||
|
||||
if(!handler)
|
||||
print("Error: unit_call() ", compiler_missing_request_handler_message(macro_target.kind, macro_target.handler_name));
|
||||
else if(macro_target.kind == UnitCallMacroKind::cli)
|
||||
{
|
||||
String prepare_error = "";
|
||||
if(!compiler_prepare_request_handler(context, su, &prepare_error, true))
|
||||
print("Error: unit_call() ", prepare_error);
|
||||
else
|
||||
compiler_execute_request_handler(context, su, handler, false, "uncaught exception during cli handler");
|
||||
}
|
||||
else
|
||||
{
|
||||
UnitInvocationScope invoke_scope(context, su);
|
||||
su->on_setup(context);
|
||||
handler(*context);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#define ONCE(X) extern "C" void __uce_once(X)
|
||||
#define INIT(X) extern "C" void __uce_init(X)
|
||||
#define WS(X) extern "C" void __uce_websocket(X)
|
||||
#define CLI(X) extern "C" void __uce_cli(X)
|
||||
#define EXPORT extern "C"
|
||||
|
||||
String preprocess_shared_unit(Request* context, SharedUnit* su);
|
||||
@@ -14,6 +15,7 @@ void compile_shared_unit(Request* context, SharedUnit* su);
|
||||
SharedUnit* get_shared_unit(Request* context, String file_name, bool opt_so_optional = false);
|
||||
void compiler_invoke(Request* context, String file_name);
|
||||
void compiler_invoke_websocket(Request* context, String file_name);
|
||||
void compiler_invoke_cli(Request* context, String file_name);
|
||||
SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String current_path = "", bool opt_so_optional = false);
|
||||
String compiler_site_directory(Request* context);
|
||||
StringList compiler_scan_site_units(Request* context);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -94,6 +94,10 @@ String html_escape(f64 a);
|
||||
String json_encode(String s, char quote_char = '"');
|
||||
String json_encode(DTree t, char quote_char = '"');
|
||||
DTree json_decode(String s);
|
||||
String xml_encode(DTree t, String root_name = "root");
|
||||
DTree xml_decode(String s);
|
||||
String yaml_encode(DTree t);
|
||||
DTree yaml_decode(String s);
|
||||
|
||||
String var_dump(StringMap map, String prefix = "", String postfix = "\n");
|
||||
String var_dump(StringList slist, String prefix = "", String postfix = "\n");
|
||||
|
||||
+150
-33
@@ -613,89 +613,201 @@ pid_t spawn_subprocess(std::function<void()> exec_after_spawn)
|
||||
}
|
||||
}
|
||||
|
||||
String task_safe_key(String key)
|
||||
{
|
||||
key = trim(key);
|
||||
if(key == "")
|
||||
throw std::runtime_error("task key cannot be empty");
|
||||
return(gen_sha1(key));
|
||||
}
|
||||
|
||||
String task_file_prefix(String key)
|
||||
{
|
||||
return(path_join(context->server->config["BIN_DIRECTORY"], "task-" + task_safe_key(key)));
|
||||
}
|
||||
|
||||
struct TaskStatus {
|
||||
pid_t pid = 0;
|
||||
String process_start_ticks = "";
|
||||
};
|
||||
|
||||
TaskStatus task_status_parse(String status_file)
|
||||
{
|
||||
TaskStatus status;
|
||||
auto lines = split(trim(status_file), "\n");
|
||||
if(lines.size() > 0)
|
||||
status.pid = (pid_t)int_val(trim(lines[0]));
|
||||
if(lines.size() > 1)
|
||||
status.process_start_ticks = trim(lines[1]);
|
||||
return(status);
|
||||
}
|
||||
|
||||
String task_process_start_ticks(pid_t pid)
|
||||
{
|
||||
if(pid <= 0)
|
||||
return("");
|
||||
String stat_file_name = "/proc/" + std::to_string(pid) + "/stat";
|
||||
int fd = open(stat_file_name.c_str(), O_RDONLY);
|
||||
if(fd == -1)
|
||||
return("");
|
||||
char buffer[4096];
|
||||
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
|
||||
close(fd);
|
||||
if(bytes_read <= 0)
|
||||
return("");
|
||||
buffer[bytes_read] = '\0';
|
||||
String stat = buffer;
|
||||
size_t command_end = stat.rfind(") ");
|
||||
if(command_end == String::npos)
|
||||
return("");
|
||||
String after_command = stat.substr(command_end + 2);
|
||||
auto fields = split_space(after_command);
|
||||
if(fields.size() <= 19)
|
||||
return("");
|
||||
return(fields[19]);
|
||||
}
|
||||
|
||||
String task_status_content(pid_t pid)
|
||||
{
|
||||
return(std::to_string(pid) + "\n" + task_process_start_ticks(pid) + "\n");
|
||||
}
|
||||
|
||||
bool task_status_is_alive(TaskStatus status)
|
||||
{
|
||||
if(status.pid <= 0)
|
||||
return(false);
|
||||
if(kill(status.pid, 0) != 0)
|
||||
return(false);
|
||||
if(status.process_start_ticks == "")
|
||||
return(true);
|
||||
return(task_process_start_ticks(status.pid) == status.process_start_ticks);
|
||||
}
|
||||
|
||||
int task_kill(pid_t pid, int sig)
|
||||
{
|
||||
if(pid <= 0)
|
||||
{
|
||||
errno = EINVAL;
|
||||
return(-1);
|
||||
}
|
||||
return(kill(pid, sig));
|
||||
}
|
||||
|
||||
pid_t task_pid(String key)
|
||||
{
|
||||
String status_file_name = context->server->config["BIN_DIRECTORY"] + "/task-" + key;
|
||||
String status_file_name = task_file_prefix(key);
|
||||
String lock_file_name = status_file_name + ".lock";
|
||||
int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644);
|
||||
int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task-pid:" + key);
|
||||
if(lock_fd == -1)
|
||||
{
|
||||
fprintf(stderr, "task_pid(): could not lock task key '%s'\n", key.c_str());
|
||||
return(0);
|
||||
}
|
||||
String status_file = file_get_contents(status_file_name);
|
||||
pid_t p = 0;
|
||||
if(status_file != "")
|
||||
{
|
||||
p = int_val(status_file);
|
||||
if(task_kill(p, 0) == 0) // process is still running
|
||||
TaskStatus status = task_status_parse(status_file);
|
||||
if(task_status_is_alive(status))
|
||||
{
|
||||
file_close_locked(lock_fd);
|
||||
return(p);
|
||||
return(status.pid);
|
||||
}
|
||||
file_unlink(status_file_name);
|
||||
}
|
||||
file_close_locked(lock_fd);
|
||||
return(p);
|
||||
return(0);
|
||||
}
|
||||
|
||||
pid_t task(String key, std::function<void()> exec_after_spawn, u64 timeout)
|
||||
{
|
||||
String status_file_name = context->server->config["BIN_DIRECTORY"] + "/task-" + key;
|
||||
String status_file_name = task_file_prefix(key);
|
||||
String lock_file_name = status_file_name + ".lock";
|
||||
int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task:" + key);
|
||||
if(lock_fd == -1)
|
||||
{
|
||||
fprintf(stderr, "task(): could not lock task key '%s'\n", key.c_str());
|
||||
return(0);
|
||||
}
|
||||
String status_file = file_get_contents(status_file_name);
|
||||
pid_t p;
|
||||
pid_t p = 0;
|
||||
if(status_file != "")
|
||||
{
|
||||
p = int_val(status_file);
|
||||
if(task_kill(p, 0) == 0) // process is still running
|
||||
TaskStatus status = task_status_parse(status_file);
|
||||
if(task_status_is_alive(status))
|
||||
{
|
||||
printf("(P) worker process '%s' already running: PID %i\n", key.c_str(), p);
|
||||
printf("(P) worker process '%s' already running: PID %i\n", key.c_str(), status.pid);
|
||||
file_close_locked(lock_fd);
|
||||
return(p);
|
||||
return(status.pid);
|
||||
}
|
||||
//printf("(P) worker process '%s' had crashed: PID %i\n", key.c_str(), p);
|
||||
file_unlink(status_file_name);
|
||||
}
|
||||
p = fork();
|
||||
if(p < 0)
|
||||
{
|
||||
fprintf(stderr, "task(): fork failed for key '%s': %s\n", key.c_str(), strerror(errno));
|
||||
file_close_locked(lock_fd);
|
||||
return(0);
|
||||
}
|
||||
if(p == 0)
|
||||
{
|
||||
file_release_process_locks("task child startup");
|
||||
file_close_locked(lock_fd);
|
||||
my_pid = getpid();
|
||||
prctl(PR_SET_PDEATHSIG, SIGHUP);
|
||||
if(timeout > 0)
|
||||
alarm(timeout);
|
||||
|
||||
close(context->resources.client_socket);
|
||||
context->resources.client_socket = 0;
|
||||
//printf("(C) child procress started, PID:%i\n", my_pid);
|
||||
//prctl(PR_SET_PDEATHSIG, SIGHUP);
|
||||
if(context->resources.client_socket > 0)
|
||||
{
|
||||
close(context->resources.client_socket);
|
||||
context->resources.client_socket = 0;
|
||||
}
|
||||
exec_after_spawn();
|
||||
int exit_lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task-exit:" + key);
|
||||
file_unlink(status_file_name);
|
||||
file_close_locked(exit_lock_fd);
|
||||
if(exit_lock_fd != -1)
|
||||
{
|
||||
file_unlink(status_file_name);
|
||||
file_close_locked(exit_lock_fd);
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "task(): could not lock task key '%s' during child cleanup\n", key.c_str());
|
||||
}
|
||||
printf("(P) worker process '%s' terminated: PID %i\n", key.c_str(), my_pid);
|
||||
exit(0);
|
||||
}
|
||||
else
|
||||
|
||||
if(!file_put_contents(status_file_name, task_status_content(p)))
|
||||
{
|
||||
file_put_contents(status_file_name, std::to_string(p));
|
||||
fprintf(stderr, "task(): could not write status file for key '%s'; terminating child PID %i\n", key.c_str(), p);
|
||||
kill(p, SIGTERM);
|
||||
file_close_locked(lock_fd);
|
||||
printf("(P) worker process '%s' spawned: PID %i\n", key.c_str(), p);
|
||||
return(p);
|
||||
return(0);
|
||||
}
|
||||
file_close_locked(lock_fd);
|
||||
printf("(P) worker process '%s' spawned: PID %i\n", key.c_str(), p);
|
||||
return(p);
|
||||
}
|
||||
|
||||
#include <unistd.h>
|
||||
pid_t task_repeat(String key, f64 interval, std::function<void()> exec_after_spawn, u64 timeout)
|
||||
{
|
||||
auto repeater_function = [&]() {
|
||||
while (true)
|
||||
if(interval <= 0)
|
||||
throw std::runtime_error("task_repeat(): interval must be greater than zero");
|
||||
auto repeater_function = [key, interval, exec_after_spawn, timeout]() {
|
||||
f64 started_at = time_precise();
|
||||
while (timeout == 0 || time_precise() - started_at < (f64)timeout)
|
||||
{
|
||||
exec_after_spawn();
|
||||
f64 elapsed = time_precise() - started_at;
|
||||
if(timeout > 0 && elapsed >= (f64)timeout)
|
||||
break;
|
||||
f64 sleep_seconds = interval;
|
||||
if(timeout > 0 && elapsed + sleep_seconds > (f64)timeout)
|
||||
sleep_seconds = (f64)timeout - elapsed;
|
||||
printf("(P) worker process '%s' sleeping\n", key.c_str());
|
||||
usleep((s64)(interval*1000000));
|
||||
if(sleep_seconds > 0)
|
||||
usleep((useconds_t)(sleep_seconds * 1000000.0));
|
||||
}
|
||||
};
|
||||
return(task(key, repeater_function, timeout));
|
||||
@@ -703,17 +815,21 @@ pid_t task_repeat(String key, f64 interval, std::function<void()> exec_after_spa
|
||||
|
||||
void on_child_exit(int sig)
|
||||
{
|
||||
pid_t pid;
|
||||
int status;
|
||||
if ((pid = waitpid(-1, &status, WNOHANG)) != -1)
|
||||
{
|
||||
(void)sig;
|
||||
pid_t pid;
|
||||
int status;
|
||||
while((pid = waitpid(-1, &status, WNOHANG)) > 0)
|
||||
{
|
||||
if(workers.count(pid) > 0)
|
||||
{
|
||||
workers.erase(pid);
|
||||
printf("(P) child terminated (PID:%i)\n", pid);
|
||||
//spawn_subprocess();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("(P) task child reaped (PID:%i)\n", pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StringList ls(String dir)
|
||||
@@ -731,6 +847,7 @@ StringMap make_server_settings()
|
||||
cfg["LIT_ESC"] = "3d5b5_1";
|
||||
cfg["CONTENT_TYPE"] = "text/html; charset=utf-8";
|
||||
cfg["FCGI_SOCKET_PATH"] = "/run/uce.sock";
|
||||
cfg["CLI_SOCKET_PATH"] = "/run/uce/cli.sock";
|
||||
cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads";
|
||||
cfg["SESSION_PATH"] = "/tmp/uce/sessions";
|
||||
cfg["COMPILER_SYS_PATH"] = ".";
|
||||
|
||||
@@ -85,6 +85,7 @@ struct SharedUnit {
|
||||
request_ref_handler on_render = 0;
|
||||
request_ref_handler on_component = 0;
|
||||
request_ref_handler on_websocket = 0;
|
||||
request_ref_handler on_cli = 0;
|
||||
request_ref_handler on_once = 0;
|
||||
request_ref_handler on_init = 0;
|
||||
|
||||
@@ -210,6 +211,7 @@ struct Request {
|
||||
u64 client_socket = 0;
|
||||
u64 server_socket = 0;
|
||||
bool is_websocket = false;
|
||||
bool is_cli = false;
|
||||
String websocket_connection_id = "";
|
||||
String websocket_scope = "";
|
||||
DTree* websocket_connection_state = 0;
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#include "hash.cpp"
|
||||
#include "sys.cpp"
|
||||
#include "uri.cpp"
|
||||
#include "cli.cpp"
|
||||
#include "compiler-parser.cpp"
|
||||
#include "compiler.cpp"
|
||||
#include "markdown.cpp"
|
||||
#include "zip.cpp"
|
||||
#include "mysql-connector.cpp"
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "functionlib.h"
|
||||
#include "sys.h"
|
||||
#include "uri.h"
|
||||
#include "cli.h"
|
||||
#include "compiler.h"
|
||||
#include "markdown.h"
|
||||
#include "zip.h"
|
||||
#include "mysql-connector.h"
|
||||
|
||||
@@ -707,6 +707,11 @@ bool WSFrame::parse(const String& buffer, String& error)
|
||||
if(buffer.length() < 4)
|
||||
return(false);
|
||||
payload_length = ((u64)raw[2] << 8) | (u64)raw[3];
|
||||
if(payload_length < 126)
|
||||
{
|
||||
error = "non-minimal websocket frame length";
|
||||
return(false);
|
||||
}
|
||||
header_length = 4;
|
||||
}
|
||||
else if(payload_length == 127)
|
||||
@@ -721,6 +726,11 @@ bool WSFrame::parse(const String& buffer, String& error)
|
||||
payload_length = 0;
|
||||
for(u32 i = 0; i < 8; i++)
|
||||
payload_length = (payload_length << 8) | (u64)raw[2 + i];
|
||||
if(payload_length <= 0xFFFF)
|
||||
{
|
||||
error = "non-minimal websocket frame length";
|
||||
return(false);
|
||||
}
|
||||
header_length = 10;
|
||||
}
|
||||
|
||||
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
#include "zip.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "../../vendor/miniz/miniz.c"
|
||||
#include "../../vendor/miniz/miniz_tdef.c"
|
||||
#include "../../vendor/miniz/miniz_tinfl.c"
|
||||
#include "../../vendor/miniz/miniz_zip.c"
|
||||
}
|
||||
|
||||
String zip_error(String api, String detail)
|
||||
{
|
||||
return(api + "(): " + detail);
|
||||
}
|
||||
|
||||
bool zip_entry_name_safe(String name)
|
||||
{
|
||||
if(name == "")
|
||||
return(false);
|
||||
if(name[0] == '/' || name[0] == '\\')
|
||||
return(false);
|
||||
if(name.find(":") != String::npos)
|
||||
return(false);
|
||||
|
||||
auto parts = split(replace(name, "\\", "/"), "/");
|
||||
for(auto part : parts)
|
||||
{
|
||||
if(part == "..")
|
||||
return(false);
|
||||
}
|
||||
return(true);
|
||||
}
|
||||
|
||||
String zip_normalize_entry_name(String name)
|
||||
{
|
||||
name = replace(name, "\\", "/");
|
||||
while(name.find("//") != String::npos)
|
||||
name = replace(name, "//", "/");
|
||||
return(name);
|
||||
}
|
||||
|
||||
bool zip_ensure_parent_directory(String path)
|
||||
{
|
||||
String parent = dirname(path);
|
||||
if(parent == "" || parent == "." || parent == "/")
|
||||
return(true);
|
||||
if(file_exists(parent))
|
||||
return(true);
|
||||
return(mkdir(parent));
|
||||
}
|
||||
|
||||
void zip_add_file_info(DTree& files, mz_zip_archive* archive, mz_uint index)
|
||||
{
|
||||
mz_zip_archive_file_stat stat;
|
||||
std::memset(&stat, 0, sizeof(stat));
|
||||
if(!mz_zip_reader_file_stat(archive, index, &stat))
|
||||
throw std::runtime_error(zip_error("zip_list", "could not read file metadata at index " + std::to_string((u64)index)));
|
||||
|
||||
DTree item;
|
||||
item["name"] = String(stat.m_filename);
|
||||
item["index"] = (f64)index;
|
||||
item["size"] = (f64)stat.m_uncomp_size;
|
||||
item["compressed_size"] = (f64)stat.m_comp_size;
|
||||
item["is_directory"].set_bool(mz_zip_reader_is_file_a_directory(archive, index));
|
||||
item["method"] = (f64)stat.m_method;
|
||||
files.push(item);
|
||||
}
|
||||
|
||||
DTree zip_list(String zip_file_name)
|
||||
{
|
||||
mz_zip_archive archive;
|
||||
std::memset(&archive, 0, sizeof(archive));
|
||||
if(!mz_zip_reader_init_file(&archive, zip_file_name.c_str(), 0))
|
||||
throw std::runtime_error(zip_error("zip_list", "could not open " + zip_file_name));
|
||||
|
||||
DTree result;
|
||||
try
|
||||
{
|
||||
mz_uint count = mz_zip_reader_get_num_files(&archive);
|
||||
result["file"] = zip_file_name;
|
||||
result["count"] = (f64)count;
|
||||
result["entries"].set_array();
|
||||
for(mz_uint i = 0; i < count; i++)
|
||||
zip_add_file_info(result["entries"], &archive, i);
|
||||
mz_zip_reader_end(&archive);
|
||||
return(result);
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
mz_zip_reader_end(&archive);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
String zip_read(String zip_file_name, String entry_name)
|
||||
{
|
||||
entry_name = zip_normalize_entry_name(entry_name);
|
||||
if(!zip_entry_name_safe(entry_name))
|
||||
throw std::runtime_error(zip_error("zip_read", "unsafe or empty entry name '" + entry_name + "'"));
|
||||
|
||||
mz_zip_archive archive;
|
||||
std::memset(&archive, 0, sizeof(archive));
|
||||
if(!mz_zip_reader_init_file(&archive, zip_file_name.c_str(), 0))
|
||||
throw std::runtime_error(zip_error("zip_read", "could not open " + zip_file_name));
|
||||
|
||||
size_t size = 0;
|
||||
void* data = mz_zip_reader_extract_file_to_heap(&archive, entry_name.c_str(), &size, 0);
|
||||
if(!data)
|
||||
{
|
||||
mz_zip_reader_end(&archive);
|
||||
throw std::runtime_error(zip_error("zip_read", "entry not found or not readable: " + entry_name));
|
||||
}
|
||||
String result((char*)data, size);
|
||||
mz_free(data);
|
||||
mz_zip_reader_end(&archive);
|
||||
return(result);
|
||||
}
|
||||
|
||||
String zip_entry_content(DTree item)
|
||||
{
|
||||
DTree* content = item.key("content");
|
||||
if(content)
|
||||
return(content->to_string());
|
||||
DTree* file_name = item.key("file");
|
||||
if(file_name)
|
||||
return(file_get_contents(file_name->to_string()));
|
||||
return(item.to_string());
|
||||
}
|
||||
|
||||
String zip_entry_name(String key, DTree item)
|
||||
{
|
||||
DTree* name = item.key("name");
|
||||
if(name)
|
||||
return(name->to_string());
|
||||
return(key);
|
||||
}
|
||||
|
||||
void zip_add_entry(mz_zip_archive* archive, String name, String content)
|
||||
{
|
||||
name = zip_normalize_entry_name(name);
|
||||
if(!zip_entry_name_safe(name))
|
||||
throw std::runtime_error(zip_error("zip_create", "unsafe or empty entry name '" + name + "'"));
|
||||
if(!mz_zip_writer_add_mem(archive, name.c_str(), content.data(), content.size(), MZ_BEST_COMPRESSION))
|
||||
throw std::runtime_error(zip_error("zip_create", "could not add entry " + name));
|
||||
}
|
||||
|
||||
bool zip_create(String zip_file_name, DTree entries)
|
||||
{
|
||||
if(!zip_ensure_parent_directory(zip_file_name))
|
||||
throw std::runtime_error(zip_error("zip_create", "could not create parent directory for " + zip_file_name));
|
||||
|
||||
mz_zip_archive archive;
|
||||
std::memset(&archive, 0, sizeof(archive));
|
||||
if(!mz_zip_writer_init_file(&archive, zip_file_name.c_str(), 0))
|
||||
throw std::runtime_error(zip_error("zip_create", "could not open " + zip_file_name + " for writing"));
|
||||
|
||||
try
|
||||
{
|
||||
entries.each([&](DTree item, String key)
|
||||
{
|
||||
String name = zip_entry_name(key, item);
|
||||
String content = zip_entry_content(item);
|
||||
zip_add_entry(&archive, name, content);
|
||||
});
|
||||
if(!mz_zip_writer_finalize_archive(&archive))
|
||||
throw std::runtime_error(zip_error("zip_create", "could not finalize " + zip_file_name));
|
||||
mz_zip_writer_end(&archive);
|
||||
return(true);
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
mz_zip_writer_end(&archive);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void gz_append_u32_le(String& out, u32 value)
|
||||
{
|
||||
out.push_back((char)(value & 0xff));
|
||||
out.push_back((char)((value >> 8) & 0xff));
|
||||
out.push_back((char)((value >> 16) & 0xff));
|
||||
out.push_back((char)((value >> 24) & 0xff));
|
||||
}
|
||||
|
||||
u32 gz_read_u32_le(String src, size_t offset)
|
||||
{
|
||||
return(
|
||||
((u32)(u8)src[offset]) |
|
||||
((u32)(u8)src[offset + 1] << 8) |
|
||||
((u32)(u8)src[offset + 2] << 16) |
|
||||
((u32)(u8)src[offset + 3] << 24)
|
||||
);
|
||||
}
|
||||
|
||||
void gz_skip_zero_terminated(String compressed, size_t& offset)
|
||||
{
|
||||
while(offset < compressed.size() && compressed[offset] != '\0')
|
||||
offset++;
|
||||
if(offset >= compressed.size())
|
||||
throw std::runtime_error("gz_uncompress(): truncated gzip header");
|
||||
offset++;
|
||||
}
|
||||
|
||||
size_t gz_deflate_offset(String compressed)
|
||||
{
|
||||
if(compressed.size() < 18)
|
||||
throw std::runtime_error("gz_uncompress(): compressed data is too short");
|
||||
if((u8)compressed[0] != 0x1f || (u8)compressed[1] != 0x8b)
|
||||
throw std::runtime_error("gz_uncompress(): missing gzip header");
|
||||
if((u8)compressed[2] != 8)
|
||||
throw std::runtime_error("gz_uncompress(): unsupported gzip compression method");
|
||||
|
||||
u8 flags = (u8)compressed[3];
|
||||
if(flags & 0xe0)
|
||||
throw std::runtime_error("gz_uncompress(): gzip header uses reserved flags");
|
||||
|
||||
size_t offset = 10;
|
||||
if(flags & 0x04)
|
||||
{
|
||||
if(offset + 2 > compressed.size())
|
||||
throw std::runtime_error("gz_uncompress(): truncated gzip extra header");
|
||||
u16 extra_len = ((u16)(u8)compressed[offset]) | ((u16)(u8)compressed[offset + 1] << 8);
|
||||
offset += 2 + extra_len;
|
||||
if(offset > compressed.size())
|
||||
throw std::runtime_error("gz_uncompress(): truncated gzip extra data");
|
||||
}
|
||||
if(flags & 0x08)
|
||||
gz_skip_zero_terminated(compressed, offset);
|
||||
if(flags & 0x10)
|
||||
gz_skip_zero_terminated(compressed, offset);
|
||||
if(flags & 0x02)
|
||||
{
|
||||
offset += 2;
|
||||
if(offset > compressed.size())
|
||||
throw std::runtime_error("gz_uncompress(): truncated gzip header crc");
|
||||
}
|
||||
if(offset + 8 > compressed.size())
|
||||
throw std::runtime_error("gz_uncompress(): missing gzip footer");
|
||||
return(offset);
|
||||
}
|
||||
|
||||
String gz_compress(String src)
|
||||
{
|
||||
mz_stream stream;
|
||||
std::memset(&stream, 0, sizeof(stream));
|
||||
int status = mz_deflateInit2(&stream, MZ_DEFAULT_COMPRESSION, MZ_DEFLATED, -MZ_DEFAULT_WINDOW_BITS, 9, MZ_DEFAULT_STRATEGY);
|
||||
if(status != MZ_OK)
|
||||
throw std::runtime_error("gz_compress(): could not initialize compressor");
|
||||
|
||||
mz_ulong bound = mz_deflateBound(&stream, src.size());
|
||||
String deflated;
|
||||
deflated.resize(bound);
|
||||
stream.next_in = (const unsigned char*)src.data();
|
||||
stream.avail_in = src.size();
|
||||
stream.next_out = (unsigned char*)deflated.data();
|
||||
stream.avail_out = deflated.size();
|
||||
status = mz_deflate(&stream, MZ_FINISH);
|
||||
if(status != MZ_STREAM_END)
|
||||
{
|
||||
mz_deflateEnd(&stream);
|
||||
throw std::runtime_error("gz_compress(): compression failed");
|
||||
}
|
||||
deflated.resize(stream.total_out);
|
||||
mz_deflateEnd(&stream);
|
||||
|
||||
String result;
|
||||
result.reserve(10 + deflated.size() + 8);
|
||||
result.push_back((char)0x1f);
|
||||
result.push_back((char)0x8b);
|
||||
result.push_back((char)0x08);
|
||||
result.push_back((char)0x00);
|
||||
gz_append_u32_le(result, 0);
|
||||
result.push_back((char)0x00);
|
||||
result.push_back((char)0xff);
|
||||
result.append(deflated);
|
||||
gz_append_u32_le(result, (u32)mz_crc32(MZ_CRC32_INIT, (const unsigned char*)src.data(), src.size()));
|
||||
gz_append_u32_le(result, (u32)src.size());
|
||||
return(result);
|
||||
}
|
||||
|
||||
String gz_uncompress(String compressed)
|
||||
{
|
||||
size_t deflate_offset = gz_deflate_offset(compressed);
|
||||
size_t footer_offset = compressed.size() - 8;
|
||||
size_t deflate_size = footer_offset - deflate_offset;
|
||||
size_t out_len = 0;
|
||||
void* out = tinfl_decompress_mem_to_heap(compressed.data() + deflate_offset, deflate_size, &out_len, 0);
|
||||
if(!out)
|
||||
throw std::runtime_error("gz_uncompress(): decompression failed");
|
||||
|
||||
String result((char*)out, out_len);
|
||||
mz_free(out);
|
||||
|
||||
u32 expected_crc = gz_read_u32_le(compressed, footer_offset);
|
||||
u32 expected_size = gz_read_u32_le(compressed, footer_offset + 4);
|
||||
u32 actual_crc = (u32)mz_crc32(MZ_CRC32_INIT, (const unsigned char*)result.data(), result.size());
|
||||
if(actual_crc != expected_crc)
|
||||
throw std::runtime_error("gz_uncompress(): crc check failed");
|
||||
if((u32)result.size() != expected_size)
|
||||
throw std::runtime_error("gz_uncompress(): size check failed");
|
||||
return(result);
|
||||
}
|
||||
|
||||
bool zip_extract(String zip_file_name, String destination_directory)
|
||||
{
|
||||
if(destination_directory == "")
|
||||
throw std::runtime_error(zip_error("zip_extract", "destination directory is empty"));
|
||||
if(!file_exists(destination_directory) && !mkdir(destination_directory))
|
||||
throw std::runtime_error(zip_error("zip_extract", "could not create destination directory " + destination_directory));
|
||||
|
||||
mz_zip_archive archive;
|
||||
std::memset(&archive, 0, sizeof(archive));
|
||||
if(!mz_zip_reader_init_file(&archive, zip_file_name.c_str(), 0))
|
||||
throw std::runtime_error(zip_error("zip_extract", "could not open " + zip_file_name));
|
||||
|
||||
try
|
||||
{
|
||||
mz_uint count = mz_zip_reader_get_num_files(&archive);
|
||||
for(mz_uint i = 0; i < count; i++)
|
||||
{
|
||||
mz_zip_archive_file_stat stat;
|
||||
std::memset(&stat, 0, sizeof(stat));
|
||||
if(!mz_zip_reader_file_stat(&archive, i, &stat))
|
||||
throw std::runtime_error(zip_error("zip_extract", "could not read file metadata at index " + std::to_string((u64)i)));
|
||||
String name = zip_normalize_entry_name(stat.m_filename);
|
||||
if(!zip_entry_name_safe(name))
|
||||
throw std::runtime_error(zip_error("zip_extract", "refusing unsafe entry name '" + name + "'"));
|
||||
|
||||
String target = path_join(destination_directory, name);
|
||||
if(mz_zip_reader_is_file_a_directory(&archive, i))
|
||||
{
|
||||
if(!file_exists(target) && !mkdir(target))
|
||||
throw std::runtime_error(zip_error("zip_extract", "could not create directory " + target));
|
||||
continue;
|
||||
}
|
||||
if(!zip_ensure_parent_directory(target))
|
||||
throw std::runtime_error(zip_error("zip_extract", "could not create parent directory for " + target));
|
||||
if(!mz_zip_reader_extract_to_file(&archive, i, target.c_str(), 0))
|
||||
throw std::runtime_error(zip_error("zip_extract", "could not extract " + name));
|
||||
}
|
||||
mz_zip_reader_end(&archive);
|
||||
return(true);
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
mz_zip_reader_end(&archive);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
DTree zip_list(String zip_file_name);
|
||||
String zip_read(String zip_file_name, String entry_name);
|
||||
bool zip_create(String zip_file_name, DTree entries);
|
||||
bool zip_extract(String zip_file_name, String destination_directory);
|
||||
String gz_compress(String src);
|
||||
String gz_uncompress(String compressed);
|
||||
+188
-53
@@ -26,6 +26,7 @@ static DTree websocket_exec_inflight_job;
|
||||
static String websocket_exec_write_buffer = "";
|
||||
|
||||
void close_inherited_server_sockets();
|
||||
u64 request_seed_from_time(f64 time_value);
|
||||
|
||||
Request* set_active_request(Request& request)
|
||||
{
|
||||
@@ -315,6 +316,25 @@ void websocket_exec_append_command(DTree command)
|
||||
context->resources.websocket_dispatch_commands.push(command);
|
||||
}
|
||||
|
||||
DTree websocket_exec_make_message_command(String action, String message, bool binary)
|
||||
{
|
||||
DTree command;
|
||||
command["action"] = action;
|
||||
command["binary"].set_bool(binary);
|
||||
command["message_b64"] = websocket_ipc_base64_encode(message);
|
||||
return(command);
|
||||
}
|
||||
|
||||
DTree websocket_exec_make_close_command(String connection_id, u16 status_code = 1000, String reason = "")
|
||||
{
|
||||
DTree command;
|
||||
command["action"] = "close";
|
||||
command["connection_id"] = connection_id;
|
||||
command["status_code"] = (f64)status_code;
|
||||
command["reason"] = reason;
|
||||
return(command);
|
||||
}
|
||||
|
||||
void websocket_exec_apply_command(DTree command)
|
||||
{
|
||||
String action = command["action"].to_string();
|
||||
@@ -470,7 +490,7 @@ Request websocket_exec_build_event_request(DTree job, String message)
|
||||
event_request.stats.time_init = time_precise();
|
||||
event_request.stats.time_start = event_request.stats.time_init;
|
||||
event_request.random_index = 0;
|
||||
event_request.random_seed = gen_noise64(*reinterpret_cast<u64*>(&event_request.stats.time_start));
|
||||
event_request.random_seed = request_seed_from_time(event_request.stats.time_start);
|
||||
event_request.response_code = "WEBSOCKET";
|
||||
event_request.header["Content-Type"] = server_state.config["CONTENT_TYPE"];
|
||||
event_request.in = message;
|
||||
@@ -747,28 +767,23 @@ u64 ws_connection_count(String scope)
|
||||
|
||||
bool ws_send(String message, bool binary, String scope)
|
||||
{
|
||||
String normalized_scope = normalize_ws_scope(scope);
|
||||
if(context && context->resources.websocket_dispatch_capture)
|
||||
{
|
||||
DTree command;
|
||||
command["action"] = "broadcast";
|
||||
command["scope"] = normalize_ws_scope(scope);
|
||||
command["binary"].set_bool(binary);
|
||||
command["message_b64"] = websocket_ipc_base64_encode(message);
|
||||
DTree command = websocket_exec_make_message_command("broadcast", message, binary);
|
||||
command["scope"] = normalized_scope;
|
||||
websocket_exec_append_command(command);
|
||||
return(true);
|
||||
}
|
||||
return(server.websocket_broadcast(normalize_ws_scope(scope), message, binary) > 0);
|
||||
return(server.websocket_broadcast(normalized_scope, message, binary) > 0);
|
||||
}
|
||||
|
||||
bool ws_send_to(String connection_id, String message, bool binary)
|
||||
{
|
||||
if(context && context->resources.websocket_dispatch_capture)
|
||||
{
|
||||
DTree command;
|
||||
command["action"] = "send_to";
|
||||
DTree command = websocket_exec_make_message_command("send_to", message, binary);
|
||||
command["connection_id"] = connection_id;
|
||||
command["binary"].set_bool(binary);
|
||||
command["message_b64"] = websocket_ipc_base64_encode(message);
|
||||
websocket_exec_append_command(command);
|
||||
return(true);
|
||||
}
|
||||
@@ -783,17 +798,156 @@ bool ws_close(String connection_id)
|
||||
return(false);
|
||||
if(context && context->resources.websocket_dispatch_capture)
|
||||
{
|
||||
DTree command;
|
||||
command["action"] = "close";
|
||||
command["connection_id"] = connection_id;
|
||||
command["status_code"] = (f64)1000;
|
||||
command["reason"] = "";
|
||||
websocket_exec_append_command(command);
|
||||
websocket_exec_append_command(websocket_exec_make_close_command(connection_id));
|
||||
return(true);
|
||||
}
|
||||
return(server.websocket_close(connection_id));
|
||||
}
|
||||
|
||||
bool cli_path_is_safe(String command)
|
||||
{
|
||||
for(auto& segment : split(command, "/"))
|
||||
{
|
||||
if(segment == "..")
|
||||
return(false);
|
||||
}
|
||||
return(true);
|
||||
}
|
||||
|
||||
String cli_resolve_unit_path(Request& request, String command, String& document_root)
|
||||
{
|
||||
document_root = first(request.params["DOCUMENT_ROOT"], cwd_get());
|
||||
if(document_root.length() > 1 && document_root[document_root.length()-1] == '/')
|
||||
document_root.resize(document_root.length()-1);
|
||||
|
||||
String script_filename = document_root + command;
|
||||
if(file_exists(script_filename))
|
||||
return(script_filename);
|
||||
|
||||
String site_filename = document_root + "/site" + command;
|
||||
if(file_exists(site_filename))
|
||||
{
|
||||
document_root = document_root + "/site";
|
||||
return(site_filename);
|
||||
}
|
||||
|
||||
return("");
|
||||
}
|
||||
|
||||
u64 request_seed_from_time(f64 time_value)
|
||||
{
|
||||
u64 bits = 0;
|
||||
static_assert(sizeof(bits) == sizeof(time_value));
|
||||
memcpy(&bits, &time_value, sizeof(bits));
|
||||
return(gen_noise64(bits));
|
||||
}
|
||||
|
||||
void prepare_request_body_maps(Request& request)
|
||||
{
|
||||
request.get = parse_query(request.params["QUERY_STRING"]);
|
||||
if(request.params["HTTP_COOKIE"].length() > 0)
|
||||
request.cookies = parse_cookies(request.params["HTTP_COOKIE"]);
|
||||
|
||||
String ct_info = request.params["CONTENT_TYPE"];
|
||||
String ct_type = nibble(ct_info, ";");
|
||||
if(request.params["REQUEST_METHOD"] == "POST")
|
||||
{
|
||||
if(ct_type == "multipart/form-data")
|
||||
{
|
||||
nibble("boundary=", ct_info);
|
||||
request.post = parse_multipart(request.in, String("--")+ct_info, request.uploaded_files);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.post = parse_query(request.in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int handle_cli_complete(FastCGIRequest& request)
|
||||
{
|
||||
Request* previous_context = set_active_request(request);
|
||||
server_state.request_count += 1;
|
||||
request.server = &server_state;
|
||||
request.resources.is_cli = true;
|
||||
request.params["UCE_CLI"] = "1";
|
||||
request.stats.time_start = time_precise();
|
||||
request.header["Content-Type"] = "text/plain; charset=utf-8";
|
||||
request.random_index = 0;
|
||||
request.random_seed = request_seed_from_time(request.stats.time_start);
|
||||
request.ob_start();
|
||||
prepare_request_body_maps(request);
|
||||
|
||||
String method = trim(request.params["REQUEST_METHOD"]);
|
||||
String command = trim(first(request.params["DOCUMENT_URI"], request.params["REQUEST_URI"]));
|
||||
|
||||
try
|
||||
{
|
||||
if(method != "GET" && method != "POST")
|
||||
{
|
||||
request.set_status(405, "Method Not Allowed");
|
||||
request.header["Allow"] = "GET, POST";
|
||||
print("UCE CLI socket accepts GET and POST commands only\n");
|
||||
}
|
||||
else if(command == "/" || command == "/help")
|
||||
{
|
||||
print("UCE CLI command socket\n\nAvailable test hooks:\n GET /ping\n GET /status\n");
|
||||
}
|
||||
else if(command == "/ping" || command == "/test")
|
||||
{
|
||||
print("uce-cli: ok\n");
|
||||
}
|
||||
else if(command == "/status")
|
||||
{
|
||||
print("pid=", std::to_string(getpid()), "\nclients=", std::to_string(server.client_sockets.size()), "\n");
|
||||
}
|
||||
else if(command.length() >= 4 && command.substr(command.length() - 4) == ".uce")
|
||||
{
|
||||
if(!cli_path_is_safe(command))
|
||||
{
|
||||
request.set_status(400, "Bad Request");
|
||||
print("invalid UCE CLI unit path\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
String document_root;
|
||||
String script_filename = cli_resolve_unit_path(request, command, document_root);
|
||||
if(script_filename == "")
|
||||
{
|
||||
request.set_status(404, "Not Found");
|
||||
print("UCE CLI unit not found: ", command, "\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
request.params["DOCUMENT_ROOT"] = document_root;
|
||||
request.params["SCRIPT_FILENAME"] = script_filename;
|
||||
request.props = DTree();
|
||||
compiler_invoke_cli(&request, script_filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
request.set_status(404, "Not Found");
|
||||
print("unknown UCE CLI command: ", command, "\n");
|
||||
}
|
||||
}
|
||||
catch(const std::exception& e)
|
||||
{
|
||||
render_request_failure(request, "uncaught exception during CLI request", e.what(), capture_backtrace_string(32, 1), 500);
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
render_request_failure(request, "unknown uncaught exception during CLI request", "", capture_backtrace_string(32, 1), 500);
|
||||
}
|
||||
|
||||
for(auto &f : request.uploaded_files)
|
||||
file_unlink(f.tmp_name);
|
||||
cleanup_mysql_connections();
|
||||
restore_active_request(previous_context);
|
||||
return(request.flags.status);
|
||||
}
|
||||
|
||||
int handle_request(FastCGIRequest& request) {
|
||||
// This is always the first event to occur. It occurs when the
|
||||
// server receives all parameters. There may be more data coming on the
|
||||
@@ -805,20 +959,9 @@ int handle_request(FastCGIRequest& request) {
|
||||
}
|
||||
|
||||
int handle_data(FastCGIRequest& request) {
|
||||
// This event occurs when data is received on the standard input stream.
|
||||
// A simple String is used to hold the input stream, so it is the
|
||||
// responsibility of the application to remember which data it has
|
||||
// processed. The application may modify it; new data will be appended
|
||||
// to it by the server. The same goes for the output and error streams:
|
||||
// the application should append data to them; the server will remove
|
||||
// all sent data from them.
|
||||
return 0; // still OK
|
||||
|
||||
std::transform(request.in.begin(), request.in.end(),
|
||||
std::back_inserter(request.err),
|
||||
std::bind1st(std::plus<char>(), 1));
|
||||
request.in.clear(); // don't process it again
|
||||
return 0; // still OK
|
||||
// Request bodies are accumulated by the FastCGI transport and parsed once
|
||||
// the input stream is closed in handle_complete().
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_complete(FastCGIRequest& request) {
|
||||
@@ -833,10 +976,10 @@ int handle_complete(FastCGIRequest& request) {
|
||||
request.stats.time_start = time_precise();
|
||||
//request.stats.mem_alloc = 0;
|
||||
//request.stats.mem_high = 0;
|
||||
request.header["Content-Type"] = context->server->config["CONTENT_TYPE"];
|
||||
request.header["Content-Type"] = (request.resources.is_cli ? "text/plain; charset=utf-8" : context->server->config["CONTENT_TYPE"]);
|
||||
request.get = parse_query(request.params["QUERY_STRING"]);
|
||||
request.random_index = 0;
|
||||
request.random_seed = gen_noise64(*reinterpret_cast<u64*>(&request.stats.time_start));
|
||||
request.random_seed = request_seed_from_time(request.stats.time_start);
|
||||
request.ob_start();
|
||||
request_fault_request = &request;
|
||||
request_fault_active = 1;
|
||||
@@ -858,27 +1001,12 @@ int handle_complete(FastCGIRequest& request) {
|
||||
{
|
||||
try
|
||||
{
|
||||
if(request.params["HTTP_COOKIE"].length() > 0)
|
||||
request.cookies = parse_cookies(request.params["HTTP_COOKIE"]);
|
||||
|
||||
String ct_info = request.params["CONTENT_TYPE"];
|
||||
String ct_type = nibble(";", ct_info);
|
||||
|
||||
if(request.params["REQUEST_METHOD"] == "POST")
|
||||
{
|
||||
if(ct_type == "multipart/form-data")
|
||||
{
|
||||
nibble("boundary=", ct_info);
|
||||
request.post = parse_multipart(request.in, String("--")+ct_info, request.uploaded_files);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.post = parse_query(request.in);
|
||||
}
|
||||
}
|
||||
|
||||
prepare_request_body_maps(request);
|
||||
request.props = DTree();
|
||||
compiler_invoke(&request, request.params["SCRIPT_FILENAME"]);
|
||||
if(request.resources.is_cli)
|
||||
compiler_invoke_cli(&request, request.params["SCRIPT_FILENAME"]);
|
||||
else
|
||||
compiler_invoke(&request, request.params["SCRIPT_FILENAME"]);
|
||||
}
|
||||
catch(const std::exception& e)
|
||||
{
|
||||
@@ -1142,6 +1270,7 @@ void listen_for_connections()
|
||||
server.on_request = &handle_request;
|
||||
server.on_data = &handle_data;
|
||||
server.on_complete = &handle_complete;
|
||||
server.on_cli_complete = &handle_cli_complete;
|
||||
server.on_websocket_message = &handle_websocket_message;
|
||||
if(worker_accepts_http)
|
||||
ensure_websocket_executor();
|
||||
@@ -1176,6 +1305,12 @@ void init_base_process()
|
||||
chmod(server_state.config["FCGI_SOCKET_PATH"].c_str(), S_IRWXU | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
|
||||
}
|
||||
|
||||
if(server_state.config["CLI_SOCKET_PATH"] != "")
|
||||
{
|
||||
server.listen_cli(server_state.config["CLI_SOCKET_PATH"]);
|
||||
chmod(server_state.config["CLI_SOCKET_PATH"].c_str(), S_IRWXU | S_IRGRP | S_IWGRP);
|
||||
}
|
||||
|
||||
if(server_state.config["HTTP_PORT"] != "")
|
||||
server.listen_http(int_val(server_state.config["HTTP_PORT"]));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user