diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..512677b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +fastfetch_server diff --git a/ansi2html.h b/ansi2html.h new file mode 100644 index 0000000..21a1c09 --- /dev/null +++ b/ansi2html.h @@ -0,0 +1,341 @@ +// usage: +// #define VTERM_ROWS 50 +// #define VTERM_COLS 200 +// #include "ansi2html.h +#pragma once + +#include +#include +#include +#include + +typedef struct ansislop_t { + int fg, bg; + unsigned char bold; + unsigned char italic; + unsigned char underline; + unsigned char dim; + unsigned char blink; + unsigned char strikethrough; + // 2 wasted bytes 🥀🥀🥀🥀🥀 +} ansislop_t; + +typedef struct { + char utf8[5]; + ansislop_t slop; +} cell_t; + +typedef struct { + cell_t cells[VTERM_ROWS][VTERM_COLS]; + int row, col; + ansislop_t slop; + int last_row, last_col; +} vterm_t; + +static void vterm_put(vterm_t* vt, const char* utf8, const int len) { + while (vt->col < 0) vt->col += VTERM_COLS; + while (vt->row < 0) vt->col += VTERM_ROWS; + while (vt->row >= VTERM_ROWS) vt->row -= VTERM_ROWS; + while (vt->col >= VTERM_COLS) vt->col -= VTERM_COLS; + cell_t* c = &vt->cells[vt->row][vt->col]; + if (vt->row > vt->last_row) vt->last_row = vt->row; + if (vt->col > vt->last_col) vt->last_col = vt->col; + memset(c->utf8, 0, 5); + memcpy(c->utf8, utf8, len); + c->slop = vt->slop; + vt->col++; +} + +static const char* ansi16[16] = { + "#241f31", "#c01c28", "#2ec27e", "#f5c211", "#1e78e4", "#9841bb", "#0ab9dc", "#c0bfbc", + "#5e5c64", "#ed333b", "#57e389", "#f8e45c", "#51a1ff", "#c061cb", "#4fd2fd", "#f6f5f4" +}; + +static int color_css(char* buf, size_t size, int32_t color, int is_bg) { + if (color == -1) { + if (is_bg) strcpy(buf, ansi16[0]); + else strcpy(buf, ansi16[15]); + return 7; + } + if ((color & 0xff) < 16) { + strcpy(buf, ansi16[color & 0xff]); + return 7; + } + + int r, g, b; + + if (color & 0x1000000) { + r = (color >> 16) & 0xff; + g = (color >> 8) & 0xff; + b = color & 0xff; + } + else if ((color & 0xff) < 232) { + int i = (color & 0xff) - 16; + b = (i % 6) * 51; i /= 6; + g = (i % 6) * 51; i /= 6; + r = i * 51; + } + else { + r = g = b = 8 + ((color & 0xff) - 232) * 10; + } + return snprintf(buf, size, "#%02x%02x%02x", r, g, b); +} + +static char* vterm_render(const vterm_t* vt) { + const size_t cap = + + (size_t)(vt->last_row + 1) * 4 + + (size_t)(vt->last_row + 1) * (vt->last_col + 1) * + (sizeof("&") - 1); + char* html = malloc(cap); + size_t pos = 0; + + #define APPEND(...) do { \ + int _n = snprintf(html + pos, cap - pos, __VA_ARGS__); \ + if (_n > 0) pos += (size_t)_n; \ + } while (0) + + APPEND( + "" + "" + "\n" + "" + "" + "fastfetch_server" + "" + "" + "
"
+    );
+
+    for (int r = 0; r <= vt->last_row; r++) {
+        ansislop_t slop;
+        for (int c = 0; c <= vt->last_col; c++) {
+            const cell_t* cell = &vt->cells[r][c];
+            if (c == 0 || (memcmp(&cell->slop, &slop, sizeof(ansislop_t)) != 0)) {
+                slop = cell->slop;
+                if (c != 0) {
+                    APPEND("");
+                }
+                struct {
+                    char data[128];
+                    size_t size;
+                } style = {{0}, 0};
+
+                char fg[16];
+                color_css(fg, sizeof(fg), slop.fg, 0);
+                style.size += snprintf(style.data + style.size, sizeof(style.data) - style.size, "color:%s;", fg);
+
+                char bg[16];
+                color_css(bg, sizeof(bg), slop.bg, 1);
+                style.size += snprintf(style.data + style.size, sizeof(style.data) - style.size, "background:%s;", bg);
+
+                struct {
+                    char data[128];
+                    size_t size;
+                } cls = {{0}, 0};
+
+                if (slop.bold) cls.size += snprintf(cls.data + cls.size, sizeof(cls.data) - cls.size, " b");
+                if (slop.italic) cls.size += snprintf(cls.data + cls.size, sizeof(cls.data) - cls.size, " i");
+                if (slop.underline) cls.size += snprintf(cls.data + cls.size, sizeof(cls.data) - cls.size, " u");
+                if (slop.strikethrough) cls.size += snprintf(cls.data + cls.size, sizeof(cls.data) - cls.size, " s");
+                if (slop.dim) cls.size += snprintf(cls.data + cls.size, sizeof(cls.data) - cls.size, " d");
+                if (slop.blink) cls.size += snprintf(cls.data + cls.size, sizeof(cls.data) - cls.size, " k");
+
+                const int has_style = style.data[0] != '\0';
+                const int has_cls = cls.data[0] != '\0';
+
+                APPEND("");
+            }
+            const char* ch = cell->utf8[0] ? cell->utf8 : " ";
+            for (const char* q = ch; *q; q++) {
+                if (*q == '<') APPEND("<");
+                else if (*q == '>') APPEND(">");
+                else if (*q == '&') APPEND("&");
+                else { html[pos++] = *q; }
+            }
+        }
+        APPEND("");
+        APPEND("
"); + } + + APPEND("
\n"); +#undef APPEND + return html; +} + +static const char* vterm_csi(vterm_t* vt, const char* p) { + struct { + int data[32]; + int size; + } params = {{0}, 0}; + + while (*p < 0x40 || *p == ';') { + if (*p == ';') { + params.size++; + if (params.size == 32) params.size = 31; + } + else if ('0' <= *p && *p <= '9') { + params.data[params.size] = params.data[params.size] * 10 + (*p - '0'); + } + p++; + } + + params.size++; + // en.wikipedia.org/wiki/ANSI_escape_code <3 + switch (*p++) { + case 'A': + vt->row -= params.data[0] > 0 ? params.data[0] : 1; + break; + case 'B': + vt->row += params.data[0] > 0 ? params.data[0] : 1; + break; + case 'C': + vt->col += params.data[0] > 0 ? params.data[0] : 1; + break; + case 'D': + vt->col -= params.data[0] > 0 ? params.data[0] : 1; + break; + case 'E': + vt->row += params.data[0] > 0 ? params.data[0] : 1; + vt->col = 0; + break; + case 'F': + vt->row -= params.data[0] > 0 ? params.data[0] : 1; + vt->col = 0; + break; + case 'G': + vt->col = (params.data[0] > 0 ? params.data[0] : 1) - 1; + break; + case 'H': + case 'f': + vt->row = (params.data[0] > 0 ? params.data[0] : 1) - 1; + vt->col = (params.data[1] > 0 ? params.data[1] : 1) - 1; + break; + case 'J': + case 'K': + // no + break; + case 'm': + for (int i = 0; i < params.size; i++) { + const int p0 = params.data[i]; + if (p0 == 0) { + vt->slop.fg = vt->slop.bg = -1; + vt->slop.bold = vt->slop.italic = vt->slop.underline = vt->slop.dim = vt->slop.blink = vt->slop.strikethrough = 0; + } + else if (p0 == 1) vt->slop.bold = 1; + else if (p0 == 2) vt->slop.dim = 1; + else if (p0 == 3) vt->slop.italic = 1; + else if (p0 == 4) vt->slop.underline = 1; + else if (p0 == 5 || p0 == 6) vt->slop.blink = 1; + else if (p0 == 9) vt->slop.strikethrough = 1; + else if (p0 == 22) vt->slop.bold = vt->slop.dim = 0; + else if (p0 == 23) vt->slop.italic = 0; + else if (p0 == 24) vt->slop.underline = 0; + else if (p0 == 25) vt->slop.blink = 0; + else if (p0 == 29) vt->slop.strikethrough = 0; + else if (p0 >= 30 && p0 <= 37) vt->slop.fg = p0 - 30; + else if (p0 == 38 && i + 2 < params.size && params.data[i + 1] == 5) { + vt->slop.fg = params.data[i + 2]; + i += 2; + } + else if (p0 == 38 && i + 4 < params.size && params.data[i + 1] == 2) { + vt->slop.fg = 0x1000000 | (params.data[i + 2] << 16) | (params.data[i + 3] << 8) | params.data[i + 4]; + i += 4; + } + else if (p0 == 39) vt->slop.fg = -1; + else if (p0 >= 40 && p0 <= 47) vt->slop.bg = p0 - 40; + else if (p0 == 48 && i + 2 < params.size && params.data[i + 1] == 5) { + vt->slop.bg = params.data[i + 2]; + i += 2; + } + else if (p0 == 48 && i + 4 < params.size && params.data[i + 1] == 2) { + vt->slop.bg = 0x1000000 | (params.data[i + 2] << 16) | (params.data[i + 3] << 8) | params.data[i + 4]; + i += 4; + } + else if (p0 == 49) vt->slop.bg = -1; + else if (p0 >= 90 && p0 <= 97) vt->slop.fg = p0 - 90 + 8; + else if (p0 >= 100 && p0 <= 107) vt->slop.bg = p0 - 100 + 8; + } + break; + default: printf("unsupported escape: %c\n", *p); + } + return p; +} + +static int utf8_len(const unsigned char c) { + if (c < 0x80) return 1; + if ((c & 0xe0) == 0xc0) return 2; + if ((c & 0xf0) == 0xe0) return 3; + if ((c & 0xf8) == 0xf0) return 4; + return 1; +} + +static char* ansi2html(const char* ansislop) { + vterm_t vt = {0}; + + vt.slop.fg = vt.slop.bg = -1; + for (int r = 0; r < VTERM_ROWS; r++) { + for (int c = 0; c < VTERM_COLS; c++) { + vt.cells[r][c].slop.fg = vt.cells[r][c].slop.bg = -1; + vt.cells[r][c].utf8[0] = ' '; + } + } + + for (const char* p = ansislop; *p;) { + if (*p == '\e') { + p++; + if (*p == '[') { + p = vterm_csi(&vt, p + 1); + } + else if (*p == '(') { + p += 2; + } + else { + while (0x40 > *p || *p > 0x7e) p++; + } + } + else if (*p == '\r') { + p++; + vt.col = 0; + } + else if (*p == '\n') { + p++; + vt.col = 0; + vt.row++; + } + else if (*p == '\t') { + p++; + vt.col = (vt.col + 8) & ~7; + if (vt.col >= VTERM_COLS) { + vt.col = 0; + vt.row++; + } + } + else if ((uint8_t)*p < 0x20) { + p++; + } + else { + const int l = utf8_len((unsigned char)*p); + vterm_put(&vt, p, l); + p += l; + } + } + return vterm_render(&vt); +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..211751c --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cc fastfetch_server.c -o fastfetch_server -O3 -lpthread diff --git a/fastfetch_server.c b/fastfetch_server.c new file mode 100644 index 0000000..240f48d --- /dev/null +++ b/fastfetch_server.c @@ -0,0 +1,235 @@ +#include +#include +#include +#include +#include +#include +#include + +#define VTERM_ROWS 50 +#define VTERM_COLS 200 +#include "ansi2html.h" + +#include "stdint.h" +#define RATELIMIT_THREADSAFE +typedef uint8_t ratelimit_table_type[16]; +#include "ratelimit.h" + +typedef struct pthread_input_t { + int client_fd; + float window; + int rate_limit; + const char* command; +} pthread_input_t; + +#define CURL_HEADERS(status) \ + "HTTP/1.1 " status "\r\n" \ + "Content-Type: text/plain\r\n" \ + "Content-Length: %zu\r\n" \ + "Connection: close\r\n" \ + "\r\n" \ + "%s" + +#define BROWSER_HEADERS(status) \ + "HTTP/1.1 " status "\r\n" \ + "Content-Type: text/html; charset=utf-8\r\n" \ + "Content-Length: %zu\r\n" \ + "Connection: close\r\n" \ + "\r\n" \ + "%s" + +#define FS_HELP \ + "usage:\n" \ + "\tfastfetch_server --addr 127.0.01 (optional)\n" \ + "\t --window 60 (optional)\n" \ + "\t --ratelimit 5 (optional)\n" \ + "\t --hyfetch (optional)\n" \ + "\t port (optional)\n" \ + "\t --addr expects an address in an IPv4 or IPv6 format\n" \ + "\t --window sets a rate limit delay in seconds\n" \ + "\t --ratelimit sets the rate limit amount\n" \ + "\t --hyfetch uses hyfetch with fastfetch backend instead of fastfetch\n" \ + "\t port specifies the port, 80 by default, might need superuser for\n" \ + "\t ports below 1024\n" \ + "so far tested on:\n" \ + "\t GNU/Linux on x86/x86_64 with glibc/musl\n" + +static void send_fastfetch(int client_fd, const char* command, int is_browser) { + char fastfetch[16384]; + FILE* fp = popen(command, "r"); + const size_t n = fp ? fread(fastfetch, sizeof(fastfetch[0]), sizeof(fastfetch) - 1, fp) : 0; + if (fp) pclose(fp); + fastfetch[n] = '\0'; + + if (!is_browser) { + dprintf(client_fd, CURL_HEADERS("200 OK"), n, fastfetch); + } + else { + char* html = ansi2html(fastfetch); + dprintf(client_fd, BROWSER_HEADERS("200 OK"), strlen(html), html); + free(html); + } +} + +static void* handle_client(void* _arg) { + pthread_input_t* arg = _arg; + int client_fd = arg->client_fd; + float window = arg->window; + int rate_limit = arg->rate_limit; + const char* command = arg->command; + + struct sockaddr_in6 peer; + socklen_t peerlen = sizeof(peer); + getpeername(client_fd, (struct sockaddr*)&peer, &peerlen); + + if (rate_limited(peer.sin6_addr.s6_addr, window, rate_limit)) { + dprintf(client_fd, CURL_HEADERS("429 Too Many Requests"), 19, "Too Many Requests\r\n"); + goto cleanup; + } + + char what_to_do[256] = {0}; + int is_browser = 0; + { + struct { + char buf[8192]; + size_t len; + } req = {.len = 0}; + + while (req.len < sizeof(req.buf) - 1) { + const ssize_t n = recv(client_fd, req.buf + req.len, sizeof(req.buf) - 1 - req.len, 0); + if (n <= 0) break; + req.len += n; + req.buf[req.len] = '\0'; + const char* body = memmem(req.buf, req.len, "\r\n\r\n", 4); + + if (body) { + for (const char* p = req.buf; p < body;) { + const char* nl = memchr(p, '\n', body - p); + const size_t line_len = nl ? (size_t)(nl - p) : (size_t)(body - p); + + if (line_len >= 14 && strncasecmp(p, "get /", 5) == 0) { + const char* protocol = memmem(p, line_len, " HTTP/1.1", 9); + if (!protocol) goto continue_label; + const size_t get_str_len = protocol - p - 5; + if (get_str_len >= sizeof(what_to_do)) goto cleanup; + memcpy(what_to_do, p + 5, get_str_len); + } + else if (line_len >= 12 && strncasecmp(p, "user-agent:", 11) == 0) { + const char* ua = p + 11; + while (*ua == ' ' || *ua == '\t') ua++; + is_browser = strncmp(ua, "Mozilla", 7) == 0 && ( + memmem(ua, line_len, "Chrome", 6) || + memmem(ua, line_len, "Firefox", 7) || + memmem(ua, line_len, "Safari", 6)); + goto we_now_know_if_we_got_a_browser; + } +continue_label: + p = nl ? nl + 1 : body; + } + } + } + } +we_now_know_if_we_got_a_browser: + if (what_to_do[0] == '\0') { + send_fastfetch(client_fd, command, is_browser); + goto cleanup; + } + if (strcmp(what_to_do, "help") == 0) { + dprintf(client_fd, CURL_HEADERS("200 OK"), sizeof(FS_HELP) - 1, FS_HELP); + goto cleanup; + } + dprintf(client_fd, CURL_HEADERS("404 Not Found"), 11, "Not Found\r\n"); + +cleanup: + shutdown(client_fd, SHUT_WR); + close(client_fd); + return NULL; +} + +#define streq(a, b) (strcmp((a), (b)) == 0) +#define exit_error(...) do { \ + fprintf(stderr, __VA_ARGS__); \ + exit(EXIT_FAILURE); \ +} while (0) + +#define exit_perror(s) do { \ + perror(s); \ + exit(EXIT_FAILURE); \ +} while (0) + +int main(int argc, char *argv[]) { + int port = 80; + float window = 60.0f; + int rate_limit = 5; + const char* command = "fastfetch --pipe false 2>/dev/null"; + struct in6_addr bind_addr = in6addr_any; + + for (int i = 1; i < argc; i++) { + if (streq(argv[i], "--help")) { + printf(FS_HELP); + return 0; + } + if (streq(argv[i], "--addr") && i + 1 < argc) { + if (inet_pton(AF_INET6, argv[++i], &bind_addr) != 1) { + struct in_addr v4; + if (inet_pton(AF_INET, argv[i], &v4) != 1) exit_error("invalid address: %s\n", argv[i]); + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.s6_addr[10] = 0xff; + bind_addr.s6_addr[11] = 0xff; + memcpy(&bind_addr.s6_addr[12], &v4, 4); + } + } + else if (streq(argv[i], "--window") && i + 1 < argc) { + window = atof(argv[++i]); + } + else if (streq(argv[i], "--ratelimit") && i + 1 < argc) { + rate_limit = atoi(argv[++i]); + } + else if (streq(argv[i], "--hyfetch")) { + command = "hyfetch --backend=fastfetch --args=\"--pipe false\" 2>/dev/null"; + } + else { + port = atoi(argv[i]); + if (port <= 0) exit_error("unknown argument: %s\n", argv[i]); + } + } + + const int server_fd = socket(AF_INET6, SOCK_STREAM, 0); + if (server_fd < 0) exit_perror("socket"); + + int opt = 0; + setsockopt(server_fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); + opt = 1; + setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + const struct sockaddr_in6 addr = { + .sin6_family = AF_INET6, + .sin6_addr = bind_addr, + .sin6_port = htons(port) + }; + + if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) exit_perror("bind"); + if (listen(server_fd, 128) < 0) exit_perror("listen"); + + char addr_str[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &bind_addr, addr_str, sizeof(addr_str)); + printf("listening on [%s]:%d (limit: %d req / %fs per IP)\n", + addr_str, port, rate_limit, window); + + for (;;) { + struct sockaddr_in6 client_addr; + socklen_t addrlen = sizeof(client_addr); + int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen); + if (client_fd < 0) continue; + + pthread_input_t pi = {.client_fd = client_fd, .window = window, .rate_limit = rate_limit, .command = command}; + + pthread_t tid; + if (pthread_create(&tid, NULL, handle_client, (void*)(intptr_t)&pi) == 0) + pthread_detach(tid); + else { + perror("pthread_create"); + close(client_fd); + } + } +} diff --git a/ratelimit.h b/ratelimit.h new file mode 100644 index 0000000..e805949 --- /dev/null +++ b/ratelimit.h @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include + +#ifdef RATELIMIT_THREADSAFE +#include +#endif + +typedef struct { + ratelimit_table_type value; + int count; + double window_start; +} ratelimit_t; + +static struct { + ratelimit_t data[256]; + size_t next; +#ifdef RATELIMIT_THREADSAFE + pthread_mutex_t lock; +#endif +} ratelimit_table = { {0}, 0 +#ifdef RATELIMIT_THREADSAFE +, PTHREAD_MUTEX_INITIALIZER +#endif +}; + +static int rate_limited(ratelimit_table_type value, float window, int rate_limit) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + double now = ts.tv_sec + ts.tv_nsec * 1e-9; + +#ifdef RATELIMIT_THREADSAFE + pthread_mutex_lock(&ratelimit_table.lock); +#endif + + ratelimit_t* e = (void*)0; + for (size_t i = 0; i < 256; i++) { + if (memcmp(ratelimit_table.data[i].value, value, sizeof(ratelimit_table_type)) == 0) { + e = &ratelimit_table.data[i]; + break; + } + } + + if (!e) { + e = &ratelimit_table.data[ratelimit_table.next++]; + memcpy(e->value, value, sizeof(ratelimit_table_type)); + e->window_start = now; + e->count = 0; + } + + if (now - e->window_start >= window) { + e->window_start = now; + e->count = 0; + } + + const int allowed = e->count < rate_limit; + if (allowed) e->count++; + +#ifdef RATELIMIT_THREADSAFE + pthread_mutex_unlock(&ratelimit_table.lock); +#endif + + return allowed ? 0 : 1; +}