From 3d13336d22868e8608aacda482cf6c3535504ea4 Mon Sep 17 00:00:00 2001 From: Kouya Heika Date: Sat, 18 Apr 2026 04:42:13 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 7 + .gitmodules | 3 + CMakeLists.txt | 17 +++ config.ini.example | 35 +++++ deps/vector | 1 + src/CMakeLists.txt | 5 + src/main.c | 183 ++++++++++++++++++++++++ src/main.h | 213 ++++++++++++++++++++++++++++ src/services.c | 347 +++++++++++++++++++++++++++++++++++++++++++++ src/services.h | 80 +++++++++++ src/util.h | 14 ++ www/style.css | 3 + 12 files changed, 908 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 config.ini.example create mode 160000 deps/vector create mode 100644 src/CMakeLists.txt create mode 100644 src/main.c create mode 100644 src/main.h create mode 100644 src/services.c create mode 100644 src/services.h create mode 100644 src/util.h create mode 100644 www/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fdd971c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Editor directories +.ecode/ + +# Project files +build/ +config.ini +messages diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9d78ede --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "deps/vector"] + path = deps/vector + url = https://github.com/goldsborough/vector.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..31ebc9e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.10) +project(stethoscope + VERSION 1.0 + LANGUAGES C) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "RelWithDebInfo") +endif() + +include_directories(${CMAKE_BINARY_DIR} ${PROJECT_SOURCE_DIR}) + +add_subdirectory(src) + +#file(COPY resources DESTINATION ${CMAKE_BINARY_DIR}) +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) \ No newline at end of file diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..fc4e148 --- /dev/null +++ b/config.ini.example @@ -0,0 +1,35 @@ +## STETHOSCOPE EXAMPLE CONFIG + +# The name of this Stethoscope instance. This name is used for the page title. (default: "My Stethoscope") +service_name=My Stethoscope + +# Port to listen on. This should be reverse proxied by something like httpd or nginx. (default: 7800) +listen=7800 + +# Path of the webroot server uses to load asset paths like the stylesheet. (default: ) +webroot= + +# Time in seconds to wait before attempting to reconnect to the endpoint. (default: 5) +backoff=5 + +# These are example services. +;[service.openssh] +;name=OpenSSH Daemon +;address=tcp://localhost:22 + +;[service.nginx] +;name=Nginx +;address=tcp://localhost:80,tcp://localhost:443,process://nginx,pidfile:///run/nginx.pid +;backoff=1 + +;[service.email] +;name=Email +;address=tcp://localhost:25,tcp://localhost:143 + +;[service.tor] +;name=Tor +;address=tcp://127.0.0.1:9050 + +;[service.mumble] +;name=mumble +;address=tcp://localhost:64738,udp://localhost:64738 \ No newline at end of file diff --git a/deps/vector b/deps/vector new file mode 160000 index 0000000..40efe82 --- /dev/null +++ b/deps/vector @@ -0,0 +1 @@ +Subproject commit 40efe822562493396fc02cb13e75f6d52f0fb466 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..2df0b89 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(${PROJECT_NAME} + main.c + ../deps/vector/vector.c +) +target_link_libraries(${PROJECT_NAME} PUBLIC microhttpd pthread inih mxml) \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..46fd5f0 --- /dev/null +++ b/src/main.c @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include +#include + +#include "main.h" + +#define CONFIG_SERVICE_PREFIX "service." + +static void signal_handler(int signo) { + if (signo == SIGINT || signo == SIGKILL) { + running = 0; + } else if (signo == SIGUSR1) { + printf("USR1 signal detected! Not doing anything for now.\n"); + } +} + +// Only used to construct basic variables of a service before initialization +struct PreService { + char* id; + char* name; + char* text; + char* endpoints; + uint32_t backoff; +}; + +static int config_handler(void* user, const char* section, const char* name, const char* value) { + if (strcmp(section, "") == 0) { + if (strcmp(name, "service_name") == 0) { + daemon_name = malloc(strlen(value)+1); + strcpy(daemon_name, value); + } else if (strcmp(name, "webroot") == 0) { + webroot = malloc(strlen(value)+1); + strcpy(webroot, value); + } else if (strcmp(name, "listen") == 0) { + listen_port = atoi(value); + } else if (strcmp(name, "backoff") == 0) { + global_backoff = atoi(value); + } else { + printf("Unknown configuration entry \'%s\'\n", name); + } + } else if (strncmp(CONFIG_SERVICE_PREFIX, section, strlen(CONFIG_SERVICE_PREFIX)) == 0) { + const char* id = section+strlen(CONFIG_SERVICE_PREFIX); + Vector* tmp_services = (Vector*)user; + struct PreService* current_service = NULL; + + Iterator it = vector_begin(tmp_services); + Iterator end = vector_end(tmp_services); + for (; !iterator_equals(&it, &end); iterator_increment(&it)) { + struct PreService* current = *(struct PreService**)iterator_get(&it); + if (strcmp(id, current->id) == 0) { + current_service = current; + break; + } + } + if (current_service == NULL) { + current_service = malloc(4096); + current_service->id = malloc(strlen(id)+1); + strcpy(current_service->id, id); + current_service->backoff = 0; + vector_push_back(tmp_services, ¤t_service); + } + if (strcmp(name, "name") == 0) { + current_service->name = malloc(strlen(value)+1); + strcpy(current_service->name, value); + } else if (strcmp(name, "text") == 0) { + current_service->text = malloc(strlen(value)+1); + strcpy(current_service->text, value); + } else if (strcmp(name, "address") == 0) { + current_service->endpoints = malloc(strlen(value)+1); + strcpy(current_service->endpoints, value); + } else if (strcmp(name, "backoff") == 0) { + current_service->backoff = atoi(value); + } else { + printf("Unknown configuration entry \'%s\' in section \'%s\'\n", name, id); + } + } else { + printf("Unknown configuration section \'%s\'\n", section); + } + fflush(stdout); + return 1; +} + +static void init_signals() { + struct sigaction sa = {}; + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + sigaction(SIGINT, &sa, NULL); + sigaction(SIGKILL, &sa, NULL); + sigaction(SIGUSR1, &sa, NULL); + + sa.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &sa, NULL); +} + +int main(int argc, char** argv) { + printf("Starting Stethoscope...\n"); + fflush(stdout); + init_signals(); + + vector_setup(&status_timeline, 100, sizeof(struct StatusPeroid*)); + + char* config_path; + for (int c = 1; c < argc; c++) { + if (strcmp(argv[c], "-c") == 0) { + if (argc > c) { + config_path = argv[c+1]; + c++; + break; + } + } + goto broken_args; + } + + if (argc <= 1) { + broken_args: + fprintf(stderr, "error: invalid number of arguments\nexample: %s -c \n", argv[0]); + fflush(stderr); + return 3; + } + + // Load configuration file + Vector tmp_services; + vector_setup(&tmp_services, 255, sizeof(struct PreService*)); + printf("Loading configuration file: %s\n", config_path); + int err = ini_parse(config_path, config_handler, &tmp_services); + if (err < 0) { + fprintf(stderr, "Failed to load configuration file: %i\n", err); + return 1; + } + + // initialize Services from temp vector + vector_setup(&services, 255, sizeof(struct Service*)); + Iterator it = vector_begin(&tmp_services); + Iterator end = vector_end(&tmp_services); + for (; !iterator_equals(&it, &end); iterator_increment(&it)) { + struct PreService* preservice = *(struct PreService**)iterator_get(&it); + printf("Initializing service \"%s\"\n", preservice->name); + fflush(stdout); + struct Service* service = malloc(8192); + service->id = preservice->id; + service->name = preservice->name; + service->text = preservice->text; + service->last_state = DOWN; + service->backoff = preservice->backoff == 0 ? global_backoff : preservice->backoff; + + char* endpoints = malloc(strlen(preservice->endpoints)+1); + strcpy(endpoints, preservice->endpoints); + char* token = strtok(endpoints, ","); + vector_setup(&service->endpoints, 255, sizeof(struct Endpoint*)); + while (token != NULL) { + add_endpoint(service, token); + fflush(stdout); + token = strtok(NULL, ","); + } + vector_push_back(&services, &service); + load_service(&service); + } + vector_resize(&services, services.size); + vector_clear(&tmp_services); + vector_destroy(&tmp_services); + + struct MHD_Daemon *daemon = MHD_start_daemon(MHD_USE_AUTO | MHD_USE_DUAL_STACK | MHD_USE_INTERNAL_POLLING_THREAD, listen_port, NULL, NULL, &http_response, NULL, MHD_OPTION_END); + if (daemon == NULL) { + fprintf(stderr, "Failed to start HTTP daemon\n"); + return 2; + } + running = 1; + while (daemon != NULL && running == 1) { + if (report_queued > 0) { + report_queued = 0; + report(); + } else sleep(global_backoff / 2); + } + printf("\nShutting down services...\n"); + fflush(stdout); + MHD_stop_daemon(daemon); + return 0; +} \ No newline at end of file diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..164928c --- /dev/null +++ b/src/main.h @@ -0,0 +1,213 @@ +#ifndef H_MAIN +#define H_MAIN +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../deps/vector/vector.h" +#include "mxml.h" + +#include "util.h" +#include "services.c" + +#define PAGE_INDEX 0 +#define PAGE_ASSET 1 +#define PAGE_ERROR 2 + +uint8_t running = 0; +char* daemon_name = "Stethoscope"; +uint16_t listen_port = 7800; +char* webroot = "share/"; +uint32_t global_backoff = 5; + +char* cached_index; + +int get_asset(char** buffer, const char* path, size_t* size) { + *size = 0; + if (strlen(webroot) + strlen(path) + 2 > PATH_MAX) + return 403; + + char full_path[PATH_MAX]; + snprintf(full_path, sizeof(full_path), "%s%s", webroot, path); + + char resolved_path[PATH_MAX]; + if (realpath(full_path, resolved_path) == NULL) + return 403; + + char resolved_base[PATH_MAX]; + if (realpath(webroot, resolved_base) == NULL) + return 403; + + size_t resolved_base_len = strlen(resolved_base); + if (strncmp(resolved_path, resolved_base, resolved_base_len) != 0) + return 400; + + if (resolved_path[resolved_base_len] != '\0' && resolved_path[resolved_base_len] != '/') + return 400; + + FILE* asset = fopen(resolved_path, "rb"); + if (!asset) return 404; + + fseek(asset, 0, SEEK_END); + *size = ftell(asset); + rewind(asset); + + *buffer = (char*)malloc(*size + 1); + if (!*buffer) { + fclose(asset); + *size = 0; + return 500; + } + + size_t bytes_read = fread(*buffer, 1, *size, asset); + fclose(asset); + + if (bytes_read != *size) { + free(*buffer); + *size = 0; + return 500; + } + + buffer[*size] = 0; + + return 200; +}; + +struct XMLServiceRow { + struct Service* service; + mxml_node_t* row; +}; + +static enum MHD_Result http_response(void *cls, struct MHD_Connection *conn, const char* uri, const char* method, const char* version, const char* upload_Data, size_t* upload_data_size, void** req_cls) { + const union MHD_ConnectionInfo *info = MHD_get_connection_info(conn, MHD_CONNECTION_INFO_CLIENT_ADDRESS); + const char *client_ip = {}; + uint16_t client_port = 0; + if (info != NULL) { + const struct sockaddr *addr = info->client_addr; + char ip_str[INET6_ADDRSTRLEN]; + + if (addr->sa_family == AF_INET) { + const struct sockaddr_in *addr4 = (const struct sockaddr_in *)addr; + client_ip = inet_ntop(AF_INET, &addr4->sin_addr, ip_str, sizeof(ip_str)); + client_port = ntohs(addr4->sin_port); + } else if (addr->sa_family == AF_INET6) { + const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)addr; + client_ip = inet_ntop(AF_INET6, &addr6->sin6_addr, ip_str, sizeof(ip_str)); + client_port = ntohs(addr6->sin6_port); + } + } else client_ip = "?.?.?.?"; + + uint16_t response_code = 0; + char* page; + size_t page_size = 0; + + if (strcmp(method, "GET") == 0) { + if (strcmp(uri, "/") != 0) + response_code = get_asset(&page, uri, &page_size); + } else response_code = 400; + + if (response_code != 200) { + mxml_node_t* doc = mxmlNewXML("1.0"); + mxml_node_t* html = mxmlNewElement(doc, "html"); + mxmlElementSetAttr(html, "xmlns", "http://www.w3.org/1999/xhtml"); + mxmlElementSetAttr(html, "xml:lang", "en"); + mxmlElementSetAttr(html, "lang", "en"); + + mxml_node_t* head = mxmlNewElement(html, "head"); + mxml_node_t *title = mxmlNewElement(head, "title"); + mxmlNewText(title, 0, daemon_name); + + mxml_node_t* meta_charset = mxmlNewElement(head, "meta"); + mxmlElementSetAttr(meta_charset, "http-equiv", "Content-Type"); + mxmlElementSetAttr(meta_charset, "content", "text/html; charset=utf-8"); + + mxml_node_t* meta_viewport = mxmlNewElement(head, "meta"); + mxmlElementSetAttr(meta_viewport, "name", "viewport"); + mxmlElementSetAttr(meta_viewport, "content", "width=device-width, initial-scale=1.0"); + + mxml_node_t* stylesheet = mxmlNewElement(head, "link"); + mxmlElementSetAttr(stylesheet, "rel", "stylesheet"); + mxmlElementSetAttr(stylesheet, "href", "style.css"); + + mxml_node_t* body = mxmlNewElement(html, "body"); + + mxml_node_t* center = mxmlNewElement(body, "center"); + + if (strcmp(uri, "/") == 0 && response_code == 0) { + mxml_node_t* header = mxmlNewElement(center, "h1"); + mxmlNewText(header, 0, daemon_name); + + int row_index = 0; + struct XMLServiceRow table_rows[services.size]; + + Iterator it = vector_begin(&services); + Iterator end = vector_end(&services); + for (; !iterator_equals(&it, &end); iterator_increment(&it)) { + struct Service* service = *(struct Service**)iterator_get(&it); + mxml_node_t* table = mxmlNewElement(center, "table"); + mxmlElementSetAttr(table, "width", "100%"); + mxml_node_t* header_row = mxmlNewElement(table, "tr"); + mxml_node_t* header_cell = mxmlNewElement(table, "th"); + mxmlElementSetAttr(header_cell, "align", "left"); + mxmlNewText(header_cell, 0, service->name); + + mxml_node_t* status_row = mxmlNewElement(table, "tr"); + + table_rows[row_index] = (struct XMLServiceRow){ service, status_row }; + row_index++; + } + + + + Iterator pit = vector_begin(&status_timeline); + Iterator pend = vector_end(&status_timeline); + for (; !iterator_equals(&pit, &pend); iterator_increment(&pit)) { + struct StatusPeroid* peroid = *(struct StatusPeroid**)iterator_get(&pit); + + Iterator sit = vector_begin(&peroid->server_statuses); + Iterator send = vector_end(&peroid->server_statuses); + for (; !iterator_equals(&sit, &send); iterator_increment(&sit)) { + struct ServerStatus* srvstat = *(struct ServerStatus**)iterator_get(&sit); + + struct XMLServiceRow row_struct; + for (int x = 0; x < services.size; x++) + if (table_rows[x].service == srvstat->service) { + row_struct = table_rows[x]; + break; + } + + mxml_node_t* cell = mxmlNewElement(row_struct.row, "td"); + mxmlElementSetAttr(cell, "_status", state_text(srvstat->state)); + char* ping_str = (char*)malloc(16); + snprintf(ping_str, 16, "%f ms", srvstat->ping); + mxmlElementSetAttr(cell, "_ping", ping_str); + } + } + + response_code = 200; + } else { + mxml_node_t* error_header = mxmlNewElement(center, "h1"); + char* err_str = (char*)malloc(5); + snprintf(err_str, 5, "%i", response_code); + mxmlNewText(error_header, 0, err_str); + } + + page = mxmlSaveAllocString(doc, MXML_NO_CALLBACK); + page_size = strlen(page); + } + + printf("[%lu] %s %s \"%s\" %s:%u %zu %zu %i\n", time(NULL), method, uri, version, client_ip, client_port, *upload_data_size, page_size, response_code); + fflush(stdout); + + struct MHD_Response* response = MHD_create_response_from_buffer_static(page_size, page); + enum MHD_Result ret = MHD_queue_response(conn, response_code, response); + MHD_destroy_response(response); + return ret; +}; + +#endif \ No newline at end of file diff --git a/src/services.c b/src/services.c new file mode 100644 index 0000000..db33aa6 --- /dev/null +++ b/src/services.c @@ -0,0 +1,347 @@ +#include +#include +#include +#include +#include "services.h" + +#ifndef _GNU_SOURCE +#define POLLRDHUP 0x2000 +#endif + +uint8_t report_queued = 0; +uint64_t queued_time = 0; +Vector services; +Vector status_timeline; // of StatusPeroid* + +void* run_endpoint(void* endpoint) { + struct Endpoint* self = (struct Endpoint*)endpoint; + struct timespec start, end; + while (self->enabled) { + int success = 0; + if (strcmp(self->scheme, "tcp") == 0 || strcmp(self->scheme, "udp") == 0) { + if (self->sockfd < 0) { + struct addrinfo hints = {0}, *result, *rp; + hints.ai_family = AF_UNSPEC; + if (strcmp(self->scheme, "udp") == 0) { + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + } else { + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + } + + char port_str[6]; + snprintf(port_str, sizeof(port_str), "%d", self->port); + + int ret = getaddrinfo(self->target, port_str, &hints, &result); + if (ret != 0) { + fprintf(stderr, "getaddrinfo failed: %s\n", gai_strerror(ret)); + queue_report((struct EndpointStatus){ self, DOWN, -1.0 }); + sleep(self->backoff); + continue; + } + + self->sockfd = -1; + for (rp = result; rp != NULL; rp = rp->ai_next) { + int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) continue; + + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + if (strcmp(self->scheme, "tcp") == 0) { + int opt = 1; + setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt)); + + int keepidle = 0; + setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle)); + + int keepintvl = self->backoff; + setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl)); + + int keepcnt = 1; + setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt)); + } + + struct timespec connect_start; + clock_gettime(CLOCK_REALTIME_ALARM, &connect_start); + + ret = connect(fd, rp->ai_addr, rp->ai_addrlen); + if (strcmp(self->scheme, "tcp") == 0) { + if (errno == EINPROGRESS) { + struct pollfd pfd = { .fd = fd, .events = POLLOUT }; + int pollret = poll(&pfd, 1, 5000); + + if (pollret > 0 && (pfd.revents & POLLOUT)) { + int error = 0; + socklen_t len = sizeof(error); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len) != 0 && error != 0) { + close(fd); + continue; + } + } + } + } else { + char* dummy = 0; + ssize_t sent = send(fd, &dummy, 1, MSG_DONTWAIT | MSG_NOSIGNAL); + if (sent < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { + close(fd); + continue; + } + + usleep(10000); + + int error = 0; + socklen_t len = sizeof(error); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len) != 0 || error != 0) { + close(fd); + continue; + } + + struct pollfd pfd = { .fd = fd, .events = POLLIN | POLLERR }; + if (poll(&pfd, 1, (self->backoff > 0 ? self->backoff * 1000 : 10000)) > 0 && (pfd.revents & (POLLERR | POLLIN))) { + if (recv(fd, NULL, sizeof(NULL), MSG_DONTWAIT) < 0) { + self->last_errno = errno; + if (self->last_errno == ECONNREFUSED || self->last_errno == EHOSTUNREACH || self->last_errno == ENETUNREACH) { + close(fd); + continue; + } + } + } + } + struct timespec now; + clock_gettime(CLOCK_REALTIME_ALARM, &now); + + double ping = (now.tv_sec - connect_start.tv_sec) * 1000.0 + (now.tv_nsec - connect_start.tv_nsec) / 1000000.0; + + self->sockfd = fd; + self->last_errno = 0; + queue_report((struct EndpointStatus){ self, UP, ping }); + } + freeaddrinfo(result); + + if (self->sockfd < 0) { + if (self->last_errno != errno) { + self->last_errno = errno; + fprintf(stderr, "%s://%s:%u connect failed: %i\n", self->scheme, self->target, self->port, errno); + } + queue_report((struct EndpointStatus){ self, DOWN, -1.0 }); + sleep(self->backoff); + continue; + } + } + + struct pollfd pfd = { .fd = self->sockfd, .events = POLLERR | POLLHUP | POLLRDHUP }; + int pollret = poll(&pfd, 1, (self->backoff > 0 ? self->backoff * 1000 : 10000)); + + if (pollret < 0) { + close(self->sockfd); + self->sockfd = -1; + continue; + } + + if (pfd.revents & (POLLERR | POLLHUP | POLLRDHUP)) { + close(self->sockfd); + self->sockfd = -1; + //queue_report((struct EndpointStatus){ self, DOWN, -1.0 }); + continue; + } + + if (strcmp(self->scheme, "udp") == 0) { + if (send(self->sockfd, NULL, sizeof(NULL), 0) < 0) { + close(self->sockfd); + self->sockfd = -1; + continue; + } + } + + if (pollret == 0) { + time_t now = time(NULL); + if (now - self->last_check >= self->backoff) { + self->last_check = now; + queue_report((struct EndpointStatus){ self, UP, self->last_ping }); + } + continue; + } + + queue_report((struct EndpointStatus){ self, DOWN, -1.0 }); + } + } + + if (self->sockfd >= 0) { + close(self->sockfd); + self->sockfd = -1; + } + return NULL; +} + +void queue_report(struct EndpointStatus status) { + status.endpoint->last_ping = status.ping; + if (status.endpoint->last_state == status.state) return; + status.endpoint->last_state = status.state; + + uint64_t time = microtime(); + printf("[%lu] State of %s://%s:%u is %s\n", (time / 1000000), status.endpoint->scheme, status.endpoint->target, status.endpoint->port, state_text(status.state)); + fflush(stdout); + + queued_time = time; + report_queued = 1; +} + +void load_service(struct Service** self) { + Iterator it = vector_begin(&(*self)->endpoints); + Iterator end = vector_end(&(*self)->endpoints); + for (; !iterator_equals(&it, &end); iterator_increment(&it)) { + struct Endpoint* endpoint = *(struct Endpoint**)iterator_get(&it); + pthread_create(&endpoint->thread, NULL, &run_endpoint, (void *)endpoint); + } +} + +uint8_t add_endpoint(struct Service* self, const char* uri) { + if (!uri || !*uri) return 255; + struct Endpoint* endpoint = (struct Endpoint*)malloc(2048); + endpoint->service = self; + endpoint->enabled = true; + endpoint->sockfd = -1; + endpoint->backoff = self->backoff; + + const char* p = uri; + const char* colon = strchr(p, ':'); + if (!colon) { + fprintf(stderr, "Target URI is malformed: %s\n", p); + return 1; + } + + size_t scheme_len = colon - p; + endpoint->scheme = (char*)malloc(scheme_len+1); + if (endpoint->scheme) { + strncpy(endpoint->scheme, p, scheme_len); + endpoint->scheme[scheme_len] = 0; + for (int i = 0; i < scheme_len; i++) + endpoint->scheme[i] = (char)tolower(endpoint->scheme[i]); + } + + p = colon + 1; + if (!(*p == '/' && *(p+1) == '/')) { + fprintf(stderr, "Target URI is malformed: %s\n", p); + return 1; + } + p += 2; + + const char* addr_start = p; + const char* addr_end = NULL; + if (*p == '[') { + ++p; + addr_start = p; + addr_end = strchr(p, ']'); + if (addr_end) + p = addr_end + 1; + else { + fprintf(stderr, "Target URI is malformed: Unable to find end of IPv6 bracket in \"%s\"\n", addr_start); + return 1; + } + } + + if (!addr_end) { + while (*p && *p != ':' && *p != '?' && *p != '#') ++p; + addr_end = p; + } + + size_t addr_len = addr_end - addr_start; + endpoint->target = (char*)malloc(addr_len+1); + if (endpoint->target) { + strncpy(endpoint->target, addr_start, addr_len); + endpoint->target[addr_len] = 0; + } + + endpoint->port = 0; + if (*p == ':') { + ++p; + endpoint->port = (int)strtol(p, NULL, 10); + } + + printf("[%s] Added endpoint to service: %s, %s, %i\n", self->id, endpoint->scheme, endpoint->target, endpoint->port); + vector_push_back(&self->endpoints, &endpoint); + return 0; +} + +enum State get_state(struct Service** self) { + Iterator it = vector_begin(&(*self)->endpoints); + Iterator end = vector_end(&(*self)->endpoints); + int up_count = 0; int total_count = (*self)->endpoints.size; + for (; !iterator_equals(&it, &end); iterator_increment(&it)) { + struct Endpoint* endpoint = *(struct Endpoint**)iterator_get(&it); + if (endpoint->last_state == UP) + up_count++; + } + if (up_count == total_count) return UP; + else if (up_count == 0) return DOWN; + else return PARTIAL; +} + +void report() { + if (status_timeline.size + 1 >= status_timeline.capacity) + vector_pop_front(&status_timeline); // remove (what should be) oldest entry + + struct StatusPeroid* peroid = malloc(512); + peroid->time = queued_time; + vector_setup(&peroid->server_statuses, services.size, sizeof(struct ServerStatus*)); + + Iterator it = vector_begin(&services); + Iterator end = vector_end(&services); + for (; !iterator_equals(&it, &end); iterator_increment(&it)) { + struct Service* service = *(struct Service**)iterator_get(&it); + struct ServerStatus* server_status = update_server_status(&service); + vector_push_back(&peroid->server_statuses, &server_status); + } + + vector_push_back(&status_timeline, &peroid); +} + +struct ServerStatus* update_server_status(struct Service** self) { + struct Service* service = *self; + double pings[service->endpoints.size] = {}; + + struct ServerStatus* server_status = malloc(512); + server_status->service = service; + enum State service_state = DOWN; + + vector_setup(&server_status->endpoint_statuses, service->endpoints.size, sizeof(struct EndpointStatus)); + + int index = 0; + Iterator it = vector_begin(&service->endpoints); + Iterator end = vector_end(&service->endpoints); + for (; !iterator_equals(&it, &end); iterator_increment(&it)) { + struct Endpoint* endpoint = *(struct Endpoint**)iterator_get(&it); + struct EndpointStatus estatus; + estatus.endpoint = endpoint; + estatus.state = endpoint->last_state; + pings[index] = estatus.ping = endpoint->last_ping; + vector_push_back(&server_status->endpoint_statuses, &estatus); + if (index == 0) service_state = endpoint->last_state; + else if ((endpoint->last_state == DOWN && service_state == UP) || (endpoint->last_state == UP && service_state == DOWN)) + service_state = PARTIAL; + index++; + } + + size_t pings_len = index; + double ping_sum = 0; + for (int i = 0; i < sizeof(pings)/sizeof(double); i++) + if (pings[i] >= 0) { + ping_sum+=pings[i]; + pings_len--; + } + double avg_ping = ping_sum / pings_len; + + server_status->state = service_state; + server_status->ping = avg_ping; + return server_status; +} + +const char* state_text(enum State state) { + if (state == UP) + return "UP"; + else if (state == PARTIAL) + return "PARTIAL"; + else return "DOWN"; +} \ No newline at end of file diff --git a/src/services.h b/src/services.h new file mode 100644 index 0000000..66064e7 --- /dev/null +++ b/src/services.h @@ -0,0 +1,80 @@ +#ifndef H_SERVICES +#define H_SERVICES +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../deps/vector/vector.h" +#include "util.h" + +enum State { DOWN, PARTIAL, UP }; +struct StatusPeroid; +struct ServerStatus; +struct EndpointStatus; +struct Endpoint; +struct Service; + +void report(); +void* run_endpoint(void* endpoint); +void queue_report(struct EndpointStatus status); +void load_service(struct Service** self); +uint8_t add_endpoint(struct Service* self, const char* uri); +enum State get_state(struct Service** self); +struct ServerStatus* update_server_status(struct Service** self); +const char* state_text(enum State state); + +struct StatusPeroid { + unsigned long time; + Vector server_statuses; // of ServerStatus* +}; + +struct ServerStatus { + struct Service* service; + Vector endpoint_statuses; // of EndpointStatus* + enum State state; + double ping; +}; + +struct EndpointStatus { + struct Endpoint* endpoint; + enum State state; + double ping; +}; + +struct Endpoint { + struct Service* service; + uint8_t enabled; + uint8_t running; + char* scheme; + char* target; + uint16_t port; + uint32_t backoff; + time_t last_check; + enum State last_state; + double last_ping; + Vector status_history; + pthread_t thread; + int sockfd; + int last_errno; +}; + +struct Service { + char* id; + char* name; + char* text; + Vector endpoints; + enum State last_state; + double average_ping; + uint32_t backoff; +}; + +#endif \ No newline at end of file diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..caaf631 --- /dev/null +++ b/src/util.h @@ -0,0 +1,14 @@ +#ifndef H_UTIL +#define H_UTIL +#include +#include +#include +#include + +static uint64_t microtime() { + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec * (uint64_t)1000000 + tv.tv_usec; +}; + +#endif \ No newline at end of file diff --git a/www/style.css b/www/style.css new file mode 100644 index 0000000..23af97e --- /dev/null +++ b/www/style.css @@ -0,0 +1,3 @@ +td[_status=UP] { background-color: green; } +td[_status=PARTIAL] { background-color: yellow; } +td[_status=DOWN] { background-color: red; } \ No newline at end of file