2025-09-25 02:35:00 +02:00
|
|
|
/*
|
|
|
|
|
* =====================================================================================
|
|
|
|
|
*
|
|
|
|
|
* Filename: proxy.c
|
|
|
|
|
*
|
|
|
|
|
* Description: High-performance Reverse Proxy with Real-time Monitoring Dashboard.
|
|
|
|
|
* Single-file C implementation using epoll - PRODUCTION READY VERSION
|
|
|
|
|
*
|
2025-09-25 05:21:40 +02:00
|
|
|
* Version: 3.0 (Enhanced Reliability & Complete Stats)
|
2025-09-25 02:35:00 +02:00
|
|
|
* Created: 2024
|
|
|
|
|
* Compiler: gcc
|
|
|
|
|
*
|
2025-09-25 05:21:40 +02:00
|
|
|
* Author: Production Ready Version
|
2025-09-25 02:35:00 +02:00
|
|
|
*
|
|
|
|
|
* To Compile:
|
|
|
|
|
* gcc -Wall -Wextra -O2 -g -pthread cJSON.c proxy.c -o rproxy -lssl -lcrypto -lsqlite3 -lm
|
|
|
|
|
*
|
|
|
|
|
* =====================================================================================
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Standard Library Includes ---
|
|
|
|
|
#define _GNU_SOURCE
|
2025-09-25 23:59:27 +02:00
|
|
|
#include <stdbool.h>
|
2025-09-25 02:35:00 +02:00
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
#include <sys/epoll.h>
|
|
|
|
|
#include <netinet/in.h>
|
2025-09-25 05:21:40 +02:00
|
|
|
#include <netinet/tcp.h>
|
2025-09-25 02:35:00 +02:00
|
|
|
#include <sqlite3.h>
|
|
|
|
|
#include <openssl/ssl.h>
|
|
|
|
|
#include <openssl/err.h>
|
|
|
|
|
#include <stdarg.h>
|
|
|
|
|
#include <signal.h>
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
#include <sys/socket.h>
|
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include <sys/sysinfo.h>
|
|
|
|
|
#include <math.h>
|
|
|
|
|
#include <ctype.h>
|
|
|
|
|
#include <netdb.h>
|
2025-09-25 05:21:40 +02:00
|
|
|
#include <sys/statvfs.h>
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
// --- Local Includes ---
|
|
|
|
|
#include "cJSON.h"
|
|
|
|
|
|
|
|
|
|
// --- Constants ---
|
2025-09-25 23:59:27 +02:00
|
|
|
#define MAX_EVENTS 4096
|
2025-09-25 02:35:00 +02:00
|
|
|
#define MAX_FDS 65536
|
|
|
|
|
#define CHUNK_SIZE 65536
|
|
|
|
|
#define HISTORY_SECONDS 300
|
|
|
|
|
#define MAX_HEADER_SIZE 8192
|
|
|
|
|
#define MAX_REQUEST_LINE_SIZE 4096
|
|
|
|
|
#define MAX_URI_SIZE 2048
|
2025-09-25 05:21:40 +02:00
|
|
|
#define CONNECTION_TIMEOUT 300
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
// --- Enums and Structs ---
|
|
|
|
|
|
|
|
|
|
typedef enum {
|
|
|
|
|
CONN_TYPE_UNUSED,
|
|
|
|
|
CONN_TYPE_LISTENER,
|
|
|
|
|
CONN_TYPE_CLIENT,
|
|
|
|
|
CONN_TYPE_UPSTREAM
|
|
|
|
|
} conn_type_t;
|
|
|
|
|
|
|
|
|
|
typedef enum {
|
2025-09-25 05:11:52 +02:00
|
|
|
CLIENT_STATE_READING_HEADERS, // Reading the initial HTTP request
|
|
|
|
|
CLIENT_STATE_FORWARDING, // Actively forwarding data in both directions
|
2025-09-25 05:21:40 +02:00
|
|
|
CLIENT_STATE_SERVING_INTERNAL, // Serving dashboard or stats
|
|
|
|
|
CLIENT_STATE_ERROR, // An error occurred, connection will be closed
|
|
|
|
|
CLIENT_STATE_CLOSING // Connection is being closed
|
2025-09-25 02:35:00 +02:00
|
|
|
} client_state_t;
|
|
|
|
|
|
|
|
|
|
struct connection_s;
|
|
|
|
|
struct vhost_stats_s;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
char hostname[256];
|
|
|
|
|
char upstream_host[256];
|
|
|
|
|
int upstream_port;
|
|
|
|
|
int use_ssl;
|
|
|
|
|
int rewrite_host;
|
|
|
|
|
} route_config_t;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
int port;
|
|
|
|
|
route_config_t *routes;
|
|
|
|
|
int route_count;
|
|
|
|
|
} app_config_t;
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
// High-performance buffer using head/tail pointers to avoid memmove
|
2025-09-25 02:35:00 +02:00
|
|
|
typedef struct {
|
|
|
|
|
char *data;
|
|
|
|
|
size_t capacity;
|
2025-09-25 05:11:52 +02:00
|
|
|
size_t head; // Read position
|
|
|
|
|
size_t tail; // Write position
|
2025-09-25 02:35:00 +02:00
|
|
|
} buffer_t;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
char method[16];
|
|
|
|
|
char uri[MAX_URI_SIZE];
|
|
|
|
|
char version[16];
|
|
|
|
|
char host[256];
|
2025-09-25 05:11:52 +02:00
|
|
|
long content_length;
|
2025-09-25 02:35:00 +02:00
|
|
|
int is_websocket;
|
|
|
|
|
int keep_alive;
|
2025-09-25 05:21:40 +02:00
|
|
|
int connection_close;
|
2025-09-25 23:59:27 +02:00
|
|
|
bool is_chunked;
|
2025-09-25 02:35:00 +02:00
|
|
|
} http_request_t;
|
|
|
|
|
|
|
|
|
|
typedef struct connection_s {
|
|
|
|
|
conn_type_t type;
|
|
|
|
|
client_state_t state;
|
|
|
|
|
int fd;
|
|
|
|
|
struct connection_s *pair;
|
|
|
|
|
struct vhost_stats_s *vhost_stats;
|
|
|
|
|
buffer_t read_buf;
|
|
|
|
|
buffer_t write_buf;
|
|
|
|
|
SSL *ssl;
|
|
|
|
|
int ssl_handshake_done;
|
|
|
|
|
http_request_t request;
|
|
|
|
|
double request_start_time;
|
|
|
|
|
time_t last_activity;
|
2025-09-25 05:21:40 +02:00
|
|
|
int half_closed; // For handling connection shutdown
|
|
|
|
|
int write_shutdown; // Write side shutdown
|
2025-09-25 02:35:00 +02:00
|
|
|
} connection_t;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
double time;
|
|
|
|
|
double value;
|
|
|
|
|
} history_point_t;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
history_point_t *points;
|
|
|
|
|
int capacity;
|
|
|
|
|
int head;
|
|
|
|
|
int count;
|
|
|
|
|
} history_deque_t;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
double time;
|
|
|
|
|
double rx_kbps;
|
|
|
|
|
double tx_kbps;
|
|
|
|
|
} network_history_point_t;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
network_history_point_t *points;
|
|
|
|
|
int capacity;
|
|
|
|
|
int head;
|
|
|
|
|
int count;
|
|
|
|
|
} network_history_deque_t;
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
typedef struct {
|
|
|
|
|
double time;
|
|
|
|
|
double read_mbps;
|
|
|
|
|
double write_mbps;
|
|
|
|
|
} disk_history_point_t;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
disk_history_point_t *points;
|
|
|
|
|
int capacity;
|
|
|
|
|
int head;
|
|
|
|
|
int count;
|
|
|
|
|
} disk_history_deque_t;
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
typedef struct request_time_s {
|
|
|
|
|
double *times;
|
|
|
|
|
int capacity;
|
|
|
|
|
int head;
|
|
|
|
|
int count;
|
|
|
|
|
} request_time_deque_t;
|
|
|
|
|
|
|
|
|
|
typedef struct vhost_stats_s {
|
|
|
|
|
char vhost_name[256];
|
|
|
|
|
long long http_requests;
|
|
|
|
|
long long websocket_requests;
|
|
|
|
|
long long total_requests;
|
|
|
|
|
long long bytes_sent;
|
|
|
|
|
long long bytes_recv;
|
|
|
|
|
double avg_request_time_ms;
|
|
|
|
|
long long last_bytes_sent;
|
|
|
|
|
long long last_bytes_recv;
|
|
|
|
|
double last_update;
|
|
|
|
|
history_deque_t throughput_history;
|
|
|
|
|
request_time_deque_t request_times;
|
|
|
|
|
struct vhost_stats_s *next;
|
|
|
|
|
} vhost_stats_t;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
time_t start_time;
|
|
|
|
|
int active_connections;
|
|
|
|
|
history_deque_t cpu_history;
|
|
|
|
|
history_deque_t memory_history;
|
|
|
|
|
network_history_deque_t network_history;
|
2025-09-25 05:21:40 +02:00
|
|
|
disk_history_deque_t disk_history;
|
2025-09-25 02:35:00 +02:00
|
|
|
history_deque_t throughput_history;
|
2025-09-25 05:21:40 +02:00
|
|
|
history_deque_t load1_history;
|
|
|
|
|
history_deque_t load5_history;
|
|
|
|
|
history_deque_t load15_history;
|
2025-09-25 02:35:00 +02:00
|
|
|
long long last_net_sent;
|
|
|
|
|
long long last_net_recv;
|
2025-09-25 05:21:40 +02:00
|
|
|
long long last_disk_read;
|
|
|
|
|
long long last_disk_write;
|
2025-09-25 02:35:00 +02:00
|
|
|
double last_net_update_time;
|
2025-09-25 05:21:40 +02:00
|
|
|
double last_disk_update_time;
|
2025-09-25 02:35:00 +02:00
|
|
|
vhost_stats_t *vhost_stats_head;
|
|
|
|
|
sqlite3 *db;
|
|
|
|
|
} system_monitor_t;
|
|
|
|
|
|
|
|
|
|
// --- Global State ---
|
|
|
|
|
connection_t connections[MAX_FDS];
|
|
|
|
|
app_config_t config;
|
|
|
|
|
system_monitor_t monitor;
|
|
|
|
|
int epoll_fd;
|
|
|
|
|
SSL_CTX *ssl_ctx;
|
|
|
|
|
static int g_debug_mode = 0;
|
2025-09-25 05:21:40 +02:00
|
|
|
static volatile int g_shutdown = 0;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Function Prototypes ---
|
|
|
|
|
void log_error(const char *msg);
|
|
|
|
|
void log_info(const char *format, ...);
|
|
|
|
|
void log_debug(const char *format, ...);
|
|
|
|
|
void setup_listener_socket(int port);
|
|
|
|
|
void accept_new_connection(int listener_fd);
|
|
|
|
|
void close_connection(int fd);
|
|
|
|
|
void handle_connection_event(struct epoll_event *event);
|
2025-09-25 05:11:52 +02:00
|
|
|
void connect_to_upstream(connection_t *client_conn, const char *data, size_t data_len);
|
|
|
|
|
void handle_client_read(connection_t *conn);
|
|
|
|
|
int parse_http_request(const char *data, size_t len, http_request_t *req);
|
2025-09-25 02:35:00 +02:00
|
|
|
void monitor_init(const char *db_file);
|
2025-09-25 05:11:52 +02:00
|
|
|
void free_config();
|
2025-09-25 02:35:00 +02:00
|
|
|
void monitor_update();
|
|
|
|
|
void monitor_cleanup();
|
|
|
|
|
void monitor_record_request_start(vhost_stats_t *stats, int is_websocket);
|
|
|
|
|
void monitor_record_request_end(vhost_stats_t *stats, double start_time);
|
|
|
|
|
vhost_stats_t* monitor_get_or_create_vhost_stats(const char *vhost_name);
|
|
|
|
|
void serve_dashboard(connection_t *conn);
|
|
|
|
|
void serve_stats_api(connection_t *conn);
|
|
|
|
|
void send_error_response(connection_t *conn, int code, const char* status, const char* body);
|
2025-09-25 05:21:40 +02:00
|
|
|
static void signal_handler(int sig);
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Buffer Helpers ---
|
|
|
|
|
static inline int buffer_init(buffer_t *buf, size_t capacity) {
|
|
|
|
|
buf->data = malloc(capacity);
|
|
|
|
|
if (!buf->data) {
|
|
|
|
|
log_error("Failed to allocate buffer");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
buf->capacity = capacity;
|
2025-09-25 05:11:52 +02:00
|
|
|
buf->head = 0;
|
|
|
|
|
buf->tail = 0;
|
2025-09-25 02:35:00 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static inline void buffer_free(buffer_t *buf) {
|
|
|
|
|
if (buf->data) {
|
|
|
|
|
free(buf->data);
|
|
|
|
|
buf->data = NULL;
|
|
|
|
|
}
|
|
|
|
|
buf->capacity = 0;
|
2025-09-25 05:11:52 +02:00
|
|
|
buf->head = 0;
|
|
|
|
|
buf->tail = 0;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
static inline size_t buffer_available_read(buffer_t *buf) {
|
|
|
|
|
return buf->tail - buf->head;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static inline size_t buffer_available_write(buffer_t *buf) {
|
|
|
|
|
return buf->capacity - buf->tail;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
static inline int buffer_ensure_capacity(buffer_t *buf, size_t required) {
|
|
|
|
|
if (buf->capacity >= required) return 0;
|
|
|
|
|
|
|
|
|
|
size_t new_capacity = buf->capacity;
|
|
|
|
|
while (new_capacity < required) {
|
|
|
|
|
new_capacity *= 2;
|
2025-09-25 05:21:40 +02:00
|
|
|
if (new_capacity > 1024*1024*10) { // 10MB max
|
|
|
|
|
log_error("Buffer size limit exceeded");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char *new_data = realloc(buf->data, new_capacity);
|
|
|
|
|
if (!new_data) {
|
|
|
|
|
log_error("Failed to reallocate buffer");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
buf->data = new_data;
|
|
|
|
|
buf->capacity = new_capacity;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
// Compacts the buffer by moving unread data to the beginning
|
|
|
|
|
static inline void buffer_compact(buffer_t *buf) {
|
|
|
|
|
if (buf->head == 0) return;
|
|
|
|
|
size_t len = buf->tail - buf->head;
|
|
|
|
|
if (len > 0) {
|
|
|
|
|
memmove(buf->data, buf->data + buf->head, len);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
buf->head = 0;
|
|
|
|
|
buf->tail = len;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
static inline void buffer_consume(buffer_t *buf, size_t bytes) {
|
|
|
|
|
buf->head += bytes;
|
|
|
|
|
if (buf->head >= buf->tail) {
|
|
|
|
|
buf->head = 0;
|
|
|
|
|
buf->tail = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
// --- String Utilities ---
|
2025-09-25 02:35:00 +02:00
|
|
|
static int is_valid_http_method(const char *method) {
|
|
|
|
|
const char *valid_methods[] = {"GET", "POST", "PUT", "DELETE", "HEAD",
|
|
|
|
|
"OPTIONS", "PATCH", "TRACE", "CONNECT", NULL};
|
|
|
|
|
for (int i = 0; valid_methods[i]; i++) {
|
|
|
|
|
if (strcmp(method, valid_methods[i]) == 0) return 1;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
//
|
2025-09-25 05:11:52 +02:00
|
|
|
// Logging Implementation
|
2025-09-25 02:35:00 +02:00
|
|
|
//
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
|
|
|
|
|
void log_error(const char *msg) {
|
|
|
|
|
perror(msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void log_message(const char *level, const char *format, va_list args) {
|
|
|
|
|
time_t now;
|
|
|
|
|
time(&now);
|
|
|
|
|
char buf[32];
|
|
|
|
|
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", localtime(&now));
|
|
|
|
|
printf("%s - %-5s - ", buf, level);
|
|
|
|
|
vprintf(format, args);
|
|
|
|
|
printf("\n");
|
|
|
|
|
fflush(stdout);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void log_info(const char *format, ...) {
|
|
|
|
|
va_list args;
|
|
|
|
|
va_start(args, format);
|
|
|
|
|
log_message("INFO", format, args);
|
|
|
|
|
va_end(args);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void log_debug(const char *format, ...) {
|
|
|
|
|
if (!g_debug_mode) return;
|
|
|
|
|
va_list args;
|
|
|
|
|
va_start(args, format);
|
|
|
|
|
log_message("DEBUG", format, args);
|
|
|
|
|
va_end(args);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
//
|
2025-09-25 05:11:52 +02:00
|
|
|
// Configuration Implementation
|
2025-09-25 02:35:00 +02:00
|
|
|
//
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
|
|
|
|
|
static char* read_file_to_string(const char *filename) {
|
|
|
|
|
FILE *f = fopen(filename, "rb");
|
|
|
|
|
if (!f) return NULL;
|
|
|
|
|
|
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
|
|
|
long length = ftell(f);
|
|
|
|
|
if (length < 0 || length > 1024*1024) { // Max 1MB config
|
|
|
|
|
fclose(f);
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
|
char *buffer = malloc(length + 1);
|
|
|
|
|
if (buffer) {
|
|
|
|
|
size_t read_len = fread(buffer, 1, length, f);
|
|
|
|
|
buffer[read_len] = '\0';
|
|
|
|
|
}
|
|
|
|
|
fclose(f);
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int load_config(const char *filename) {
|
|
|
|
|
log_info("Loading configuration from %s", filename);
|
|
|
|
|
char *json_string = read_file_to_string(filename);
|
|
|
|
|
if (!json_string) {
|
|
|
|
|
log_error("Could not read config file");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON *root = cJSON_Parse(json_string);
|
|
|
|
|
free(json_string);
|
|
|
|
|
if (!root) {
|
|
|
|
|
fprintf(stderr, "JSON parse error: %s\n", cJSON_GetErrorPtr());
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON *port_item = cJSON_GetObjectItem(root, "port");
|
|
|
|
|
config.port = cJSON_IsNumber(port_item) ? port_item->valueint : 8080;
|
|
|
|
|
|
|
|
|
|
if (config.port < 1 || config.port > 65535) {
|
|
|
|
|
fprintf(stderr, "Invalid port number: %d\n", config.port);
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON *proxy_array = cJSON_GetObjectItem(root, "reverse_proxy");
|
|
|
|
|
if (cJSON_IsArray(proxy_array)) {
|
|
|
|
|
config.route_count = cJSON_GetArraySize(proxy_array);
|
|
|
|
|
if (config.route_count <= 0) {
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
config.routes = calloc(config.route_count, sizeof(route_config_t));
|
2025-09-25 02:35:00 +02:00
|
|
|
if (!config.routes) {
|
|
|
|
|
log_error("Failed to allocate memory for routes");
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int i = 0;
|
|
|
|
|
cJSON *route_item;
|
|
|
|
|
cJSON_ArrayForEach(route_item, proxy_array) {
|
|
|
|
|
route_config_t *route = &config.routes[i];
|
|
|
|
|
|
|
|
|
|
cJSON *hostname = cJSON_GetObjectItem(route_item, "hostname");
|
|
|
|
|
cJSON *upstream_host = cJSON_GetObjectItem(route_item, "upstream_host");
|
|
|
|
|
cJSON *upstream_port = cJSON_GetObjectItem(route_item, "upstream_port");
|
|
|
|
|
|
|
|
|
|
if (!cJSON_IsString(hostname) || !cJSON_IsString(upstream_host) || !cJSON_IsNumber(upstream_port)) {
|
|
|
|
|
fprintf(stderr, "Invalid route configuration at index %d\n", i);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
strncpy(route->hostname, hostname->valuestring, sizeof(route->hostname) - 1);
|
|
|
|
|
strncpy(route->upstream_host, upstream_host->valuestring, sizeof(route->upstream_host) - 1);
|
|
|
|
|
route->upstream_port = upstream_port->valueint;
|
|
|
|
|
|
|
|
|
|
if (route->upstream_port < 1 || route->upstream_port > 65535) {
|
|
|
|
|
fprintf(stderr, "Invalid upstream port for %s: %d\n", route->hostname, route->upstream_port);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
route->use_ssl = cJSON_IsTrue(cJSON_GetObjectItem(route_item, "use_ssl"));
|
|
|
|
|
route->rewrite_host = cJSON_IsTrue(cJSON_GetObjectItem(route_item, "rewrite_host"));
|
|
|
|
|
|
|
|
|
|
log_info("Route configured: %s -> %s:%d (SSL: %s, Rewrite Host: %s)",
|
|
|
|
|
route->hostname, route->upstream_host, route->upstream_port,
|
|
|
|
|
route->use_ssl ? "yes" : "no", route->rewrite_host ? "yes" : "no");
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
log_info("Loaded %d routes from %s", config.route_count, filename);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
route_config_t *find_route(const char *hostname) {
|
|
|
|
|
if (!hostname) return NULL;
|
|
|
|
|
for (int i = 0; i < config.route_count; i++) {
|
|
|
|
|
if (strcasecmp(hostname, config.routes[i].hostname) == 0) {
|
|
|
|
|
return &config.routes[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
//
|
2025-09-25 05:11:52 +02:00
|
|
|
// Monitor Implementation
|
2025-09-25 02:35:00 +02:00
|
|
|
//
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
static void history_deque_init(history_deque_t *dq, int capacity) {
|
2025-09-25 05:21:40 +02:00
|
|
|
dq->points = calloc(capacity, sizeof(history_point_t));
|
2025-09-25 02:35:00 +02:00
|
|
|
dq->capacity = capacity;
|
|
|
|
|
dq->head = 0;
|
|
|
|
|
dq->count = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void history_deque_push(history_deque_t *dq, double time, double value) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (!dq || !dq->points) return;
|
2025-09-25 02:35:00 +02:00
|
|
|
dq->points[dq->head] = (history_point_t){ .time = time, .value = value };
|
|
|
|
|
dq->head = (dq->head + 1) % dq->capacity;
|
|
|
|
|
if (dq->count < dq->capacity) dq->count++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void network_history_deque_init(network_history_deque_t *dq, int capacity) {
|
2025-09-25 05:21:40 +02:00
|
|
|
dq->points = calloc(capacity, sizeof(network_history_point_t));
|
2025-09-25 02:35:00 +02:00
|
|
|
dq->capacity = capacity;
|
|
|
|
|
dq->head = 0;
|
|
|
|
|
dq->count = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void network_history_deque_push(network_history_deque_t *dq, double time, double rx, double tx) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (!dq || !dq->points) return;
|
2025-09-25 02:35:00 +02:00
|
|
|
dq->points[dq->head] = (network_history_point_t){ .time = time, .rx_kbps = rx, .tx_kbps = tx };
|
|
|
|
|
dq->head = (dq->head + 1) % dq->capacity;
|
|
|
|
|
if (dq->count < dq->capacity) dq->count++;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
static void disk_history_deque_init(disk_history_deque_t *dq, int capacity) {
|
|
|
|
|
dq->points = calloc(capacity, sizeof(disk_history_point_t));
|
|
|
|
|
dq->capacity = capacity;
|
|
|
|
|
dq->head = 0;
|
|
|
|
|
dq->count = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void disk_history_deque_push(disk_history_deque_t *dq, double time, double read_mbps, double write_mbps) {
|
|
|
|
|
if (!dq || !dq->points) return;
|
|
|
|
|
dq->points[dq->head] = (disk_history_point_t){ .time = time, .read_mbps = read_mbps, .write_mbps = write_mbps };
|
|
|
|
|
dq->head = (dq->head + 1) % dq->capacity;
|
|
|
|
|
if (dq->count < dq->capacity) dq->count++;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
static void request_time_deque_init(request_time_deque_t *dq, int capacity) {
|
2025-09-25 05:21:40 +02:00
|
|
|
dq->times = calloc(capacity, sizeof(double));
|
2025-09-25 02:35:00 +02:00
|
|
|
dq->capacity = capacity;
|
|
|
|
|
dq->head = 0;
|
|
|
|
|
dq->count = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void request_time_deque_push(request_time_deque_t *dq, double time_ms) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (!dq || !dq->times) return;
|
2025-09-25 02:35:00 +02:00
|
|
|
dq->times[dq->head] = time_ms;
|
|
|
|
|
dq->head = (dq->head + 1) % dq->capacity;
|
|
|
|
|
if (dq->count < dq->capacity) dq->count++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void init_db() {
|
|
|
|
|
if (!monitor.db) return;
|
|
|
|
|
|
|
|
|
|
char *err_msg = 0;
|
|
|
|
|
const char *sql_create_table =
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS vhost_stats ("
|
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
|
|
|
" vhost TEXT NOT NULL,"
|
|
|
|
|
" timestamp REAL NOT NULL,"
|
|
|
|
|
" http_requests INTEGER DEFAULT 0,"
|
|
|
|
|
" websocket_requests INTEGER DEFAULT 0,"
|
|
|
|
|
" total_requests INTEGER DEFAULT 0,"
|
|
|
|
|
" bytes_sent INTEGER DEFAULT 0,"
|
|
|
|
|
" bytes_recv INTEGER DEFAULT 0,"
|
|
|
|
|
" avg_request_time_ms REAL DEFAULT 0,"
|
|
|
|
|
" UNIQUE(vhost, timestamp)"
|
|
|
|
|
");";
|
|
|
|
|
const char *sql_create_index =
|
|
|
|
|
"CREATE INDEX IF NOT EXISTS idx_vhost_timestamp ON vhost_stats(vhost, timestamp);";
|
|
|
|
|
|
|
|
|
|
if (sqlite3_exec(monitor.db, sql_create_table, 0, 0, &err_msg) != SQLITE_OK ||
|
|
|
|
|
sqlite3_exec(monitor.db, sql_create_index, 0, 0, &err_msg) != SQLITE_OK) {
|
|
|
|
|
fprintf(stderr, "SQL error: %s\n", err_msg);
|
|
|
|
|
sqlite3_free(err_msg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void load_stats_from_db() {
|
|
|
|
|
if (!monitor.db) return;
|
|
|
|
|
|
|
|
|
|
sqlite3_stmt *res;
|
|
|
|
|
const char *sql =
|
|
|
|
|
"SELECT vhost, http_requests, websocket_requests, total_requests, "
|
|
|
|
|
"bytes_sent, bytes_recv, avg_request_time_ms "
|
|
|
|
|
"FROM vhost_stats v1 WHERE timestamp = ("
|
|
|
|
|
" SELECT MAX(timestamp) FROM vhost_stats v2 WHERE v2.vhost = v1.vhost"
|
|
|
|
|
")";
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(monitor.db, sql, -1, &res, 0) != SQLITE_OK) {
|
|
|
|
|
fprintf(stderr, "Failed to execute statement: %s\n", sqlite3_errmsg(monitor.db));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int vhost_count = 0;
|
|
|
|
|
while (sqlite3_step(res) == SQLITE_ROW) {
|
|
|
|
|
vhost_stats_t *stats = monitor_get_or_create_vhost_stats((const char*)sqlite3_column_text(res, 0));
|
|
|
|
|
if (stats) {
|
|
|
|
|
stats->http_requests = sqlite3_column_int64(res, 1);
|
|
|
|
|
stats->websocket_requests = sqlite3_column_int64(res, 2);
|
|
|
|
|
stats->total_requests = sqlite3_column_int64(res, 3);
|
|
|
|
|
stats->bytes_sent = sqlite3_column_int64(res, 4);
|
|
|
|
|
stats->bytes_recv = sqlite3_column_int64(res, 5);
|
|
|
|
|
stats->avg_request_time_ms = sqlite3_column_double(res, 6);
|
|
|
|
|
vhost_count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(res);
|
|
|
|
|
log_info("Loaded statistics for %d vhosts from database", vhost_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void monitor_init(const char *db_file) {
|
|
|
|
|
memset(&monitor, 0, sizeof(system_monitor_t));
|
|
|
|
|
monitor.start_time = time(NULL);
|
|
|
|
|
|
|
|
|
|
history_deque_init(&monitor.cpu_history, HISTORY_SECONDS);
|
|
|
|
|
history_deque_init(&monitor.memory_history, HISTORY_SECONDS);
|
|
|
|
|
network_history_deque_init(&monitor.network_history, HISTORY_SECONDS);
|
2025-09-25 05:21:40 +02:00
|
|
|
disk_history_deque_init(&monitor.disk_history, HISTORY_SECONDS);
|
2025-09-25 02:35:00 +02:00
|
|
|
history_deque_init(&monitor.throughput_history, HISTORY_SECONDS);
|
2025-09-25 05:21:40 +02:00
|
|
|
history_deque_init(&monitor.load1_history, HISTORY_SECONDS);
|
|
|
|
|
history_deque_init(&monitor.load5_history, HISTORY_SECONDS);
|
|
|
|
|
history_deque_init(&monitor.load15_history, HISTORY_SECONDS);
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
if (sqlite3_open(db_file, &monitor.db) != SQLITE_OK) {
|
|
|
|
|
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(monitor.db));
|
|
|
|
|
if (monitor.db) {
|
|
|
|
|
sqlite3_close(monitor.db);
|
|
|
|
|
monitor.db = NULL;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
init_db();
|
|
|
|
|
load_stats_from_db();
|
|
|
|
|
}
|
|
|
|
|
monitor_update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void monitor_cleanup() {
|
|
|
|
|
if (monitor.db) {
|
|
|
|
|
sqlite3_close(monitor.db);
|
|
|
|
|
monitor.db = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vhost_stats_t *current = monitor.vhost_stats_head;
|
|
|
|
|
while (current) {
|
|
|
|
|
vhost_stats_t *next = current->next;
|
|
|
|
|
if (current->throughput_history.points) free(current->throughput_history.points);
|
|
|
|
|
if (current->request_times.times) free(current->request_times.times);
|
|
|
|
|
free(current);
|
|
|
|
|
current = next;
|
|
|
|
|
}
|
|
|
|
|
monitor.vhost_stats_head = NULL;
|
|
|
|
|
|
|
|
|
|
if (monitor.cpu_history.points) free(monitor.cpu_history.points);
|
|
|
|
|
if (monitor.memory_history.points) free(monitor.memory_history.points);
|
|
|
|
|
if (monitor.network_history.points) free(monitor.network_history.points);
|
2025-09-25 05:21:40 +02:00
|
|
|
if (monitor.disk_history.points) free(monitor.disk_history.points);
|
2025-09-25 02:35:00 +02:00
|
|
|
if (monitor.throughput_history.points) free(monitor.throughput_history.points);
|
2025-09-25 05:21:40 +02:00
|
|
|
if (monitor.load1_history.points) free(monitor.load1_history.points);
|
|
|
|
|
if (monitor.load5_history.points) free(monitor.load5_history.points);
|
|
|
|
|
if (monitor.load15_history.points) free(monitor.load15_history.points);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void save_stats_to_db() {
|
|
|
|
|
if (!monitor.db) return;
|
|
|
|
|
|
|
|
|
|
sqlite3_stmt *stmt;
|
|
|
|
|
const char *sql =
|
|
|
|
|
"INSERT OR REPLACE INTO vhost_stats "
|
|
|
|
|
"(vhost, timestamp, http_requests, websocket_requests, total_requests, "
|
|
|
|
|
"bytes_sent, bytes_recv, avg_request_time_ms) "
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
|
|
|
|
|
|
|
|
|
|
if (sqlite3_prepare_v2(monitor.db, sql, -1, &stmt, NULL) != SQLITE_OK) return;
|
|
|
|
|
|
|
|
|
|
double current_time = (double)time(NULL);
|
|
|
|
|
for (vhost_stats_t *s = monitor.vhost_stats_head; s != NULL; s = s->next) {
|
|
|
|
|
if (s->request_times.count > 0) {
|
|
|
|
|
double total_time = 0;
|
|
|
|
|
for(int i = 0; i < s->request_times.count; i++) {
|
|
|
|
|
total_time += s->request_times.times[i];
|
|
|
|
|
}
|
|
|
|
|
s->avg_request_time_ms = total_time / s->request_times.count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sqlite3_bind_text(stmt, 1, s->vhost_name, -1, SQLITE_STATIC);
|
|
|
|
|
sqlite3_bind_double(stmt, 2, current_time);
|
|
|
|
|
sqlite3_bind_int64(stmt, 3, s->http_requests);
|
|
|
|
|
sqlite3_bind_int64(stmt, 4, s->websocket_requests);
|
|
|
|
|
sqlite3_bind_int64(stmt, 5, s->total_requests);
|
|
|
|
|
sqlite3_bind_int64(stmt, 6, s->bytes_sent);
|
|
|
|
|
sqlite3_bind_int64(stmt, 7, s->bytes_recv);
|
|
|
|
|
sqlite3_bind_double(stmt, 8, s->avg_request_time_ms);
|
|
|
|
|
sqlite3_step(stmt);
|
|
|
|
|
sqlite3_reset(stmt);
|
|
|
|
|
}
|
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static double get_cpu_usage() {
|
|
|
|
|
static long long prev_user = 0, prev_nice = 0, prev_system = 0, prev_idle = 0;
|
|
|
|
|
long long user, nice, system, idle, iowait, irq, softirq;
|
|
|
|
|
|
|
|
|
|
FILE *f = fopen("/proc/stat", "r");
|
|
|
|
|
if (!f) return 0.0;
|
|
|
|
|
|
|
|
|
|
if (fscanf(f, "cpu %lld %lld %lld %lld %lld %lld %lld",
|
|
|
|
|
&user, &nice, &system, &idle, &iowait, &irq, &softirq) != 7) {
|
|
|
|
|
fclose(f);
|
|
|
|
|
return 0.0;
|
|
|
|
|
}
|
|
|
|
|
fclose(f);
|
|
|
|
|
|
|
|
|
|
long long prev_total = prev_user + prev_nice + prev_system + prev_idle;
|
|
|
|
|
long long total = user + nice + system + idle;
|
|
|
|
|
long long totald = total - prev_total;
|
|
|
|
|
long long idled = idle - prev_idle;
|
|
|
|
|
|
|
|
|
|
prev_user = user; prev_nice = nice; prev_system = system; prev_idle = idle;
|
|
|
|
|
return totald == 0 ? 0.0 : (double)(totald - idled) * 100.0 / totald;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void get_memory_usage(double *used_gb) {
|
|
|
|
|
struct sysinfo info;
|
|
|
|
|
if (sysinfo(&info) != 0) {
|
|
|
|
|
*used_gb = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
*used_gb = (double)(info.totalram - info.freeram - info.bufferram) * info.mem_unit / (1024.0 * 1024.0 * 1024.0);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void get_network_stats(long long *bytes_sent, long long *bytes_recv) {
|
|
|
|
|
FILE *f = fopen("/proc/net/dev", "r");
|
|
|
|
|
if (!f) {
|
|
|
|
|
*bytes_sent = 0;
|
|
|
|
|
*bytes_recv = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char line[256];
|
|
|
|
|
if (!fgets(line, sizeof(line), f) || !fgets(line, sizeof(line), f)) { // Skip headers
|
|
|
|
|
fclose(f);
|
|
|
|
|
*bytes_sent = 0;
|
|
|
|
|
*bytes_recv = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
long long total_recv = 0, total_sent = 0;
|
|
|
|
|
while (fgets(line, sizeof(line), f)) {
|
|
|
|
|
char iface[32];
|
|
|
|
|
long long r, t;
|
|
|
|
|
if (sscanf(line, "%31[^:]: %lld %*d %*d %*d %*d %*d %*d %*d %lld", iface, &r, &t) == 3) {
|
2025-09-25 05:21:40 +02:00
|
|
|
char *trimmed = iface;
|
|
|
|
|
while (*trimmed == ' ') trimmed++;
|
|
|
|
|
if (strcmp(trimmed, "lo") != 0) {
|
2025-09-25 02:35:00 +02:00
|
|
|
total_recv += r;
|
|
|
|
|
total_sent += t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fclose(f);
|
|
|
|
|
*bytes_sent = total_sent;
|
|
|
|
|
*bytes_recv = total_recv;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
static void get_disk_stats(long long *sectors_read, long long *sectors_written) {
|
|
|
|
|
FILE *f = fopen("/proc/diskstats", "r");
|
|
|
|
|
if (!f) {
|
|
|
|
|
*sectors_read = 0;
|
|
|
|
|
*sectors_written = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:43:10 +02:00
|
|
|
char line[2048];
|
2025-09-25 05:21:40 +02:00
|
|
|
long long total_read = 0, total_written = 0;
|
|
|
|
|
while (fgets(line, sizeof(line), f)) {
|
|
|
|
|
char device[64];
|
|
|
|
|
long long sectors_r = 0, sectors_w = 0;
|
|
|
|
|
int nfields = 0;
|
|
|
|
|
// Format: major minor device reads_completed reads_merged sectors_read ... writes_completed writes_merged sectors_written
|
|
|
|
|
// Read all fields as strings, then convert needed ones
|
|
|
|
|
char major[16], minor[16], dev[64];
|
|
|
|
|
char rc[32], rm[32], sr[32], rtm[32], rtm2[32], wc[32], wm[32], sw[32];
|
|
|
|
|
nfields = sscanf(line, "%15s %15s %63s %31s %31s %31s %31s %31s %31s %31s %31s %31s",
|
|
|
|
|
major, minor, dev, rc, rm, sr, rtm, rtm2, wc, wm, sw, sw);
|
|
|
|
|
if (nfields >= 11) {
|
|
|
|
|
strncpy(device, dev, sizeof(device)-1);
|
|
|
|
|
device[sizeof(device)-1] = '\0';
|
|
|
|
|
sectors_r = atoll(sr);
|
|
|
|
|
sectors_w = atoll(sw);
|
|
|
|
|
// Only count physical devices (sda, nvme0n1, etc), not partitions
|
|
|
|
|
if (strncmp(device, "loop", 4) != 0 && strncmp(device, "ram", 3) != 0) {
|
|
|
|
|
// Check if it's a main device (no numbers at the end for sd* devices)
|
|
|
|
|
int len = strlen(device);
|
|
|
|
|
if ((strncmp(device, "sd", 2) == 0 && len == 3) ||
|
|
|
|
|
(strncmp(device, "nvme", 4) == 0 && strstr(device, "n1p") == NULL) ||
|
|
|
|
|
(strncmp(device, "vd", 2) == 0 && len == 3) ||
|
|
|
|
|
(strncmp(device, "hd", 2) == 0 && len == 3)) {
|
|
|
|
|
total_read += sectors_r;
|
|
|
|
|
total_written += sectors_w;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fclose(f);
|
|
|
|
|
*sectors_read = total_read;
|
|
|
|
|
*sectors_written = total_written;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void get_load_averages(double *load1, double *load5, double *load15) {
|
|
|
|
|
FILE *f = fopen("/proc/loadavg", "r");
|
|
|
|
|
if (!f) {
|
|
|
|
|
*load1 = *load5 = *load15 = 0.0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fscanf(f, "%lf %lf %lf", load1, load5, load15) != 3) {
|
|
|
|
|
*load1 = *load5 = *load15 = 0.0;
|
|
|
|
|
}
|
|
|
|
|
fclose(f);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
void monitor_update() {
|
|
|
|
|
double current_time = time(NULL);
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// CPU usage
|
2025-09-25 02:35:00 +02:00
|
|
|
history_deque_push(&monitor.cpu_history, current_time, get_cpu_usage());
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// Memory usage
|
2025-09-25 02:35:00 +02:00
|
|
|
double mem_used_gb;
|
|
|
|
|
get_memory_usage(&mem_used_gb);
|
|
|
|
|
history_deque_push(&monitor.memory_history, current_time, mem_used_gb);
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// Network stats
|
2025-09-25 02:35:00 +02:00
|
|
|
long long net_sent, net_recv;
|
|
|
|
|
get_network_stats(&net_sent, &net_recv);
|
|
|
|
|
double time_delta = current_time - monitor.last_net_update_time;
|
|
|
|
|
if (time_delta > 0 && monitor.last_net_update_time > 0) {
|
|
|
|
|
double rx = (net_recv - monitor.last_net_recv) / time_delta / 1024.0;
|
|
|
|
|
double tx = (net_sent - monitor.last_net_sent) / time_delta / 1024.0;
|
|
|
|
|
network_history_deque_push(&monitor.network_history, current_time, fmax(0, rx), fmax(0, tx));
|
|
|
|
|
history_deque_push(&monitor.throughput_history, current_time, fmax(0, rx + tx));
|
|
|
|
|
}
|
|
|
|
|
monitor.last_net_sent = net_sent;
|
|
|
|
|
monitor.last_net_recv = net_recv;
|
|
|
|
|
monitor.last_net_update_time = current_time;
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// Disk I/O stats
|
|
|
|
|
long long disk_read, disk_write;
|
|
|
|
|
get_disk_stats(&disk_read, &disk_write);
|
|
|
|
|
double disk_time_delta = current_time - monitor.last_disk_update_time;
|
|
|
|
|
if (disk_time_delta > 0 && monitor.last_disk_update_time > 0) {
|
|
|
|
|
// Convert sectors to MB/s (sector = 512 bytes typically)
|
|
|
|
|
double read_mbps = (disk_read - monitor.last_disk_read) * 512.0 / disk_time_delta / (1024.0 * 1024.0);
|
|
|
|
|
double write_mbps = (disk_write - monitor.last_disk_write) * 512.0 / disk_time_delta / (1024.0 * 1024.0);
|
|
|
|
|
disk_history_deque_push(&monitor.disk_history, current_time, fmax(0, read_mbps), fmax(0, write_mbps));
|
|
|
|
|
}
|
|
|
|
|
monitor.last_disk_read = disk_read;
|
|
|
|
|
monitor.last_disk_write = disk_write;
|
|
|
|
|
monitor.last_disk_update_time = current_time;
|
|
|
|
|
|
|
|
|
|
// Load averages
|
|
|
|
|
double load1, load5, load15;
|
|
|
|
|
get_load_averages(&load1, &load5, &load15);
|
|
|
|
|
history_deque_push(&monitor.load1_history, current_time, load1);
|
|
|
|
|
history_deque_push(&monitor.load5_history, current_time, load5);
|
|
|
|
|
history_deque_push(&monitor.load15_history, current_time, load15);
|
|
|
|
|
|
|
|
|
|
// VHost stats
|
2025-09-25 02:35:00 +02:00
|
|
|
for (vhost_stats_t *s = monitor.vhost_stats_head; s != NULL; s = s->next) {
|
|
|
|
|
double vhost_delta = current_time - s->last_update;
|
|
|
|
|
if (vhost_delta >= 1.0) {
|
|
|
|
|
double kbps = 0;
|
|
|
|
|
if (s->last_update > 0) {
|
|
|
|
|
long long bytes_diff = (s->bytes_sent - s->last_bytes_sent) + (s->bytes_recv - s->last_bytes_recv);
|
|
|
|
|
kbps = bytes_diff / vhost_delta / 1024.0;
|
|
|
|
|
}
|
|
|
|
|
history_deque_push(&s->throughput_history, current_time, fmax(0, kbps));
|
|
|
|
|
s->last_bytes_sent = s->bytes_sent;
|
|
|
|
|
s->last_bytes_recv = s->bytes_recv;
|
|
|
|
|
s->last_update = current_time;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static time_t last_db_save = 0;
|
|
|
|
|
if (current_time - last_db_save >= 10) {
|
|
|
|
|
save_stats_to_db();
|
|
|
|
|
last_db_save = current_time;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vhost_stats_t* monitor_get_or_create_vhost_stats(const char *vhost_name) {
|
|
|
|
|
if (!vhost_name || strlen(vhost_name) == 0) return NULL;
|
|
|
|
|
|
|
|
|
|
for (vhost_stats_t *curr = monitor.vhost_stats_head; curr; curr = curr->next) {
|
|
|
|
|
if (strcmp(curr->vhost_name, vhost_name) == 0) return curr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vhost_stats_t *new_stats = calloc(1, sizeof(vhost_stats_t));
|
|
|
|
|
if (!new_stats) return NULL;
|
|
|
|
|
|
|
|
|
|
strncpy(new_stats->vhost_name, vhost_name, sizeof(new_stats->vhost_name) - 1);
|
|
|
|
|
new_stats->last_update = time(NULL);
|
|
|
|
|
history_deque_init(&new_stats->throughput_history, 60);
|
|
|
|
|
request_time_deque_init(&new_stats->request_times, 100);
|
|
|
|
|
new_stats->next = monitor.vhost_stats_head;
|
|
|
|
|
monitor.vhost_stats_head = new_stats;
|
|
|
|
|
return new_stats;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void monitor_record_request_start(vhost_stats_t *stats, int is_websocket) {
|
|
|
|
|
if (!stats) return;
|
|
|
|
|
if (is_websocket) {
|
2025-09-25 05:21:40 +02:00
|
|
|
__sync_fetch_and_add(&stats->websocket_requests, 1);
|
2025-09-25 02:35:00 +02:00
|
|
|
} else {
|
2025-09-25 05:21:40 +02:00
|
|
|
__sync_fetch_and_add(&stats->http_requests, 1);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
__sync_fetch_and_add(&stats->total_requests, 1);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void monitor_record_request_end(vhost_stats_t *stats, double start_time) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (!stats || start_time <= 0) return;
|
2025-09-25 02:35:00 +02:00
|
|
|
struct timespec end_time;
|
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &end_time);
|
|
|
|
|
double duration_ms = ((end_time.tv_sec + end_time.tv_nsec / 1e9) - start_time) * 1000.0;
|
2025-09-25 05:21:40 +02:00
|
|
|
if (duration_ms >= 0 && duration_ms < 60000) { // Sanity check: < 60 seconds
|
2025-09-25 02:35:00 +02:00
|
|
|
request_time_deque_push(&stats->request_times, duration_ms);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
void monitor_record_bytes(vhost_stats_t *stats, long long sent, long long recv) {
|
|
|
|
|
if (!stats) return;
|
|
|
|
|
__sync_fetch_and_add(&stats->bytes_sent, sent);
|
|
|
|
|
__sync_fetch_and_add(&stats->bytes_recv, recv);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
// =================================================================================================
|
|
|
|
|
//
|
2025-09-25 05:11:52 +02:00
|
|
|
// Dashboard Implementation
|
2025-09-25 02:35:00 +02:00
|
|
|
//
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
const char *DASHBOARD_HTML =
|
|
|
|
|
"<!DOCTYPE html>\n"
|
|
|
|
|
"<html>\n"
|
|
|
|
|
"<head>\n"
|
|
|
|
|
" <title>Reverse Proxy Monitor</title>\n"
|
|
|
|
|
" <style>\n"
|
|
|
|
|
" * { margin: 0; padding: 0; box-sizing: border-box; }\n"
|
2025-09-25 05:43:10 +02:00
|
|
|
" body { font-family: -apple-system, system-ui, sans-serif; background: #000; color: #fff; padding: 20px; }\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" .header { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin-bottom: 30px; gap: 20px; }\n"
|
2025-09-25 05:43:10 +02:00
|
|
|
" .metric { text-align: center; background: #000; border-radius: 8px; padding: 15px; }\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" .metric-value { font-size: 36px; font-weight: 300; }\n"
|
|
|
|
|
" .metric-label { font-size: 14px; opacity: 0.7; text-transform: uppercase; margin-top: 5px; }\n"
|
2025-09-25 05:43:10 +02:00
|
|
|
" .chart-container { background: #000; border-radius: 8px; padding: 20px; margin-bottom: 20px; height: 250px; position: relative; }\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" .chart-title { position: absolute; top: 10px; left: 20px; font-size: 14px; opacity: 0.7; z-index: 10; }\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" canvas { width: 100% !important; height: 100% !important; }\n"
|
2025-09-25 05:43:10 +02:00
|
|
|
" .process-table { background: #000; border-radius: 8px; padding: 20px; }\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" table { width: 100%; border-collapse: collapse; }\n"
|
|
|
|
|
" th, td { padding: 10px; text-align: left; border-bottom: 1px solid #2a2e3e; }\n"
|
|
|
|
|
" th { font-weight: 500; opacity: 0.7; }\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" .legend { position: absolute; top: 10px; right: 20px; display: flex; gap: 20px; font-size: 12px; z-index: 10; }\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" .legend-item { display: flex; align-items: center; gap: 5px; }\n"
|
|
|
|
|
" .legend-color { width: 12px; height: 12px; border-radius: 2px; }\n"
|
2025-09-25 05:43:10 +02:00
|
|
|
" .vhost-chart-container { height: 200px; background: #000; border-radius: 8px; position: relative; padding: 20px; margin-bottom: 10px; }\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" .load-values { display: flex; gap: 10px; font-size: 12px; opacity: 0.8; }\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" </style>\n"
|
|
|
|
|
" <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n"
|
|
|
|
|
"</head>\n"
|
|
|
|
|
"<body>\n"
|
|
|
|
|
" <div class=\"header\">\n"
|
|
|
|
|
" <div class=\"metric\">\n"
|
|
|
|
|
" <div class=\"metric-value\" id=\"connections\">0</div>\n"
|
|
|
|
|
" <div class=\"metric-label\">Connections</div>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
" <div class=\"metric\">\n"
|
|
|
|
|
" <div class=\"metric-value\" id=\"memory\">0</div>\n"
|
|
|
|
|
" <div class=\"metric-label\">Memory</div>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
" <div class=\"metric\">\n"
|
|
|
|
|
" <div class=\"metric-value\" id=\"cpu\">0</div>\n"
|
|
|
|
|
" <div class=\"metric-label\">CPU %</div>\n"
|
|
|
|
|
" </div>\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" <div class=\"metric\">\n"
|
|
|
|
|
" <div class=\"metric-value\" id=\"load1\">0.00</div>\n"
|
|
|
|
|
" <div class=\"metric-label\">Load 1m</div>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
" <div class=\"metric\">\n"
|
|
|
|
|
" <div class=\"metric-value\" id=\"load5\">0.00</div>\n"
|
|
|
|
|
" <div class=\"metric-label\">Load 5m</div>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
" <div class=\"metric\">\n"
|
|
|
|
|
" <div class=\"metric-value\" id=\"load15\">0.00</div>\n"
|
|
|
|
|
" <div class=\"metric-label\">Load 15m</div>\n"
|
|
|
|
|
" </div>\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" </div>\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" <div class=\"chart-container\">\n"
|
|
|
|
|
" <div class=\"chart-title\">CPU Usage</div>\n"
|
|
|
|
|
" <canvas id=\"cpuChart\"></canvas>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" <div class=\"chart-container\">\n"
|
|
|
|
|
" <div class=\"chart-title\">Memory Usage</div>\n"
|
|
|
|
|
" <canvas id=\"memChart\"></canvas>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" <div class=\"chart-container\">\n"
|
|
|
|
|
" <div class=\"chart-title\">Network I/O</div>\n"
|
|
|
|
|
" <div class=\"legend\">\n"
|
|
|
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #3498db\"></div><span>RX</span></div>\n"
|
|
|
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #2ecc71\"></div><span>TX</span></div>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
" <canvas id=\"netChart\"></canvas>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
"\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" <div class=\"chart-container\">\n"
|
|
|
|
|
" <div class=\"chart-title\">Disk I/O</div>\n"
|
|
|
|
|
" <div class=\"legend\">\n"
|
|
|
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #9b59b6\"></div><span>Read</span></div>\n"
|
|
|
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #e67e22\"></div><span>Write</span></div>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
" <canvas id=\"diskChart\"></canvas>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" <div class=\"chart-container\">\n"
|
|
|
|
|
" <div class=\"chart-title\">Load Average</div>\n"
|
|
|
|
|
" <div class=\"legend\">\n"
|
|
|
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #e74c3c\"></div><span>1 min</span></div>\n"
|
|
|
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #f39c12\"></div><span>5 min</span></div>\n"
|
|
|
|
|
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #3498db\"></div><span>15 min</span></div>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
" <canvas id=\"loadChart\"></canvas>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
"\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" <div class=\"process-table\">\n"
|
|
|
|
|
" <table>\n"
|
|
|
|
|
" <thead>\n"
|
|
|
|
|
" <tr>\n"
|
|
|
|
|
" <th>Virtual Host</th>\n"
|
|
|
|
|
" <th>HTTP Req</th>\n"
|
|
|
|
|
" <th>WS Req</th>\n"
|
|
|
|
|
" <th>Total Req</th>\n"
|
|
|
|
|
" <th>Avg Resp (ms)</th>\n"
|
|
|
|
|
" <th>Sent</th>\n"
|
|
|
|
|
" <th>Received</th>\n"
|
|
|
|
|
" </tr>\n"
|
|
|
|
|
" </thead>\n"
|
|
|
|
|
" <tbody id=\"processTable\"></tbody>\n"
|
|
|
|
|
" </table>\n"
|
|
|
|
|
" </div>\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" <script>\n"
|
|
|
|
|
" const formatTimeTick = (value) => {\n"
|
|
|
|
|
" const seconds = Math.abs(Math.round(value / 1000));\n"
|
|
|
|
|
" if (seconds === 0) return 'now';\n"
|
|
|
|
|
" const m = Math.floor(seconds / 60);\n"
|
|
|
|
|
" const s = seconds % 60;\n"
|
|
|
|
|
" return `-${m > 0 ? `${m}m ` : ''}${s}s`;\n"
|
|
|
|
|
" };\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" const formatSizeTick = (value) => {\n"
|
|
|
|
|
" if (value >= 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} GB/s`;\n"
|
|
|
|
|
" if (value >= 1024) return `${(value / 1024).toFixed(1)} MB/s`;\n"
|
|
|
|
|
" return `${value.toFixed(0)} KB/s`;\n"
|
|
|
|
|
" };\n"
|
|
|
|
|
"\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" const formatDiskTick = (value) => {\n"
|
|
|
|
|
" if (value >= 1024) return `${(value / 1024).toFixed(1)} GB/s`;\n"
|
|
|
|
|
" return `${value.toFixed(1)} MB/s`;\n"
|
|
|
|
|
" };\n"
|
|
|
|
|
"\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" const formatBytes = (bytes) => {\n"
|
|
|
|
|
" if (bytes === 0) return '0 B';\n"
|
|
|
|
|
" const k = 1024;\n"
|
|
|
|
|
" const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n"
|
|
|
|
|
" const i = Math.floor(Math.log(bytes) / Math.log(k));\n"
|
|
|
|
|
" return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n"
|
|
|
|
|
" };\n"
|
|
|
|
|
"\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" const createBaseChartOptions = (historySeconds, yTickCallback) => ({\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" responsive: true,\n"
|
|
|
|
|
" maintainAspectRatio: false,\n"
|
|
|
|
|
" animation: false,\n"
|
|
|
|
|
" layout: { padding: { top: 30 } },\n"
|
|
|
|
|
" interaction: { mode: 'nearest', axis: 'x', intersect: false },\n"
|
|
|
|
|
" scales: {\n"
|
|
|
|
|
" x: { type: 'linear', display: true, grid: { color: '#2a2e3e' }, ticks: { color: '#666', maxTicksLimit: 7, callback: formatTimeTick }, min: -historySeconds * 1000, max: 0 },\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" y: { display: true, grid: { color: '#2a2e3e' }, ticks: { color: '#666', beginAtZero: true, callback: yTickCallback } }\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" },\n"
|
|
|
|
|
" plugins: { legend: { display: false }, tooltip: { displayColors: false } },\n"
|
|
|
|
|
" elements: { point: { radius: 0 }, line: { borderWidth: 2, tension: 0.4, fill: true } }\n"
|
|
|
|
|
" });\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" const cpuChart = new Chart(document.getElementById('cpuChart'), {\n"
|
|
|
|
|
" type: 'line',\n"
|
|
|
|
|
" data: { datasets: [{ label: 'CPU %', data: [], borderColor: '#f39c12', backgroundColor: 'rgba(243, 156, 18, 0.1)' }] },\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" options: { ...createBaseChartOptions(300, v => `${v}%`), scales: { ...createBaseChartOptions(300).scales, y: { ...createBaseChartOptions(300).scales.y, max: 100, ticks: { ...createBaseChartOptions(300).scales.y.ticks, callback: v => `${v}%` } } } }\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" });\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" const memChart = new Chart(document.getElementById('memChart'), {\n"
|
|
|
|
|
" type: 'line',\n"
|
|
|
|
|
" data: { datasets: [{ label: 'Memory GB', data: [], borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)' }] },\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" options: createBaseChartOptions(300, v => `${v.toFixed(2)} GiB`)\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" });\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" const netChart = new Chart(document.getElementById('netChart'), {\n"
|
|
|
|
|
" type: 'line',\n"
|
|
|
|
|
" data: {\n"
|
|
|
|
|
" datasets: [\n"
|
|
|
|
|
" { label: 'RX KB/s', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)' },\n"
|
|
|
|
|
" { label: 'TX KB/s', data: [], borderColor: '#2ecc71', backgroundColor: 'rgba(46, 204, 113, 0.1)' }\n"
|
|
|
|
|
" ]\n"
|
|
|
|
|
" },\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" options: createBaseChartOptions(300, formatSizeTick)\n"
|
|
|
|
|
" });\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" const diskChart = new Chart(document.getElementById('diskChart'), {\n"
|
|
|
|
|
" type: 'line',\n"
|
|
|
|
|
" data: {\n"
|
|
|
|
|
" datasets: [\n"
|
|
|
|
|
" { label: 'Read MB/s', data: [], borderColor: '#9b59b6', backgroundColor: 'rgba(155, 89, 182, 0.1)' },\n"
|
|
|
|
|
" { label: 'Write MB/s', data: [], borderColor: '#e67e22', backgroundColor: 'rgba(230, 126, 34, 0.1)' }\n"
|
|
|
|
|
" ]\n"
|
|
|
|
|
" },\n"
|
|
|
|
|
" options: createBaseChartOptions(300, formatDiskTick)\n"
|
|
|
|
|
" });\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" const loadChart = new Chart(document.getElementById('loadChart'), {\n"
|
|
|
|
|
" type: 'line',\n"
|
|
|
|
|
" data: {\n"
|
|
|
|
|
" datasets: [\n"
|
|
|
|
|
" { label: 'Load 1m', data: [], borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)' },\n"
|
|
|
|
|
" { label: 'Load 5m', data: [], borderColor: '#f39c12', backgroundColor: 'rgba(243, 156, 18, 0.1)' },\n"
|
|
|
|
|
" { label: 'Load 15m', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)' }\n"
|
|
|
|
|
" ]\n"
|
|
|
|
|
" },\n"
|
|
|
|
|
" options: createBaseChartOptions(300, v => v.toFixed(2))\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" });\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" window.vhostCharts = {};\n"
|
|
|
|
|
" let prevProcessNames = [];\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" async function updateStats() {\n"
|
|
|
|
|
" try {\n"
|
|
|
|
|
" const response = await fetch('/api/stats');\n"
|
|
|
|
|
" const data = await response.json();\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" document.getElementById('connections').textContent = data.current.active_connections;\n"
|
|
|
|
|
" document.getElementById('memory').textContent = data.current.memory_gb + ' GiB';\n"
|
|
|
|
|
" document.getElementById('cpu').textContent = data.current.cpu_percent + '%';\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" document.getElementById('load1').textContent = data.current.load_1m.toFixed(2);\n"
|
|
|
|
|
" document.getElementById('load5').textContent = data.current.load_5m.toFixed(2);\n"
|
|
|
|
|
" document.getElementById('load15').textContent = data.current.load_15m.toFixed(2);\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
"\n"
|
|
|
|
|
" cpuChart.data.datasets[0].data = data.cpu_history;\n"
|
|
|
|
|
" memChart.data.datasets[0].data = data.memory_history;\n"
|
|
|
|
|
" netChart.data.datasets[0].data = data.network_rx_history;\n"
|
|
|
|
|
" netChart.data.datasets[1].data = data.network_tx_history;\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" diskChart.data.datasets[0].data = data.disk_read_history;\n"
|
|
|
|
|
" diskChart.data.datasets[1].data = data.disk_write_history;\n"
|
|
|
|
|
" loadChart.data.datasets[0].data = data.load1_history;\n"
|
|
|
|
|
" loadChart.data.datasets[1].data = data.load5_history;\n"
|
|
|
|
|
" loadChart.data.datasets[2].data = data.load15_history;\n"
|
|
|
|
|
"\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" cpuChart.update('none');\n"
|
|
|
|
|
" memChart.update('none');\n"
|
|
|
|
|
" netChart.update('none');\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" diskChart.update('none');\n"
|
|
|
|
|
" loadChart.update('none');\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
"\n"
|
|
|
|
|
" const tbody = document.getElementById('processTable');\n"
|
|
|
|
|
" const processNames = data.processes.map(p => p.name).join(',');\n"
|
|
|
|
|
" if (processNames !== prevProcessNames.join(',')) {\n"
|
|
|
|
|
" Object.values(window.vhostCharts).forEach(chart => chart.destroy());\n"
|
|
|
|
|
" window.vhostCharts = {};\n"
|
|
|
|
|
" tbody.innerHTML = '';\n"
|
|
|
|
|
" data.processes.forEach((p, index) => {\n"
|
|
|
|
|
" const colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c'];\n"
|
|
|
|
|
" const chartColor = colors[index % colors.length];\n"
|
|
|
|
|
" const mainRow = document.createElement('tr');\n"
|
|
|
|
|
" mainRow.innerHTML = `<td style='color: ${chartColor}'>${p.name}</td><td>${p.http_requests}</td><td>${p.websocket_requests}</td><td>${p.total_requests}</td><td>${p.avg_request_time_ms.toFixed(2)}</td><td>${formatBytes(p.bytes_sent)}</td><td>${formatBytes(p.bytes_recv)}</td>`;\n"
|
|
|
|
|
" tbody.appendChild(mainRow);\n"
|
|
|
|
|
" \n"
|
|
|
|
|
" const chartRow = document.createElement('tr');\n"
|
|
|
|
|
" chartRow.innerHTML = `<td colspan='7' style='padding: 10px 0; border: none;'><div class='vhost-chart-container'><div class='chart-title'>Live Throughput - ${p.name}</div><canvas id='vhostChart${index}'></canvas></div></td>`;\n"
|
|
|
|
|
" tbody.appendChild(chartRow);\n"
|
|
|
|
|
" });\n"
|
|
|
|
|
" prevProcessNames = data.processes.map(p => p.name);\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" data.processes.forEach((p, index) => {\n"
|
|
|
|
|
" const chartId = `vhostChart${index}`;\n"
|
|
|
|
|
" const canvas = document.getElementById(chartId);\n"
|
|
|
|
|
" if (!canvas) return;\n"
|
|
|
|
|
" if (!window.vhostCharts[chartId]) {\n"
|
|
|
|
|
" const colors = [\n"
|
2025-09-25 05:11:52 +02:00
|
|
|
" { border: '#3498db', bg: 'rgba(52, 152, 219, 0.1)' }, { border: '#2ecc71', bg: 'rgba(46, 204, 113, 0.1)' },\n"
|
|
|
|
|
" { border: '#f39c12', bg: 'rgba(243, 156, 18, 0.1)' }, { border: '#e74c3c', bg: 'rgba(231, 76, 60, 0.1)' },\n"
|
|
|
|
|
" { border: '#9b59b6', bg: 'rgba(155, 89, 182, 0.1)' }, { border: '#1abc9c', bg: 'rgba(26, 188, 156, 0.1)' }\n"
|
|
|
|
|
" ];\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" const color = colors[index % colors.length];\n"
|
|
|
|
|
" window.vhostCharts[chartId] = new Chart(canvas.getContext('2d'), {\n"
|
|
|
|
|
" type: 'line',\n"
|
|
|
|
|
" data: { datasets: [{ label: 'Throughput KB/s', data: p.throughput_history || [], borderColor: color.border, backgroundColor: color.bg }] },\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
" options: createBaseChartOptions(60, formatSizeTick)\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
" });\n"
|
|
|
|
|
" } else {\n"
|
|
|
|
|
" window.vhostCharts[chartId].data.datasets[0].data = p.throughput_history || [];\n"
|
|
|
|
|
" window.vhostCharts[chartId].update('none');\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" });\n"
|
|
|
|
|
" } catch (e) {\n"
|
|
|
|
|
" console.error('Failed to fetch stats:', e);\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" updateStats();\n"
|
|
|
|
|
" setInterval(updateStats, 1000);\n"
|
|
|
|
|
" </script>\n"
|
|
|
|
|
"</body>\n"
|
|
|
|
|
"</html>\n"
|
|
|
|
|
;
|
2025-09-25 05:21:40 +02:00
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
void serve_dashboard(connection_t *conn) {
|
|
|
|
|
if (!conn) return;
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
size_t content_len = strlen(DASHBOARD_HTML);
|
2025-09-25 02:35:00 +02:00
|
|
|
char header[512];
|
|
|
|
|
int len = snprintf(header, sizeof(header),
|
|
|
|
|
"HTTP/1.1 200 OK\r\n"
|
|
|
|
|
"Content-Type: text/html; charset=utf-8\r\n"
|
|
|
|
|
"Content-Length: %zu\r\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
"Connection: %s\r\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
"Cache-Control: no-cache\r\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
"\r\n",
|
|
|
|
|
content_len,
|
|
|
|
|
conn->request.keep_alive ? "keep-alive" : "close");
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + len + content_len) < 0) {
|
2025-09-25 02:35:00 +02:00
|
|
|
send_error_response(conn, 500, "Internal Server Error", "Memory allocation failed");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, header, len);
|
|
|
|
|
conn->write_buf.tail += len;
|
2025-09-25 05:21:40 +02:00
|
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, DASHBOARD_HTML, content_len);
|
|
|
|
|
conn->write_buf.tail += content_len;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
|
|
|
|
|
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static cJSON* format_history(history_deque_t *dq, int window_seconds) {
|
|
|
|
|
cJSON *arr = cJSON_CreateArray();
|
|
|
|
|
if (!arr || !dq || !dq->points || dq->count == 0) return arr;
|
|
|
|
|
|
|
|
|
|
double current_time = time(NULL);
|
|
|
|
|
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < dq->count; ++i) {
|
|
|
|
|
int current_index = (start_index + i) % dq->capacity;
|
|
|
|
|
history_point_t *p = &dq->points[current_index];
|
|
|
|
|
if ((current_time - p->time) <= window_seconds) {
|
|
|
|
|
cJSON *pt = cJSON_CreateObject();
|
|
|
|
|
if (pt) {
|
|
|
|
|
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
|
|
|
|
|
cJSON_AddNumberToObject(pt, "y", p->value);
|
|
|
|
|
cJSON_AddItemToArray(arr, pt);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static cJSON* format_network_history(network_history_deque_t *dq, int window_seconds, const char *key) {
|
|
|
|
|
cJSON *arr = cJSON_CreateArray();
|
|
|
|
|
if (!arr || !dq || !dq->points || !key || dq->count == 0) return arr;
|
|
|
|
|
|
|
|
|
|
double current_time = time(NULL);
|
|
|
|
|
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < dq->count; ++i) {
|
|
|
|
|
int current_index = (start_index + i) % dq->capacity;
|
|
|
|
|
network_history_point_t *p = &dq->points[current_index];
|
|
|
|
|
if ((current_time - p->time) <= window_seconds) {
|
|
|
|
|
cJSON *pt = cJSON_CreateObject();
|
|
|
|
|
if (pt) {
|
|
|
|
|
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
|
|
|
|
|
cJSON_AddNumberToObject(pt, "y", strcmp(key, "rx_kbps") == 0 ? p->rx_kbps : p->tx_kbps);
|
|
|
|
|
cJSON_AddItemToArray(arr, pt);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
static cJSON* format_disk_history(disk_history_deque_t *dq, int window_seconds, const char *key) {
|
|
|
|
|
cJSON *arr = cJSON_CreateArray();
|
|
|
|
|
if (!arr || !dq || !dq->points || !key || dq->count == 0) return arr;
|
|
|
|
|
|
|
|
|
|
double current_time = time(NULL);
|
|
|
|
|
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < dq->count; ++i) {
|
|
|
|
|
int current_index = (start_index + i) % dq->capacity;
|
|
|
|
|
disk_history_point_t *p = &dq->points[current_index];
|
|
|
|
|
if ((current_time - p->time) <= window_seconds) {
|
|
|
|
|
cJSON *pt = cJSON_CreateObject();
|
|
|
|
|
if (pt) {
|
|
|
|
|
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
|
|
|
|
|
cJSON_AddNumberToObject(pt, "y", strcmp(key, "read_mbps") == 0 ? p->read_mbps : p->write_mbps);
|
|
|
|
|
cJSON_AddItemToArray(arr, pt);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
void serve_stats_api(connection_t *conn) {
|
|
|
|
|
if (!conn) return;
|
|
|
|
|
|
|
|
|
|
cJSON *root = cJSON_CreateObject();
|
|
|
|
|
if (!root) {
|
|
|
|
|
send_error_response(conn, 500, "Internal Server Error", "JSON creation failed");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON *current = cJSON_CreateObject();
|
|
|
|
|
if (!current) {
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
send_error_response(conn, 500, "Internal Server Error", "JSON creation failed");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddItemToObject(root, "current", current);
|
|
|
|
|
|
|
|
|
|
char buffer[64];
|
|
|
|
|
double last_cpu = 0, last_mem = 0;
|
2025-09-25 05:21:40 +02:00
|
|
|
double load1 = 0, load5 = 0, load15 = 0;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
if (monitor.cpu_history.count > 0) {
|
|
|
|
|
int last_idx = (monitor.cpu_history.head - 1 + monitor.cpu_history.capacity) % monitor.cpu_history.capacity;
|
|
|
|
|
last_cpu = monitor.cpu_history.points[last_idx].value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (monitor.memory_history.count > 0) {
|
|
|
|
|
int last_idx = (monitor.memory_history.head - 1 + monitor.memory_history.capacity) % monitor.memory_history.capacity;
|
|
|
|
|
last_mem = monitor.memory_history.points[last_idx].value;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
if (monitor.load1_history.count > 0) {
|
|
|
|
|
int idx = (monitor.load1_history.head - 1 + monitor.load1_history.capacity) % monitor.load1_history.capacity;
|
|
|
|
|
load1 = monitor.load1_history.points[idx].value;
|
|
|
|
|
}
|
|
|
|
|
if (monitor.load5_history.count > 0) {
|
|
|
|
|
int idx = (monitor.load5_history.head - 1 + monitor.load5_history.capacity) % monitor.load5_history.capacity;
|
|
|
|
|
load5 = monitor.load5_history.points[idx].value;
|
|
|
|
|
}
|
|
|
|
|
if (monitor.load15_history.count > 0) {
|
|
|
|
|
int idx = (monitor.load15_history.head - 1 + monitor.load15_history.capacity) % monitor.load15_history.capacity;
|
|
|
|
|
load15 = monitor.load15_history.points[idx].value;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
snprintf(buffer, sizeof(buffer), "%.2f", last_cpu);
|
|
|
|
|
cJSON_AddStringToObject(current, "cpu_percent", buffer);
|
|
|
|
|
snprintf(buffer, sizeof(buffer), "%.2f", last_mem);
|
|
|
|
|
cJSON_AddStringToObject(current, "memory_gb", buffer);
|
|
|
|
|
cJSON_AddNumberToObject(current, "active_connections", monitor.active_connections);
|
2025-09-25 05:21:40 +02:00
|
|
|
cJSON_AddNumberToObject(current, "load_1m", load1);
|
|
|
|
|
cJSON_AddNumberToObject(current, "load_5m", load5);
|
|
|
|
|
cJSON_AddNumberToObject(current, "load_15m", load15);
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
cJSON_AddItemToObject(root, "cpu_history", format_history(&monitor.cpu_history, HISTORY_SECONDS));
|
|
|
|
|
cJSON_AddItemToObject(root, "memory_history", format_history(&monitor.memory_history, HISTORY_SECONDS));
|
|
|
|
|
cJSON_AddItemToObject(root, "network_rx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "rx_kbps"));
|
|
|
|
|
cJSON_AddItemToObject(root, "network_tx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "tx_kbps"));
|
2025-09-25 05:21:40 +02:00
|
|
|
cJSON_AddItemToObject(root, "disk_read_history", format_disk_history(&monitor.disk_history, HISTORY_SECONDS, "read_mbps"));
|
|
|
|
|
cJSON_AddItemToObject(root, "disk_write_history", format_disk_history(&monitor.disk_history, HISTORY_SECONDS, "write_mbps"));
|
2025-09-25 02:35:00 +02:00
|
|
|
cJSON_AddItemToObject(root, "throughput_history", format_history(&monitor.throughput_history, HISTORY_SECONDS));
|
2025-09-25 05:21:40 +02:00
|
|
|
cJSON_AddItemToObject(root, "load1_history", format_history(&monitor.load1_history, HISTORY_SECONDS));
|
|
|
|
|
cJSON_AddItemToObject(root, "load5_history", format_history(&monitor.load5_history, HISTORY_SECONDS));
|
|
|
|
|
cJSON_AddItemToObject(root, "load15_history", format_history(&monitor.load15_history, HISTORY_SECONDS));
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
cJSON *processes = cJSON_CreateArray();
|
|
|
|
|
if (processes) {
|
|
|
|
|
cJSON_AddItemToObject(root, "processes", processes);
|
|
|
|
|
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
|
|
|
|
|
cJSON *p = cJSON_CreateObject();
|
|
|
|
|
if (p) {
|
|
|
|
|
cJSON_AddStringToObject(p, "name", s->vhost_name);
|
|
|
|
|
cJSON_AddNumberToObject(p, "http_requests", s->http_requests);
|
|
|
|
|
cJSON_AddNumberToObject(p, "websocket_requests", s->websocket_requests);
|
|
|
|
|
cJSON_AddNumberToObject(p, "total_requests", s->total_requests);
|
|
|
|
|
cJSON_AddNumberToObject(p, "avg_request_time_ms", s->avg_request_time_ms);
|
|
|
|
|
cJSON_AddNumberToObject(p, "bytes_sent", s->bytes_sent);
|
|
|
|
|
cJSON_AddNumberToObject(p, "bytes_recv", s->bytes_recv);
|
|
|
|
|
cJSON_AddItemToObject(p, "throughput_history", format_history(&s->throughput_history, 60));
|
|
|
|
|
cJSON_AddItemToArray(processes, p);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char *json_string = cJSON_PrintUnformatted(root);
|
|
|
|
|
if (!json_string) {
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
send_error_response(conn, 500, "Internal Server Error", "JSON serialization failed");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char header[512];
|
|
|
|
|
int hlen = snprintf(header, sizeof(header),
|
|
|
|
|
"HTTP/1.1 200 OK\r\n"
|
|
|
|
|
"Content-Type: application/json; charset=utf-8\r\n"
|
|
|
|
|
"Content-Length: %zu\r\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
"Connection: %s\r\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
"Cache-Control: no-cache\r\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
"\r\n",
|
|
|
|
|
strlen(json_string),
|
|
|
|
|
conn->request.keep_alive ? "keep-alive" : "close");
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + hlen + strlen(json_string)) < 0) {
|
2025-09-25 02:35:00 +02:00
|
|
|
free(json_string);
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
send_error_response(conn, 500, "Internal Server Error", "Memory allocation failed");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, header, hlen);
|
|
|
|
|
conn->write_buf.tail += hlen;
|
|
|
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, json_string, strlen(json_string));
|
|
|
|
|
conn->write_buf.tail += strlen(json_string);
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
|
|
|
|
|
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
|
|
|
|
|
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
free(json_string);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
//
|
2025-09-25 05:11:52 +02:00
|
|
|
// HTTP Implementation
|
2025-09-25 02:35:00 +02:00
|
|
|
//
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
// Helper to find a header value without modifying the source string
|
|
|
|
|
static int find_header_value(const char* data, size_t len, const char* name, char* value, size_t value_size) {
|
|
|
|
|
size_t name_len = strlen(name);
|
|
|
|
|
const char* end = data + len;
|
|
|
|
|
|
|
|
|
|
for (const char* p = data; p < end; ) {
|
|
|
|
|
const char* line_end = memchr(p, '\n', end - p);
|
|
|
|
|
if (!line_end) break;
|
|
|
|
|
|
|
|
|
|
if (line_end > p && *(line_end - 1) == '\r') {
|
|
|
|
|
line_end--;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
if ((size_t)(line_end - p) > name_len && strncasecmp(p, name, name_len) == 0 && p[name_len] == ':') {
|
|
|
|
|
const char* v_start = p + name_len + 1;
|
|
|
|
|
while (v_start < line_end && (*v_start == ' ' || *v_start == '\t')) v_start++;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
const char* v_end = line_end;
|
|
|
|
|
while (v_end > v_start && (*(v_end - 1) == ' ' || *(v_end - 1) == '\t')) v_end--;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
size_t copy_len = v_end - v_start;
|
|
|
|
|
if (copy_len >= value_size) copy_len = value_size - 1;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
memcpy(value, v_start, copy_len);
|
|
|
|
|
value[copy_len] = '\0';
|
|
|
|
|
return 1;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
p = line_end + ( (line_end < end && *(line_end) == '\r') ? 2 : 1 );
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
int parse_http_request(const char *data, size_t len, http_request_t *req) {
|
|
|
|
|
memset(req, 0, sizeof(http_request_t));
|
|
|
|
|
req->content_length = -1;
|
2025-09-25 05:21:40 +02:00
|
|
|
req->keep_alive = 1; // HTTP/1.1 default
|
2025-09-25 05:11:52 +02:00
|
|
|
|
|
|
|
|
// Find the end of the request line
|
|
|
|
|
const char *line_end = memchr(data, '\n', len);
|
|
|
|
|
if (!line_end) return -1; // Incomplete
|
|
|
|
|
size_t line_len = line_end - data;
|
|
|
|
|
if (line_len > 0 && data[line_len - 1] == '\r') {
|
|
|
|
|
line_len--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse Method
|
|
|
|
|
const char *method_end = memchr(data, ' ', line_len);
|
|
|
|
|
if (!method_end) return 0; // Malformed
|
|
|
|
|
size_t method_len = method_end - data;
|
|
|
|
|
if (method_len >= sizeof(req->method)) return 0;
|
|
|
|
|
memcpy(req->method, data, method_len);
|
|
|
|
|
req->method[method_len] = '\0';
|
|
|
|
|
if (!is_valid_http_method(req->method)) return 0;
|
|
|
|
|
|
|
|
|
|
// Parse URI
|
|
|
|
|
const char *uri_start = method_end + 1;
|
|
|
|
|
while (uri_start < data + line_len && *uri_start == ' ') uri_start++;
|
|
|
|
|
const char *uri_end = data + line_len;
|
|
|
|
|
// Look for version part from the right
|
|
|
|
|
const char *version_start = memrchr(uri_start, ' ', uri_end - uri_start);
|
|
|
|
|
if (!version_start || version_start == uri_start) return 0;
|
|
|
|
|
|
|
|
|
|
// Set URI
|
|
|
|
|
size_t uri_len = version_start - uri_start;
|
|
|
|
|
if (uri_len >= sizeof(req->uri)) return 0;
|
|
|
|
|
memcpy(req->uri, uri_start, uri_len);
|
|
|
|
|
req->uri[uri_len] = '\0';
|
|
|
|
|
|
|
|
|
|
// Set Version
|
|
|
|
|
version_start++;
|
|
|
|
|
while (version_start < uri_end && *version_start == ' ') version_start++;
|
|
|
|
|
size_t version_len = uri_end - version_start;
|
|
|
|
|
if (version_len >= sizeof(req->version)) return 0;
|
|
|
|
|
memcpy(req->version, version_start, version_len);
|
|
|
|
|
req->version[version_len] = '\0';
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// HTTP/1.0 defaults to close
|
|
|
|
|
if (strncmp(req->version, "HTTP/1.0", 8) == 0) {
|
|
|
|
|
req->keep_alive = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
// Parse headers
|
|
|
|
|
const char *headers_start = line_end + 1;
|
|
|
|
|
char value[1024];
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
if (find_header_value(headers_start, len - (headers_start - data), "Host", req->host, sizeof(req->host))) {
|
|
|
|
|
char *port_colon = strchr(req->host, ':');
|
|
|
|
|
if (port_colon) *port_colon = '\0';
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
if (find_header_value(headers_start, len - (headers_start - data), "Content-Length", value, sizeof(value))) {
|
|
|
|
|
req->content_length = atol(value);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
// --- START GIT CLIENT FIX ---
|
|
|
|
|
// Check for chunked encoding, which is used by Git for pushes/pulls.
|
|
|
|
|
if (find_header_value(headers_start, len - (headers_start - data), "Transfer-Encoding", value, sizeof(value))) {
|
|
|
|
|
if (strcasecmp(value, "chunked") == 0) {
|
|
|
|
|
req->is_chunked = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// --- END GIT CLIENT FIX ---
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
if (find_header_value(headers_start, len - (headers_start - data), "Connection", value, sizeof(value))) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (strcasecmp(value, "close") == 0) {
|
|
|
|
|
req->keep_alive = 0;
|
|
|
|
|
req->connection_close = 1;
|
|
|
|
|
} else if (strcasecmp(value, "keep-alive") == 0) {
|
|
|
|
|
req->keep_alive = 1;
|
|
|
|
|
} else if (strcasecmp(value, "upgrade") == 0) {
|
2025-09-25 23:59:27 +02:00
|
|
|
// Upgrade implies a streaming connection, similar to chunked.
|
|
|
|
|
req->is_websocket = 1;
|
2025-09-25 05:21:40 +02:00
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
if (find_header_value(headers_start, len - (headers_start - data), "Upgrade", value, sizeof(value))) {
|
|
|
|
|
if (strcasecmp(value, "websocket") == 0) req->is_websocket = 1;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
void send_error_response(connection_t *conn, int code, const char* status, const char* body) {
|
|
|
|
|
if (!conn || !status || !body) return;
|
|
|
|
|
|
|
|
|
|
char response[2048];
|
|
|
|
|
int len = snprintf(response, sizeof(response),
|
|
|
|
|
"HTTP/1.1 %d %s\r\n"
|
|
|
|
|
"Content-Type: text/plain; charset=utf-8\r\n"
|
|
|
|
|
"Content-Length: %zu\r\n"
|
|
|
|
|
"Connection: close\r\n"
|
2025-09-25 05:21:40 +02:00
|
|
|
"Server: ReverseProxy/3.0\r\n"
|
2025-09-25 02:35:00 +02:00
|
|
|
"\r\n"
|
|
|
|
|
"%s",
|
|
|
|
|
code, status, strlen(body), body);
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
if (len > 0 && (size_t)len < sizeof(response)) {
|
|
|
|
|
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + len) == 0) {
|
|
|
|
|
memcpy(conn->write_buf.data + conn->write_buf.tail, response, len);
|
|
|
|
|
conn->write_buf.tail += len;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
|
|
|
|
|
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conn->state = CLIENT_STATE_ERROR;
|
2025-09-25 05:21:40 +02:00
|
|
|
conn->request.keep_alive = 0; // Force close after error
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
//
|
|
|
|
|
// Proxy Implementation
|
|
|
|
|
//
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
|
|
|
|
|
static void set_non_blocking(int fd) {
|
|
|
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
|
|
|
if (flags >= 0) {
|
|
|
|
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
static void set_tcp_keepalive(int fd) {
|
|
|
|
|
int yes = 1;
|
|
|
|
|
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes));
|
|
|
|
|
int idle = 60;
|
|
|
|
|
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
|
|
|
|
|
int interval = 10;
|
|
|
|
|
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
|
|
|
|
|
int maxpkt = 6;
|
|
|
|
|
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &maxpkt, sizeof(maxpkt));
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
static void add_to_epoll(int fd, uint32_t events) {
|
|
|
|
|
struct epoll_event event = { .data.fd = fd, .events = events };
|
|
|
|
|
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1) {
|
|
|
|
|
log_error("epoll_ctl_add failed");
|
|
|
|
|
close(fd);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void modify_epoll(int fd, uint32_t events) {
|
|
|
|
|
struct epoll_event event = { .data.fd = fd, .events = events };
|
|
|
|
|
if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event) == -1) {
|
2025-09-25 05:21:40 +02:00
|
|
|
// This can happen if fd was closed
|
2025-09-25 05:43:10 +02:00
|
|
|
if(errno != EBADF && errno != ENOENT) log_debug("epoll_ctl_mod failed for fd %d: %s", fd, strerror(errno));
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setup_listener_socket(int port) {
|
|
|
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
|
|
|
if (fd < 0) {
|
|
|
|
|
log_error("socket failed");
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int reuse = 1;
|
|
|
|
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
|
|
|
|
|
log_error("setsockopt SO_REUSEADDR failed");
|
|
|
|
|
close(fd);
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct sockaddr_in addr = {
|
|
|
|
|
.sin_family = AF_INET,
|
|
|
|
|
.sin_port = htons(port),
|
|
|
|
|
.sin_addr.s_addr = htonl(INADDR_ANY)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
|
|
|
|
log_error("bind failed");
|
|
|
|
|
close(fd);
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (listen(fd, SOMAXCONN) == -1) {
|
|
|
|
|
log_error("listen failed");
|
|
|
|
|
close(fd);
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set_non_blocking(fd);
|
|
|
|
|
add_to_epoll(fd, EPOLLIN);
|
|
|
|
|
|
|
|
|
|
connections[fd].type = CONN_TYPE_LISTENER;
|
|
|
|
|
connections[fd].fd = fd;
|
|
|
|
|
|
|
|
|
|
log_info("Listening on port %d (fd=%d)", port, fd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void accept_new_connection(int listener_fd) {
|
2025-09-25 05:21:40 +02:00
|
|
|
while (1) {
|
|
|
|
|
struct sockaddr_in client_addr;
|
|
|
|
|
socklen_t client_len = sizeof(client_addr);
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
int client_fd = accept(listener_fd, (struct sockaddr*)&client_addr, &client_len);
|
|
|
|
|
if (client_fd == -1) {
|
|
|
|
|
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
|
|
|
|
log_error("accept failed");
|
|
|
|
|
}
|
|
|
|
|
break;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
if (client_fd >= MAX_FDS) {
|
|
|
|
|
log_error("Connection fd too high, closing");
|
|
|
|
|
close(client_fd);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
set_non_blocking(client_fd);
|
|
|
|
|
set_tcp_keepalive(client_fd);
|
|
|
|
|
add_to_epoll(client_fd, EPOLLIN);
|
|
|
|
|
|
|
|
|
|
connection_t *conn = &connections[client_fd];
|
|
|
|
|
memset(conn, 0, sizeof(connection_t));
|
|
|
|
|
conn->type = CONN_TYPE_CLIENT;
|
|
|
|
|
conn->state = CLIENT_STATE_READING_HEADERS;
|
|
|
|
|
conn->fd = client_fd;
|
|
|
|
|
conn->last_activity = time(NULL);
|
|
|
|
|
|
|
|
|
|
if (buffer_init(&conn->read_buf, CHUNK_SIZE) < 0 ||
|
|
|
|
|
buffer_init(&conn->write_buf, CHUNK_SIZE) < 0) {
|
|
|
|
|
close_connection(client_fd);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
__sync_fetch_and_add(&monitor.active_connections, 1);
|
|
|
|
|
log_debug("New connection on fd %d from %s, total: %d",
|
|
|
|
|
client_fd, inet_ntoa(client_addr.sin_addr), monitor.active_connections);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 19:36:47 +02:00
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
void close_connection(int fd) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (fd < 0 || fd >= MAX_FDS) return;
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
connection_t *conn = &connections[fd];
|
2025-09-25 23:59:27 +02:00
|
|
|
if (conn->type == CONN_TYPE_UNUSED || conn->fd == -1) return;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
connection_t *pair = conn->pair;
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
// --- START: PERFECTIONIZED STATE RESET LOGIC ---
|
|
|
|
|
if (pair) {
|
|
|
|
|
// If the connection being closed is an upstream...
|
|
|
|
|
if (conn->type == CONN_TYPE_UPSTREAM && pair->type == CONN_TYPE_CLIENT) {
|
|
|
|
|
log_debug("Upstream fd %d is closing. Resetting client fd %d to READING_HEADERS.", fd, pair->fd);
|
|
|
|
|
|
|
|
|
|
// ...reset its client pair to the initial state, ready for a new request.
|
|
|
|
|
pair->state = CLIENT_STATE_READING_HEADERS;
|
|
|
|
|
pair->pair = NULL; // Unlink from the connection we are about to close.
|
|
|
|
|
|
|
|
|
|
// IMPORTANT: If the client already sent a pipelined request (like /dashboard),
|
|
|
|
|
// its data will be waiting in the buffer. Process it immediately.
|
|
|
|
|
if (buffer_available_read(&pair->read_buf) > 0) {
|
|
|
|
|
handle_client_read(pair);
|
|
|
|
|
}
|
|
|
|
|
} else if (conn->type == CONN_TYPE_CLIENT && pair->type == CONN_TYPE_UPSTREAM) {
|
|
|
|
|
// If a client closes, its upstream pair is now an orphan and should also be closed.
|
|
|
|
|
log_debug("Client fd %d is closing. Closing orphaned upstream pair fd %d.", fd, pair->fd);
|
|
|
|
|
// Unlink first to prevent recursion before closing the orphan.
|
|
|
|
|
pair->pair = NULL;
|
|
|
|
|
close_connection(pair->fd);
|
|
|
|
|
}
|
|
|
|
|
conn->pair = NULL;
|
2025-09-25 19:31:04 +02:00
|
|
|
}
|
2025-09-25 23:59:27 +02:00
|
|
|
// --- END: PERFECTIONIZED STATE RESET LOGIC ---
|
2025-09-25 19:31:04 +02:00
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
if (conn->vhost_stats && conn->request_start_time > 0) {
|
|
|
|
|
monitor_record_request_end(conn->vhost_stats, conn->request_start_time);
|
2025-09-25 05:21:40 +02:00
|
|
|
conn->request_start_time = 0;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
|
|
|
|
|
if (conn->type == CONN_TYPE_CLIENT) {
|
2025-09-25 23:59:27 +02:00
|
|
|
__sync_fetch_and_sub(&monitor.active_connections, 1);
|
2025-09-25 05:11:52 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
log_debug("Closing and cleaning up fd %d", fd);
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
|
|
|
|
|
|
|
|
|
|
if (conn->ssl) {
|
|
|
|
|
SSL_shutdown(conn->ssl);
|
|
|
|
|
SSL_free(conn->ssl);
|
|
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
close(fd);
|
|
|
|
|
|
|
|
|
|
buffer_free(&conn->read_buf);
|
|
|
|
|
buffer_free(&conn->write_buf);
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
memset(conn, 0, sizeof(connection_t));
|
2025-09-25 02:35:00 +02:00
|
|
|
conn->type = CONN_TYPE_UNUSED;
|
2025-09-25 05:21:40 +02:00
|
|
|
conn->fd = -1;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 19:31:04 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
void connect_to_upstream(connection_t *client, const char *data, size_t data_len) {
|
|
|
|
|
if (!client || !data) return;
|
|
|
|
|
|
|
|
|
|
route_config_t *route = find_route(client->request.host);
|
|
|
|
|
if (!route) {
|
2025-09-25 05:21:40 +02:00
|
|
|
send_error_response(client, 502, "Bad Gateway", "No route configured for this host");
|
2025-09-25 05:11:52 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
int up_fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
|
|
|
if (up_fd < 0) {
|
|
|
|
|
send_error_response(client, 502, "Bad Gateway", "Failed to create upstream socket");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (up_fd >= MAX_FDS) {
|
|
|
|
|
close(up_fd);
|
|
|
|
|
send_error_response(client, 502, "Bad Gateway", "Connection limit exceeded");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct sockaddr_in addr;
|
|
|
|
|
memset(&addr, 0, sizeof(addr));
|
|
|
|
|
addr.sin_family = AF_INET;
|
|
|
|
|
addr.sin_port = htons(route->upstream_port);
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// Resolve hostname
|
2025-09-25 02:35:00 +02:00
|
|
|
if (inet_pton(AF_INET, route->upstream_host, &addr.sin_addr) <= 0) {
|
|
|
|
|
struct hostent *he = gethostbyname(route->upstream_host);
|
|
|
|
|
if (!he) {
|
|
|
|
|
close(up_fd);
|
|
|
|
|
send_error_response(client, 502, "Bad Gateway", "Cannot resolve upstream hostname");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set_non_blocking(up_fd);
|
2025-09-25 05:21:40 +02:00
|
|
|
set_tcp_keepalive(up_fd);
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
int connect_result = connect(up_fd, (struct sockaddr*)&addr, sizeof(addr));
|
|
|
|
|
if (connect_result < 0 && errno != EINPROGRESS) {
|
|
|
|
|
close(up_fd);
|
|
|
|
|
send_error_response(client, 502, "Bad Gateway", "Failed to connect to upstream");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
add_to_epoll(up_fd, EPOLLIN | EPOLLOUT);
|
|
|
|
|
|
|
|
|
|
connection_t *up = &connections[up_fd];
|
|
|
|
|
memset(up, 0, sizeof(connection_t));
|
|
|
|
|
up->type = CONN_TYPE_UPSTREAM;
|
|
|
|
|
up->fd = up_fd;
|
|
|
|
|
up->last_activity = time(NULL);
|
|
|
|
|
|
|
|
|
|
client->pair = up;
|
|
|
|
|
up->pair = client;
|
|
|
|
|
up->vhost_stats = client->vhost_stats;
|
|
|
|
|
|
|
|
|
|
if (buffer_init(&up->read_buf, CHUNK_SIZE) < 0 ||
|
|
|
|
|
buffer_init(&up->write_buf, CHUNK_SIZE) < 0) {
|
2025-09-25 05:21:40 +02:00
|
|
|
close_connection(client->fd);
|
2025-09-25 02:35:00 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// Prepare data to send
|
2025-09-25 05:11:52 +02:00
|
|
|
char *data_to_send = (char*)data;
|
|
|
|
|
size_t len_to_send = data_len;
|
|
|
|
|
char *modified_request = NULL;
|
|
|
|
|
|
|
|
|
|
if (route->rewrite_host) {
|
|
|
|
|
char new_host_header[512];
|
|
|
|
|
const char *old_host_header_start = NULL;
|
|
|
|
|
const char *old_host_header_end = NULL;
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// Find the old host header
|
2025-09-25 05:11:52 +02:00
|
|
|
const char *current = data;
|
|
|
|
|
const char *end = data + data_len;
|
|
|
|
|
while(current < end) {
|
|
|
|
|
const char* line_end = memchr(current, '\n', end - current);
|
|
|
|
|
if (!line_end) break;
|
|
|
|
|
if (strncasecmp(current, "Host:", 5) == 0) {
|
|
|
|
|
old_host_header_start = current;
|
|
|
|
|
old_host_header_end = line_end + 1;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
current = line_end + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (old_host_header_start) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (route->upstream_port == 80 || route->upstream_port == 443) {
|
|
|
|
|
snprintf(new_host_header, sizeof(new_host_header), "Host: %s\r\n", route->upstream_host);
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(new_host_header, sizeof(new_host_header), "Host: %s:%d\r\n", route->upstream_host, route->upstream_port);
|
|
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
size_t new_host_len = strlen(new_host_header);
|
|
|
|
|
size_t old_host_len = old_host_header_end - old_host_header_start;
|
|
|
|
|
|
|
|
|
|
len_to_send = data_len - old_host_len + new_host_len;
|
2025-09-25 05:21:40 +02:00
|
|
|
modified_request = malloc(len_to_send + 1);
|
2025-09-25 05:11:52 +02:00
|
|
|
if (modified_request) {
|
|
|
|
|
char* p = modified_request;
|
|
|
|
|
// Copy part before Host header
|
2025-09-25 05:21:40 +02:00
|
|
|
size_t prefix_len = old_host_header_start - data;
|
|
|
|
|
memcpy(p, data, prefix_len);
|
|
|
|
|
p += prefix_len;
|
2025-09-25 05:11:52 +02:00
|
|
|
// Copy new Host header
|
|
|
|
|
memcpy(p, new_host_header, new_host_len);
|
|
|
|
|
p += new_host_len;
|
|
|
|
|
// Copy part after Host header
|
2025-09-25 05:21:40 +02:00
|
|
|
size_t suffix_len = data_len - (old_host_header_end - data);
|
|
|
|
|
memcpy(p, old_host_header_end, suffix_len);
|
2025-09-25 05:11:52 +02:00
|
|
|
data_to_send = modified_request;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (buffer_ensure_capacity(&up->write_buf, len_to_send) == 0) {
|
|
|
|
|
memcpy(up->write_buf.data, data_to_send, len_to_send);
|
|
|
|
|
up->write_buf.tail = len_to_send;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (modified_request) {
|
|
|
|
|
free(modified_request);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
if (route->use_ssl) {
|
|
|
|
|
up->ssl = SSL_new(ssl_ctx);
|
|
|
|
|
if (!up->ssl) {
|
2025-09-25 05:11:52 +02:00
|
|
|
close_connection(client->fd);
|
2025-09-25 02:35:00 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
|
|
|
|
|
SSL_set_tlsext_host_name(up->ssl, route->upstream_host); // SNI
|
2025-09-25 02:35:00 +02:00
|
|
|
SSL_set_fd(up->ssl, up_fd);
|
|
|
|
|
SSL_set_connect_state(up->ssl);
|
|
|
|
|
}
|
2025-09-25 05:43:10 +02:00
|
|
|
|
|
|
|
|
up->state = CLIENT_STATE_FORWARDING;
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
log_debug("Connecting to upstream %s:%d on fd %d (SSL: %s)",
|
|
|
|
|
route->upstream_host, route->upstream_port, up_fd, route->use_ssl ? "yes" : "no");
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
static int do_read(connection_t *conn) {
|
|
|
|
|
if (!conn) return -1;
|
2025-09-25 05:11:52 +02:00
|
|
|
|
|
|
|
|
buffer_t *buf = &conn->read_buf;
|
2025-09-25 05:21:40 +02:00
|
|
|
buffer_compact(buf);
|
|
|
|
|
|
|
|
|
|
size_t available = buffer_available_write(buf);
|
|
|
|
|
if (available == 0) {
|
|
|
|
|
// Try to grow the buffer
|
|
|
|
|
if (buffer_ensure_capacity(buf, buf->capacity * 2) < 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
available = buffer_available_write(buf);
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
int bytes_read;
|
|
|
|
|
if (conn->ssl && conn->ssl_handshake_done) {
|
2025-09-25 05:21:40 +02:00
|
|
|
bytes_read = SSL_read(conn->ssl, buf->data + buf->tail, available);
|
2025-09-25 02:35:00 +02:00
|
|
|
if (bytes_read <= 0) {
|
|
|
|
|
int ssl_error = SSL_get_error(conn->ssl, bytes_read);
|
|
|
|
|
if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) {
|
|
|
|
|
errno = EAGAIN;
|
2025-09-25 05:21:40 +02:00
|
|
|
return 0;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
return bytes_read;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2025-09-25 05:21:40 +02:00
|
|
|
bytes_read = read(conn->fd, buf->data + buf->tail, available);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
if (bytes_read > 0) {
|
|
|
|
|
buf->tail += bytes_read;
|
|
|
|
|
conn->last_activity = time(NULL);
|
|
|
|
|
if (conn->vhost_stats) {
|
|
|
|
|
monitor_record_bytes(conn->vhost_stats, 0, bytes_read);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
return bytes_read;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int do_write(connection_t *conn) {
|
2025-09-25 05:11:52 +02:00
|
|
|
if (!conn) return -1;
|
|
|
|
|
|
|
|
|
|
buffer_t *buf = &conn->write_buf;
|
2025-09-25 05:21:40 +02:00
|
|
|
size_t available = buffer_available_read(buf);
|
|
|
|
|
if (available == 0) return 0;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
int written;
|
|
|
|
|
if (conn->ssl && conn->ssl_handshake_done) {
|
2025-09-25 05:21:40 +02:00
|
|
|
written = SSL_write(conn->ssl, buf->data + buf->head, available);
|
2025-09-25 02:35:00 +02:00
|
|
|
if (written <= 0) {
|
|
|
|
|
int ssl_error = SSL_get_error(conn->ssl, written);
|
|
|
|
|
if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) {
|
|
|
|
|
errno = EAGAIN;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
return written;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2025-09-25 05:21:40 +02:00
|
|
|
written = write(conn->fd, buf->data + buf->head, available);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (written > 0) {
|
2025-09-25 05:21:40 +02:00
|
|
|
buffer_consume(buf, written);
|
|
|
|
|
conn->last_activity = time(NULL);
|
|
|
|
|
if (conn->vhost_stats) {
|
|
|
|
|
monitor_record_bytes(conn->vhost_stats, written, 0);
|
2025-09-25 05:11:52 +02:00
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
return written;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
void handle_client_read(connection_t *conn) {
|
2025-09-25 05:21:40 +02:00
|
|
|
int bytes_read = do_read(conn);
|
|
|
|
|
if (bytes_read < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
2025-09-25 06:37:51 +02:00
|
|
|
log_debug("[ROUTING] Closing connection fd=%d due to read error", conn->fd);
|
2025-09-25 02:35:00 +02:00
|
|
|
close_connection(conn->fd);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
buffer_t *buf = &conn->read_buf;
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 06:37:51 +02:00
|
|
|
if (conn->state == CLIENT_STATE_FORWARDING && conn->pair == NULL) {
|
|
|
|
|
conn->state = CLIENT_STATE_READING_HEADERS;
|
|
|
|
|
}
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 19:16:07 +02:00
|
|
|
if (conn->state == CLIENT_STATE_FORWARDING) {
|
2025-09-25 06:37:51 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-09-25 19:16:07 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
while (buffer_available_read(buf) > 0) {
|
2025-09-25 05:11:52 +02:00
|
|
|
char *data_start = buf->data + buf->head;
|
2025-09-25 05:21:40 +02:00
|
|
|
size_t data_len = buffer_available_read(buf);
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
char *headers_end = memmem(data_start, data_len, "\r\n\r\n", 4);
|
2025-09-25 19:16:07 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
if (!headers_end) {
|
|
|
|
|
if (data_len >= MAX_HEADER_SIZE) {
|
2025-09-25 19:16:07 +02:00
|
|
|
send_error_response(conn, 413, "Request Header Too Large", "Header is too large.");
|
2025-09-25 05:21:40 +02:00
|
|
|
return;
|
2025-09-25 05:11:52 +02:00
|
|
|
}
|
2025-09-25 19:16:07 +02:00
|
|
|
log_debug("fd %d: Incomplete headers, waiting for more data.", conn->fd);
|
2025-09-25 23:59:27 +02:00
|
|
|
break;
|
2025-09-25 05:11:52 +02:00
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 19:16:07 +02:00
|
|
|
size_t headers_len = (headers_end - data_start) + 4;
|
2025-09-25 05:21:40 +02:00
|
|
|
int parse_result = parse_http_request(data_start, headers_len, &conn->request);
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 19:16:07 +02:00
|
|
|
if (parse_result == 0) {
|
|
|
|
|
send_error_response(conn, 400, "Bad Request", "Malformed HTTP request.");
|
2025-09-25 02:35:00 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-09-25 19:16:07 +02:00
|
|
|
if (parse_result < 0) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
long long body_len = (conn->request.content_length > 0) ? conn->request.content_length : 0;
|
|
|
|
|
size_t total_request_len = headers_len + body_len;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
// --- START GIT CLIENT FIX ---
|
|
|
|
|
// For chunked requests (like git push), we don't know the body length.
|
|
|
|
|
// We must forward immediately after headers. The 'is_chunked' flag allows us to skip the body-length check.
|
|
|
|
|
if (!conn->request.is_chunked && data_len < total_request_len) {
|
2025-09-25 19:16:07 +02:00
|
|
|
// Body is not fully received yet, wait for more data.
|
|
|
|
|
log_debug("fd %d: Incomplete body, waiting for more data.", conn->fd);
|
|
|
|
|
break;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
// For chunked requests, we forward only the headers we have received.
|
|
|
|
|
// The rest of the chunked body will be streamed by handle_forwarding.
|
|
|
|
|
size_t len_to_forward = (conn->request.is_chunked) ? headers_len : total_request_len;
|
|
|
|
|
// --- END GIT CLIENT FIX ---
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
struct timespec ts;
|
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
|
|
|
conn->request_start_time = ts.tv_sec + ts.tv_nsec / 1e9;
|
2025-09-25 19:16:07 +02:00
|
|
|
|
|
|
|
|
if (strcmp(conn->request.method, "GET") == 0 &&
|
|
|
|
|
(strncmp(conn->request.uri, "/dashboard", 10) == 0 || strncmp(conn->request.uri, "/api/stats", 10) == 0)) {
|
|
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
// ... (rest of this if-block is unchanged)
|
|
|
|
|
log_info("[ROUTING-INTERNAL] Serving internal route %s for fd=%d", conn->request.uri, conn->fd);
|
2025-09-25 19:16:07 +02:00
|
|
|
if (conn->pair) {
|
|
|
|
|
close_connection(conn->pair->fd);
|
|
|
|
|
conn->pair = NULL;
|
2025-09-25 05:11:52 +02:00
|
|
|
}
|
2025-09-25 19:16:07 +02:00
|
|
|
conn->state = CLIENT_STATE_SERVING_INTERNAL;
|
|
|
|
|
if (strncmp(conn->request.uri, "/dashboard", 10) == 0) {
|
|
|
|
|
serve_dashboard(conn);
|
|
|
|
|
} else {
|
2025-09-25 05:11:52 +02:00
|
|
|
serve_stats_api(conn);
|
|
|
|
|
}
|
2025-09-25 23:59:27 +02:00
|
|
|
buffer_consume(buf, total_request_len);
|
2025-09-25 19:16:07 +02:00
|
|
|
if (!conn->request.keep_alive) {
|
|
|
|
|
conn->state = CLIENT_STATE_CLOSING;
|
2025-09-25 23:59:27 +02:00
|
|
|
return;
|
2025-09-25 19:16:07 +02:00
|
|
|
}
|
2025-09-25 23:59:27 +02:00
|
|
|
conn->state = CLIENT_STATE_READING_HEADERS;
|
|
|
|
|
continue;
|
2025-09-25 05:11:52 +02:00
|
|
|
}
|
2025-09-25 19:16:07 +02:00
|
|
|
|
|
|
|
|
log_info("[ROUTING-FORWARD] Forwarding request for fd=%d: %s %s", conn->fd, conn->request.method, conn->request.uri);
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
conn->vhost_stats = monitor_get_or_create_vhost_stats(conn->request.host);
|
|
|
|
|
monitor_record_request_start(conn->vhost_stats, conn->request.is_websocket);
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
conn->state = CLIENT_STATE_FORWARDING;
|
2025-09-25 23:59:27 +02:00
|
|
|
// --- MODIFIED LINE ---
|
|
|
|
|
connect_to_upstream(conn, data_start, len_to_forward);
|
|
|
|
|
|
|
|
|
|
// --- MODIFIED LINE ---
|
|
|
|
|
buffer_consume(buf, len_to_forward);
|
|
|
|
|
|
|
|
|
|
return;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
static void handle_forwarding(connection_t *conn) {
|
|
|
|
|
connection_t *pair = conn->pair;
|
2025-09-25 05:21:40 +02:00
|
|
|
if (!pair || pair->fd == -1) {
|
2025-09-25 06:37:51 +02:00
|
|
|
if (conn->type == CONN_TYPE_CLIENT) {
|
|
|
|
|
log_info("[ROUTING-ORPHAN] Client fd=%d lost upstream, resetting to READING_HEADERS", conn->fd);
|
|
|
|
|
conn->state = CLIENT_STATE_READING_HEADERS;
|
|
|
|
|
conn->pair = NULL;
|
|
|
|
|
if (buffer_available_read(&conn->read_buf) > 0) {
|
|
|
|
|
handle_client_read(conn);
|
|
|
|
|
}
|
2025-09-25 19:23:20 +02:00
|
|
|
} else {
|
|
|
|
|
close_connection(conn->fd);
|
2025-09-25 06:37:51 +02:00
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
int bytes_read = do_read(conn);
|
2025-09-25 23:59:27 +02:00
|
|
|
|
|
|
|
|
// --- START: GIT HALF-CLOSE FIX ---
|
|
|
|
|
if (bytes_read == 0) { // EOF received, meaning this side is done writing.
|
|
|
|
|
log_debug("EOF on fd %d, performing half-close on pair fd %d", conn->fd, pair->fd);
|
|
|
|
|
conn->half_closed = 1;
|
|
|
|
|
|
|
|
|
|
// Stop listening for reads on this socket, as it's closed.
|
|
|
|
|
modify_epoll(conn->fd, buffer_available_read(&conn->write_buf) ? EPOLLOUT : 0);
|
|
|
|
|
|
|
|
|
|
// Tell the other side we are done writing to it.
|
|
|
|
|
if (pair->fd != -1 && !pair->write_shutdown) {
|
|
|
|
|
if (shutdown(pair->fd, SHUT_WR) == -1 && errno != ENOTCONN) {
|
|
|
|
|
log_debug("shutdown(SHUT_WR) failed for fd %d: %s", pair->fd, strerror(errno));
|
|
|
|
|
}
|
|
|
|
|
pair->write_shutdown = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the other side had already signaled it was done writing, we can now fully close.
|
|
|
|
|
if (pair->half_closed) {
|
|
|
|
|
close_connection(conn->fd);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// --- END: GIT HALF-CLOSE FIX ---
|
|
|
|
|
|
|
|
|
|
if (bytes_read < 0 && (errno != EAGAIN && errno != EWOULDBLOCK)) {
|
2025-09-25 19:36:47 +02:00
|
|
|
close_connection(conn->fd);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
// Pipelining check remains unchanged...
|
2025-09-25 19:36:47 +02:00
|
|
|
if (bytes_read > 0 && conn->type == CONN_TYPE_CLIENT) {
|
2025-09-25 23:59:27 +02:00
|
|
|
// ...
|
2025-09-25 19:36:47 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
// Forwarding logic remains unchanged...
|
2025-09-25 05:21:40 +02:00
|
|
|
size_t data_to_forward = buffer_available_read(&conn->read_buf);
|
2025-09-25 05:11:52 +02:00
|
|
|
if (data_to_forward > 0) {
|
2025-09-25 19:23:20 +02:00
|
|
|
if (buffer_ensure_capacity(&pair->write_buf, pair->write_buf.tail + data_to_forward) < 0) {
|
2025-09-25 19:31:04 +02:00
|
|
|
close_connection(conn->fd);
|
2025-09-25 19:23:20 +02:00
|
|
|
return;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 06:37:51 +02:00
|
|
|
memcpy(pair->write_buf.data + pair->write_buf.tail,
|
|
|
|
|
conn->read_buf.data + conn->read_buf.head,
|
2025-09-25 05:21:40 +02:00
|
|
|
data_to_forward);
|
|
|
|
|
pair->write_buf.tail += data_to_forward;
|
|
|
|
|
buffer_consume(&conn->read_buf, data_to_forward);
|
|
|
|
|
modify_epoll(pair->fd, EPOLLIN | EPOLLOUT);
|
2025-09-25 05:11:52 +02:00
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 19:31:04 +02:00
|
|
|
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
static void handle_ssl_handshake(connection_t *conn) {
|
|
|
|
|
if (!conn->ssl || conn->ssl_handshake_done) return;
|
|
|
|
|
|
|
|
|
|
int ret = SSL_do_handshake(conn->ssl);
|
|
|
|
|
if (ret == 1) {
|
|
|
|
|
conn->ssl_handshake_done = 1;
|
|
|
|
|
log_debug("SSL handshake completed for fd %d", conn->fd);
|
|
|
|
|
|
|
|
|
|
// Process any pending data
|
|
|
|
|
if (buffer_available_read(&conn->write_buf) > 0) {
|
|
|
|
|
modify_epoll(conn->fd, EPOLLIN | EPOLLOUT);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
int ssl_error = SSL_get_error(conn->ssl, ret);
|
|
|
|
|
if (ssl_error == SSL_ERROR_WANT_READ) {
|
|
|
|
|
modify_epoll(conn->fd, EPOLLIN);
|
|
|
|
|
} else if (ssl_error == SSL_ERROR_WANT_WRITE) {
|
|
|
|
|
modify_epoll(conn->fd, EPOLLOUT);
|
|
|
|
|
} else {
|
|
|
|
|
log_debug("SSL handshake failed for fd %d: %d", conn->fd, ssl_error);
|
|
|
|
|
if (conn->pair) {
|
|
|
|
|
send_error_response(conn->pair, 502, "Bad Gateway", "SSL handshake failed");
|
|
|
|
|
} else {
|
|
|
|
|
close_connection(conn->fd);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-25 05:11:52 +02:00
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
static void handle_write_event(connection_t *conn) {
|
|
|
|
|
conn->last_activity = time(NULL);
|
|
|
|
|
|
|
|
|
|
if (conn->type == CONN_TYPE_UPSTREAM && !conn->ssl_handshake_done) {
|
2025-09-25 23:59:27 +02:00
|
|
|
// ... (this section is unchanged)
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
int written = do_write(conn);
|
2025-09-25 23:59:27 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
if (buffer_available_read(&conn->write_buf) == 0) {
|
2025-09-25 23:59:27 +02:00
|
|
|
// --- START: HALF-CLOSE COMPLETION LOGIC ---
|
|
|
|
|
// If our write buffer is empty AND we've been told to shut down writing...
|
|
|
|
|
if (conn->write_shutdown) {
|
|
|
|
|
// ...and the other side is also done, then this connection is finished.
|
|
|
|
|
if (conn->half_closed) {
|
|
|
|
|
close_connection(conn->fd);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// If the other side is not done, just stop listening for write events.
|
2025-09-25 05:21:40 +02:00
|
|
|
modify_epoll(conn->fd, EPOLLIN);
|
2025-09-25 05:11:52 +02:00
|
|
|
} else {
|
2025-09-25 23:59:27 +02:00
|
|
|
// --- END: HALF-CLOSE COMPLETION LOGIC ---
|
|
|
|
|
if (conn->state == CLIENT_STATE_ERROR ||
|
|
|
|
|
(conn->state == CLIENT_STATE_SERVING_INTERNAL && !conn->request.keep_alive)) {
|
|
|
|
|
close_connection(conn->fd);
|
|
|
|
|
} else if (conn->state == CLIENT_STATE_SERVING_INTERNAL && conn->request.keep_alive) {
|
|
|
|
|
conn->state = CLIENT_STATE_READING_HEADERS;
|
|
|
|
|
modify_epoll(conn->fd, EPOLLIN);
|
|
|
|
|
} else {
|
|
|
|
|
modify_epoll(conn->fd, EPOLLIN);
|
|
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
}
|
|
|
|
|
} else if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
|
|
|
|
close_connection(conn->fd);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 23:59:27 +02:00
|
|
|
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
void handle_connection_event(struct epoll_event *event) {
|
|
|
|
|
int fd = event->data.fd;
|
2025-09-25 05:21:40 +02:00
|
|
|
if (fd < 0 || fd >= MAX_FDS) return;
|
|
|
|
|
|
|
|
|
|
connection_t *conn = &connections[fd];
|
|
|
|
|
if (conn->type == CONN_TYPE_UNUSED || conn->fd == -1) return;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// Handle errors
|
2025-09-25 02:35:00 +02:00
|
|
|
if (event->events & (EPOLLERR | EPOLLHUP)) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (event->events & EPOLLERR) {
|
|
|
|
|
log_debug("EPOLLERR on fd %d", fd);
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
close_connection(fd);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (conn->type == CONN_TYPE_LISTENER) {
|
|
|
|
|
if (event->events & EPOLLIN) {
|
|
|
|
|
accept_new_connection(fd);
|
|
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
} else {
|
|
|
|
|
// Handle SSL handshake for upstream connections
|
|
|
|
|
if (conn->type == CONN_TYPE_UPSTREAM && conn->ssl && !conn->ssl_handshake_done) {
|
|
|
|
|
handle_ssl_handshake(conn);
|
|
|
|
|
if (!conn->ssl_handshake_done) {
|
|
|
|
|
return; // Still waiting for handshake
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process events
|
2025-09-25 02:35:00 +02:00
|
|
|
if (event->events & EPOLLOUT) {
|
|
|
|
|
handle_write_event(conn);
|
|
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
|
|
|
|
|
// Check if connection still exists after write
|
2025-09-25 05:11:52 +02:00
|
|
|
if (connections[fd].type != CONN_TYPE_UNUSED && (event->events & EPOLLIN)) {
|
|
|
|
|
if (conn->type == CONN_TYPE_CLIENT && conn->state == CLIENT_STATE_READING_HEADERS) {
|
|
|
|
|
handle_client_read(conn);
|
2025-09-25 05:21:40 +02:00
|
|
|
} else if (conn->state == CLIENT_STATE_FORWARDING) {
|
2025-09-25 05:11:52 +02:00
|
|
|
handle_forwarding(conn);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
// =================================================================================================
|
|
|
|
|
//
|
2025-09-25 05:11:52 +02:00
|
|
|
// Main Implementation
|
2025-09-25 02:35:00 +02:00
|
|
|
//
|
|
|
|
|
// =================================================================================================
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
static void signal_handler(int sig) {
|
|
|
|
|
if (sig == SIGINT || sig == SIGTERM) {
|
|
|
|
|
log_info("Received signal %d, shutting down...", sig);
|
|
|
|
|
g_shutdown = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
void create_default_config_if_not_exists(const char* filename) {
|
|
|
|
|
FILE *f = fopen(filename, "r");
|
|
|
|
|
if (f) {
|
|
|
|
|
fclose(f);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f = fopen(filename, "w");
|
|
|
|
|
if (!f) {
|
|
|
|
|
log_error("Cannot create default config file");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fprintf(f, "{\n"
|
|
|
|
|
" \"port\": 8080,\n"
|
|
|
|
|
" \"reverse_proxy\": [\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" \"hostname\": \"localhost\",\n"
|
|
|
|
|
" \"upstream_host\": \"127.0.0.1\",\n"
|
|
|
|
|
" \"upstream_port\": 3000,\n"
|
|
|
|
|
" \"use_ssl\": false,\n"
|
|
|
|
|
" \"rewrite_host\": true\n"
|
|
|
|
|
" },\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" \"hostname\": \"example.com\",\n"
|
|
|
|
|
" \"upstream_host\": \"127.0.0.1\",\n"
|
|
|
|
|
" \"upstream_port\": 5000,\n"
|
|
|
|
|
" \"use_ssl\": false,\n"
|
|
|
|
|
" \"rewrite_host\": false\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" ]\n"
|
|
|
|
|
"}\n");
|
|
|
|
|
fclose(f);
|
|
|
|
|
log_info("Created default config file: %s", filename);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void init_ssl() {
|
|
|
|
|
SSL_load_error_strings();
|
|
|
|
|
OpenSSL_add_ssl_algorithms();
|
|
|
|
|
ssl_ctx = SSL_CTX_new(TLS_client_method());
|
|
|
|
|
if (!ssl_ctx) {
|
|
|
|
|
log_error("Failed to create SSL context");
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_NONE, NULL);
|
|
|
|
|
SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
|
2025-09-25 05:21:40 +02:00
|
|
|
SSL_CTX_set_mode(ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void cleanup() {
|
2025-09-25 05:21:40 +02:00
|
|
|
log_info("Cleaning up resources...");
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
for (int i = 0; i < MAX_FDS; i++) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (connections[i].type != CONN_TYPE_UNUSED && connections[i].fd != -1) {
|
2025-09-25 02:35:00 +02:00
|
|
|
close_connection(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
free_config();
|
|
|
|
|
monitor_cleanup();
|
|
|
|
|
|
|
|
|
|
if (epoll_fd >= 0) {
|
|
|
|
|
close(epoll_fd);
|
2025-09-25 05:21:40 +02:00
|
|
|
epoll_fd = -1;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ssl_ctx) {
|
|
|
|
|
SSL_CTX_free(ssl_ctx);
|
2025-09-25 05:21:40 +02:00
|
|
|
ssl_ctx = NULL;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EVP_cleanup();
|
2025-09-25 05:21:40 +02:00
|
|
|
log_info("Cleanup complete");
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void cleanup_idle_connections() {
|
|
|
|
|
time_t current_time = time(NULL);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < MAX_FDS; i++) {
|
|
|
|
|
connection_t *conn = &connections[i];
|
2025-09-25 05:21:40 +02:00
|
|
|
if (conn->type != CONN_TYPE_UNUSED &&
|
|
|
|
|
conn->type != CONN_TYPE_LISTENER &&
|
|
|
|
|
conn->fd != -1) {
|
|
|
|
|
if (current_time - conn->last_activity > CONNECTION_TIMEOUT) {
|
2025-09-25 02:35:00 +02:00
|
|
|
log_debug("Closing idle connection fd=%d", i);
|
|
|
|
|
close_connection(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:11:52 +02:00
|
|
|
void free_config() {
|
|
|
|
|
if (config.routes) {
|
|
|
|
|
free(config.routes);
|
|
|
|
|
config.routes = NULL;
|
|
|
|
|
}
|
|
|
|
|
config.route_count = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 02:35:00 +02:00
|
|
|
int main(int argc, char *argv[]) {
|
2025-09-25 05:21:40 +02:00
|
|
|
// Setup signal handlers
|
2025-09-25 02:35:00 +02:00
|
|
|
signal(SIGPIPE, SIG_IGN);
|
2025-09-25 05:21:40 +02:00
|
|
|
signal(SIGINT, signal_handler);
|
|
|
|
|
signal(SIGTERM, signal_handler);
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
if (getenv("DEBUG")) {
|
|
|
|
|
g_debug_mode = 1;
|
|
|
|
|
log_info("Debug mode enabled");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char *config_file = (argc > 1) ? argv[1] : "proxy_config.json";
|
|
|
|
|
create_default_config_if_not_exists(config_file);
|
|
|
|
|
|
|
|
|
|
if (!load_config(config_file)) {
|
|
|
|
|
fprintf(stderr, "Failed to load configuration\n");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init_ssl();
|
|
|
|
|
monitor_init("proxy_stats.db");
|
|
|
|
|
|
|
|
|
|
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
|
|
|
|
|
if (epoll_fd == -1) {
|
|
|
|
|
log_error("epoll_create1 failed");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
// Initialize all connections as unused
|
2025-09-25 02:35:00 +02:00
|
|
|
for (int i = 0; i < MAX_FDS; i++) {
|
|
|
|
|
connections[i].type = CONN_TYPE_UNUSED;
|
2025-09-25 05:21:40 +02:00
|
|
|
connections[i].fd = -1;
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setup_listener_socket(config.port);
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
log_info("=========================================");
|
|
|
|
|
log_info("Reverse proxy v3.0 running on port %d", config.port);
|
|
|
|
|
log_info("Dashboard: http://localhost:%d/dashboard", config.port);
|
|
|
|
|
log_info("API Stats: http://localhost:%d/api/stats", config.port);
|
|
|
|
|
log_info("=========================================");
|
2025-09-25 02:35:00 +02:00
|
|
|
|
|
|
|
|
atexit(cleanup);
|
|
|
|
|
|
|
|
|
|
struct epoll_event events[MAX_EVENTS];
|
2025-09-25 05:21:40 +02:00
|
|
|
time_t last_monitor_update = 0;
|
|
|
|
|
time_t last_cleanup = 0;
|
2025-09-25 02:35:00 +02:00
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
while (!g_shutdown) {
|
2025-09-25 02:35:00 +02:00
|
|
|
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);
|
|
|
|
|
if (n == -1) {
|
2025-09-25 05:21:40 +02:00
|
|
|
if (errno == EINTR) continue;
|
2025-09-25 02:35:00 +02:00
|
|
|
log_error("epoll_wait failed");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
|
handle_connection_event(&events[i]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
time_t current_time = time(NULL);
|
|
|
|
|
|
|
|
|
|
// Update monitoring stats every second
|
|
|
|
|
if (current_time > last_monitor_update) {
|
2025-09-25 02:35:00 +02:00
|
|
|
monitor_update();
|
|
|
|
|
last_monitor_update = current_time;
|
|
|
|
|
}
|
2025-09-25 05:21:40 +02:00
|
|
|
|
|
|
|
|
// Cleanup idle connections every 60 seconds
|
|
|
|
|
if (current_time - last_cleanup >= 60) {
|
|
|
|
|
cleanup_idle_connections();
|
|
|
|
|
last_cleanup = current_time;
|
|
|
|
|
}
|
2025-09-25 02:35:00 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 05:21:40 +02:00
|
|
|
log_info("Shutdown complete");
|
2025-09-25 02:35:00 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|