Refactor FastCGI server and compiler, enhance HTTP header parsing, and add WebSocket support

- Refactored FastCGIServer class to improve socket handling and added shutdown functionality.
- Updated compiler functions to streamline HTML and text literal processing.
- Enhanced split_kv function to support uppercase keys and added split_http_headers for better HTTP header parsing.
- Introduced new session management functions to validate and handle session IDs.
- Added WebSocket frame parsing and handling in the URI module.
- Created systemd service scripts for easier deployment and management of the UCE FastCGI runtime.
- Added new test cases for header handling, time parsing, and WebSocket functionality.
This commit is contained in:
udo
2026-04-18 14:04:58 +00:00
parent 984453f336
commit cd8f07aaa7
26 changed files with 913 additions and 367 deletions
+23 -36
View File
@@ -1,7 +1,7 @@
#include "compiler.h"
#include <sys/file.h>
String process_html_literal(Request* context, SharedUnit* su, String content)
String process_text_literal(Request* context, SharedUnit* su, String content)
{
String pc;
String HT_START = "print(R\"(";
@@ -89,7 +89,12 @@ String process_html_literal(Request* context, SharedUnit* su, String content)
String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String content)
{
String pc = "#include \"" + su->src_file_name + ".setup.h" + "\"\n#line 1\n";
String pc =
("#include \"")+context->server->config["COMPILER_SYS_PATH"] +"/src/lib/uce_lib.h\" \n"+
file_get_contents(
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]
)+
"#line 1\n";
String token = "";
String html_buffer = "";
u8 mode = 0;
@@ -100,29 +105,13 @@ String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String
{
char c = content[i];
current_line.append(1, c);
if(mode == 2)
if(mode == 1)
{
auto end_pos = content.find(String("</")+token+">", i);
if(end_pos != String::npos)
if(c == '<' && (content[i+1] == '/') && (content[i+2] == '>'))
{
u32 len = token.length() + 3 + end_pos - i;
html_buffer.append(content.substr(i, len - (token.length() == 0 ? 3 : 0)));
i += len - 1;
pc.append(process_html_literal(context, su, html_buffer));
}
else
{
printf("(!) unterminated HTML literal <%s> in %s", token.c_str(), su->file_name.c_str());
}
mode = 0;
}
else if(mode == 1)
{
if(isspace(c) || c == '>')
{
mode = 2;
if(token.length() > 0)
html_buffer.append(1, c);
i += 2;
pc.append(process_text_literal(context, su, html_buffer));
mode = 0;
}
else
{
@@ -130,13 +119,12 @@ String preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String
html_buffer.append(1, c);
}
}
else if(!inside_quote && c == '<' && (content[i+1] == '>'/* || isalpha(content[i+1])*/))
else if(!inside_quote && c == '<' && (content[i+1] == '>'))
{
mode = 1;
token = "";
html_buffer = "";
if(content[i+1] != '>')
html_buffer.append(1, c);
i += 1;
}
else if(c == '\"')
{
@@ -206,7 +194,7 @@ void setup_unit_paths(Request* context, SharedUnit* su, String file_name)
su->so_name = su->bin_path + "/" + su->bin_file_name;
su->api_file_name = su->bin_path + "/" + su->src_file_name + ".exports.txt";
su->setup_file_name = su->bin_path + "/" + su->src_file_name + ".setup.h";
//su->setup_file_name = su->bin_path + "/" + su->src_file_name + ".setup.h";
}
void load_shared_unit(Request* context, SharedUnit* su, String file_name)
@@ -245,7 +233,7 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name)
}
}
String compile_setup_file(Request* context, SharedUnit* su)
/*String compile_setup_file(Request* context, SharedUnit* su)
{
String result =
String("#ifndef UCE_LIB_INCLUDED\n") +
@@ -254,17 +242,13 @@ String compile_setup_file(Request* context, SharedUnit* su)
file_get_contents(
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]) +
"#endif \n";
StringList declarations;
StringList load_units;
result = replace(result, "/*load_declarations*/", join(declarations, "\n"));
result = replace(result, "/*load_units*/", join(load_units, "\n"));
return(result);
}
}*/
void compile_shared_unit(Request* context, SharedUnit* su, String file_name)
{
//setup_unit_paths(context, su, file_name);
f64 comp_start = microtime();
if(!file_exists(su->file_name))
{
@@ -274,7 +258,7 @@ void compile_shared_unit(Request* context, SharedUnit* su, String file_name)
shell_exec("mkdir -p " + shell_escape(su->pre_path));
file_put_contents(su->pre_path + "/" + su->pre_file_name, preprocess_shared_unit(context, su));
file_put_contents(su->setup_file_name, compile_setup_file(context, su));
//file_put_contents(su->setup_file_name, compile_setup_file(context, su));
file_put_contents(su->api_file_name, join(su->api_declarations, "\n"));
if(!su->opt_so_optional)
@@ -293,7 +277,9 @@ void compile_shared_unit(Request* context, SharedUnit* su, String file_name)
else
{
load_shared_unit(context, su, file_name);
//printf("(i) compiled unit %s\n", file_name.c_str());
printf("(i) compiled unit %s in %f s\n",
(su->pre_path + "/" + su->pre_file_name).c_str(),
microtime() - comp_start);
}
}
@@ -390,6 +376,7 @@ SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String
void compiler_invoke(Request* context, String file_name, DTree& call_param)
{
printf("(i) compiler_invoke file %s\n", file_name.c_str());
auto su = compiler_load_shared_unit(context, file_name, "", false);
if(su)
{
View File
+69 -17
View File
@@ -6,7 +6,7 @@ String var_dump(StringMap map, String prefix, String postfix)
for (auto it = map.begin(); it != map.end(); ++it)
{
result.append(prefix + it->first + ": " + it->second + postfix);
result.append(prefix + to_upper(it->first) + ": " + it->second + postfix);
}
return(result);
@@ -42,25 +42,15 @@ u8 hex_to_u8(String src)
String to_lower(String s)
{
String result = "";
for(auto c : s)
{
if(c >= 'A' && c <= 'Z')
c = tolower(c);
result.append(1, c);
}
String result = s;
std::transform(result.begin(), result.end(),result.begin(), ::tolower);
return(result);
}
String to_upper(String s)
{
String result = "";
for(auto c : s)
{
if(c >= 'A' && c <= 'Z')
c = toupper(c);
result.append(1, c);
}
String result = s;
std::transform(result.begin(), result.end(),result.begin(), ::toupper);
return(result);
}
@@ -140,7 +130,7 @@ StringList split(String str, String delim)
return(result);
}
StringMap split_kv(String s, char separator, bool trim_whitespace)
StringMap split_kv(String s, char separator, bool trim_whitespace, bool uppercase_keys)
{
StringMap result;
String k;
@@ -157,7 +147,7 @@ StringMap split_kv(String s, char separator, bool trim_whitespace)
if(c == separator)
mode = 1;
else
k.append(1, c);
k.append(1, uppercase_keys ? toupper(c) : c);
}
else
{
@@ -175,6 +165,68 @@ StringMap split_kv(String s, char separator, bool trim_whitespace)
return(result);
}
StringMap split_http_headers(String s)
{
StringMap result;
String k;
String v;
String query_string;
String base_uri;
for(auto s : split(s, "\n"))
{
u8 mode = 0;
k = "";
v = "";
for(auto c : s)
{
if(mode == 0)
{
if(c == ':')
mode = 1;
else
k.append(1, c);
}
else
{
v.append(1, c);
}
}
if(k != "")
{
k = trim(k);
v = trim(v);
if(v == "")
{
if(result["REQUEST_METHOD"] == "")
{
result["REQUEST_METHOD"] = nibble(k, " ");
result["REQUEST_URI"] = nibble(k, " ");
String query_string = result["REQUEST_URI"];
String base_uri = nibble(query_string, "?");
result["SERVER_PROTOCOL"] = k;
result["SCRIPT_NAME"] = base_uri;
result["DOCUMENT_URI"] = base_uri;
result["QUERY_STRING"] = query_string;
}
else
{
if(k != "")
result["_"] = k;
}
}
else
{
String header_key = to_upper(k);
std::replace(header_key.begin(), header_key.end(), '-', '_');
result["HTTP_"+header_key] = v;
if(header_key == "CONTENT_TYPE" || header_key == "CONTENT_LENGTH")
result[header_key] = v;
}
}
}
return(result);
}
String join(StringList l, String delim)
{
String result;
+5 -3
View File
@@ -11,7 +11,8 @@ String trim(String raw);
StringList split_space(String str);
StringList split(String str, String delim);
StringList split_utf8(String s, bool compound_characters = false);
StringMap split_kv(String s, char separator = '=', bool trim_whitespace = true);
StringMap split_kv(String s, char separator = '=', bool trim_whitespace = true, bool uppercase_keys = false);
StringMap split_http_headers(String s);
String join(StringList l, String delim = "\n");
String nibble(String& haystack, String delim);
void json_consume_space(String s, u32& i);
@@ -84,9 +85,10 @@ String var_dump(StringMap map, String prefix = "", String postfix = "\n");
String var_dump(StringList slist, String prefix = "", String postfix = "\n");
void ob_start();
void ob_clear();
String ob_get_clear();
void ob_close();
String ob_get();
String ob_get_close();
String safe_name(String raw);
#define is_bit_set(var,pos) ((var) & (1<<(pos)))
+31 -7
View File
@@ -135,14 +135,28 @@ String file_get_contents(String file_name)
bool file_put_contents(String file_name, String content)
{
s32 fd = open(file_name.c_str(), O_WRONLY | O_CREAT | O_TRUNC);
s32 fd = open(file_name.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd == -1)
{
printf("(!) Could not write %s\n", file_name.c_str());
return(false);
}
flock(fd, LOCK_EX);
write(fd, content.data(), content.length());
const char* data = content.data();
size_t remaining = content.length();
while(remaining > 0)
{
auto bytes_written = write(fd, data, remaining);
if(bytes_written < 0)
{
flock(fd, LOCK_UN);
close(fd);
printf("(!) Could not fully write %s\n", file_name.c_str());
return(false);
}
data += bytes_written;
remaining -= bytes_written;
}
flock(fd, LOCK_UN);
close(fd);
return(true);
@@ -242,7 +256,7 @@ String gmdate(String format, u64 timestamp)
u64 parse_time(String time_String)
{
return(int_val(trim(shell_exec("date -u -d '"+time_String+"' +'%s'"))));
return(int_val(trim(shell_exec("date -u -d "+shell_escape(time_String)+" +'%s'"))));
}
u64 socket_connect(String host, short port)
@@ -475,8 +489,8 @@ pid_t task(String key, std::function<void()> exec_after_spawn, u64 timeout)
my_pid = getpid();
file_put_contents(status_file_name, std::to_string(my_pid));
close(context->resources.fcgi_socket);
context->resources.fcgi_socket = 0;
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);
exec_after_spawn();
@@ -534,13 +548,13 @@ StringMap make_server_settings()
cfg["SETUP_TEMPLATE"] = "scripts/setup.h.template";
cfg["LIT_ESC"] = "3d5b5_1";
cfg["CONTENT_TYPE"] = "text/html; charset=utf-8";
cfg["SOCKET_PATH"] = "/run/uce.sock";
cfg["FCGI_SOCKET_PATH"] = "/run/uce.sock";
cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads";
cfg["SESSION_PATH"] = "/tmp/uce/sessions";
cfg["COMPILER_SYS_PATH"] = ".";
cfg["PRECOMPILE_FILES_IN"] = ".";
cfg["LISTEN_PORT"] = std::to_string(9993);
cfg["HTTP_PORT"] = std::to_string(8080);
cfg["SESSION_TIME"] = std::to_string(60*60*24*30);
cfg["WORKER_COUNT"] = std::to_string(4);
cfg["MAX_MEMORY"] = std::to_string(1024*1024*16);
@@ -550,5 +564,15 @@ StringMap make_server_settings()
cfg[it.first] = it.second;
}
if(cfg["FCGI_SOCKET_PATH"] == "" && cfg["SOCKET_PATH"] != "")
cfg["FCGI_SOCKET_PATH"] = cfg["SOCKET_PATH"];
if(cfg["SOCKET_PATH"] == "" && cfg["FCGI_SOCKET_PATH"] != "")
cfg["SOCKET_PATH"] = cfg["FCGI_SOCKET_PATH"];
if(cfg["FCGI_PORT"] == "" && cfg["LISTEN_PORT"] != "")
cfg["FCGI_PORT"] = cfg["LISTEN_PORT"];
if(cfg["LISTEN_PORT"] == "" && cfg["FCGI_PORT"] != "")
cfg["LISTEN_PORT"] = cfg["FCGI_PORT"];
return(cfg);
}
+1 -1
View File
@@ -41,7 +41,7 @@ String nibble(String div, String& haystack)
void Request::ob_start()
{
ob_stack.push_back(new std::ostringstream());
ob_stack.push_back(new ByteStream());
ob = ob_stack.back();
}
+23 -11
View File
@@ -47,6 +47,7 @@ String operator+(String lhs, f32 rhs) {
typedef std::map<String, String> StringMap;
typedef std::vector<String> StringList;
typedef std::ostringstream ByteStream;
struct Request;
struct DTree;
@@ -129,22 +130,27 @@ struct Request {
String session_name = "";
std::vector<UploadedFile> uploaded_files;
String response_code = "HTTP/1.1 200 OK";
StringMap header;
StringList set_cookies;
u64 random_seed;
u64 random_index;
std::vector<ByteStream*> ob_stack;
ByteStream* ob;
String in;
std::vector<std::ostringstream*> ob_stack;
std::ostringstream* ob;
String out;
String err;
bool is_finished = false;
struct Flags {
bool log_request = true;
bool is_finished = false;
int status;
bool output_closed = false;
bool params_closed = false;
bool input_closed = false;
} flags;
struct Stats {
@@ -152,20 +158,20 @@ struct Request {
f64 time_init;
f64 time_start;
f64 time_end;
u64 mem_high;
u64 mem_alloc;
u64 mem_high = 0;
u64 mem_alloc = 0;
u32 invoke_count = 0;
} stats;
// OS-related resources
struct Resources {
std::vector<u64> sockets;
std::vector<void*> mysql_connections;
u64 fcgi_socket = 0;
u64 client_socket = 0;
u64 server_socket = 0;
std::string params_buffer;
} resources;
//void invoke(String file_name);
//void invoke(String file_name, DTree& call_param);
void ob_start();
~Request();
@@ -184,10 +190,16 @@ void print(Ts... args)
((*context->ob << args), ...);
}
template <typename... Ts>
void out(Ts... args)
{
((*context->ob << args), ...);
}
template <typename... Ts>
String concat(Ts... args)
{
std::stringstream out;
ByteStream out;
((out << args), ...);
return(out.str());
}
+135 -27
View File
@@ -101,42 +101,117 @@ String encode_query(StringMap map)
return(result);
}
String trim_wrapping_quotes(String raw)
{
if(raw.length() >= 2 && raw[0] == '"' && raw[raw.length()-1] == '"')
return(raw.substr(1, raw.length()-2));
return(raw);
}
void parse_multipart_content_disposition(
String raw,
String& disposition_type,
String& field_name,
String& file_name)
{
auto parts = split(raw, ";");
if(parts.size() == 0)
return;
disposition_type = to_lower(trim(parts[0]));
for(u32 i = 1; i < parts.size(); i++)
{
String part = trim(parts[i]);
String key = to_lower(trim(nibble(part, "=")));
String value = trim_wrapping_quotes(trim(part));
if(key == "name")
field_name = value;
else if(key == "filename")
file_name = value;
}
}
String make_upload_tmp_name()
{
String upload_path = context->server->config["TMP_UPLOAD_PATH"];
if(upload_path.length() > 0 && upload_path[upload_path.length()-1] != '/')
upload_path.append(1, '/');
return(upload_path + make_session_id());
}
StringMap parse_multipart(String q, String boundary, std::vector<UploadedFile>& uploaded_files)
{
StringMap result;
auto i = boundary.length();
if(boundary == "")
return(result);
u64 i = 0;
while(i < q.length())
{
auto end_pos = q.find(boundary, i);
if(end_pos != String::npos)
auto start_pos = q.find(boundary, i);
if(start_pos == String::npos)
break;
start_pos += boundary.length();
if(q.substr(start_pos, 2) == "--")
break;
if(q.substr(start_pos, 2) == "\r\n")
start_pos += 2;
auto header_end = q.find("\r\n\r\n", start_pos);
if(header_end == String::npos)
break;
String header_block = q.substr(start_pos, header_end - start_pos);
auto end_pos = q.find(boundary, header_end + 4);
if(end_pos == String::npos)
break;
String body = q.substr(header_end + 4, end_pos - (header_end + 4));
if(body.length() >= 2 && body.substr(body.length()-2) == "\r\n")
body.resize(body.length()-2);
String disposition_type;
String field_name;
String file_name;
for(auto header_line : split(header_block, "\r\n"))
{
String field = q.substr(i, end_pos - i);
nibble(":", field);
String ftype = trim(nibble(";", field));
if(ftype == "form-data")
String header_name = to_lower(trim(nibble(header_line, ":")));
String header_value = trim(header_line);
if(header_name == "content-disposition")
{
nibble("=\"", field);
String field_name = nibble("\"", field);
result[field_name] = field.substr(4, field.length()-6);
parse_multipart_content_disposition(
header_value,
disposition_type,
field_name,
file_name
);
}
else if(ftype == "attachment")
}
if(field_name != "")
{
bool is_uploaded_file =
(disposition_type == "form-data" || disposition_type == "attachment") &&
file_name != "";
if(is_uploaded_file)
{
nibble("=\"", field);
UploadedFile f;
f.tmp_name = context->server->config["TMP_UPLOAD_PATH"] + std::to_string(rand());
f.file_name = nibble("\"", field);
String bin = field.substr(4, field.length()-6);
f.size = bin.length();
file_put_contents(f.tmp_name, bin);
f.tmp_name = make_upload_tmp_name();
f.file_name = file_name;
f.size = body.length();
file_put_contents(f.tmp_name, body);
uploaded_files.push_back(f);
result[field_name] = file_name;
}
else
{
result[field_name] = body;
}
}
else
{
// we're done
end_pos = q.length();
}
i = end_pos + boundary.length();
}
@@ -287,23 +362,56 @@ String make_session_id()
return(to_hex(rand())+to_hex(rand())+to_hex(rand())+to_hex(rand()));
}
bool is_valid_session_id(String session_id)
{
if(session_id.length() < 16 || session_id.length() > 128)
return(false);
for(auto c : session_id)
{
if(!isxdigit(c))
return(false);
}
return(true);
}
String session_file_path(String session_id)
{
if(!is_valid_session_id(session_id))
return("");
return(context->server->config["SESSION_PATH"] + "/" + session_id);
}
StringMap load_session_data(String session_id)
{
return(parse_query(file_get_contents(context->server->config["SESSION_PATH"] + "/" + session_id)));
String session_path = session_file_path(session_id);
if(session_path == "")
return(StringMap());
return(parse_query(file_get_contents(session_path)));
}
void save_session_data(String session_id, StringMap data)
{
file_put_contents(context->server->config["SESSION_PATH"] + "/" + session_id, encode_query(data));
String session_path = session_file_path(session_id);
if(session_path == "")
{
printf("(!) Refusing to save invalid session id\n");
return;
}
file_put_contents(session_path, encode_query(data));
}
String session_start(String session_name)
{
if(context->cookies[session_name].length() == 0)
String session_id = context->cookies[session_name];
if(!is_valid_session_id(session_id))
session_id = "";
if(session_id.length() == 0)
{
set_cookie(session_name, make_session_id(), time() + int_val(context->server->config["SESSION_TIME"]));
session_id = make_session_id();
set_cookie(session_name, session_id, time() + int_val(context->server->config["SESSION_TIME"]));
}
context->session_id = context->cookies[session_name];
context->session_id = session_id;
context->session_name = session_name;
context->session = load_session_data(context->session_id);
return(context->session_id);
+66
View File
@@ -17,3 +17,69 @@ StringMap load_session_data(String session_id);
void save_session_data(String session_id, StringMap data);
String session_start(String session_name = "uce-session");
void session_destroy(String session_name = "uce-session");
struct WSFrame {
u8 opcode;
u8 is_final_fragment;
u8 mask_bit;
u64 payload_length;
u64 offset;
u8* payload;
WSFrame(String& bufstr)
{
const char* buffer = bufstr.c_str();
u64 buffer_size = bufstr.size();
opcode = buffer[0] & 0x0F;
is_final_fragment = buffer[0] & 0x80;
mask_bit = buffer[1] & 0x80;
payload_length = buffer[1] & 0x7F;
offset = 2;
if (payload_length == 126) {
payload_length = ((u64) buffer[2] << 8) | buffer[3];
offset = 4;
} else if (payload_length == 127) {
payload_length = ((u64) buffer[2] << 56) |
((u64) buffer[3] << 48) |
((u64) buffer[4] << 40) |
((u64) buffer[5] << 32) |
((u64) buffer[6] << 24) |
((u64) buffer[7] << 16) |
((u64) buffer[8] << 8) |
buffer[9];
offset = 10;
}
uint8_t* maskingKey = nullptr;
if (mask_bit) {
maskingKey = reinterpret_cast<uint8_t*>(const_cast<char*>(buffer + offset));
offset += 4;
}
// Parse the frame payload
payload = reinterpret_cast<uint8_t*>(const_cast<char*>(buffer + offset));
if (mask_bit) {
for (int i = 0; i < payload_length; i++) {
payload[i] = payload[i] ^ maskingKey[i % 4];
}
}
// Print the parsed frame
std::cout << "Parsed WebSocket frame:" << std::endl;
std::cout << " Opcode: " << (int) opcode << std::endl;
std::cout << " Is final fragment: " << (is_final_fragment ? "true" : "false") << std::endl;
std::cout << " Payload length: " << payload_length << std::endl;
if (mask_bit) {
std::cout << " Masking key: ";
for (int i = 0; i < 4; i++) {
std::cout << std::hex << (int) maskingKey[i] << " ";
}
std::cout << std::endl;
}
std::cout << " Payload: " << std::endl << std::string((const char*) payload, payload_length) << std::endl;
}
};