#define _POSIX_C_SOURCE 200809L #include "dat.h" #include "dat_dns.h" #include "dat_fetch.h" #include "dat_paths.h" #include "dat_dpi_icons.h" #include "s7.h" #include "sleep_feed.h" #include #include #include #include #include #include #include #include #include #include #include #include #define TAG_CAP 8192 #define DPI_ARCHIVES_MAX 128 typedef struct { char key[65]; char root[1200]; char title[256]; char description[512]; uint64_t version; int used; } dpi_archive_t; static dpi_archive_t open_archives[DPI_ARCHIVES_MAX]; static s7_scheme *metadata_scheme; static int quit_requested; typedef struct { char *data; size_t length; size_t capacity; } body_buffer_t; static int body_appendf(body_buffer_t *body, const char *format, ...) { va_list args, copy; int needed; size_t capacity; char *data; va_start(args, format); va_copy(copy, args); needed = vsnprintf(NULL, 0, format, copy); va_end(copy); if (needed < 0) { va_end(args); return -1; } if ((size_t)needed > SIZE_MAX - body->length - 1) { va_end(args); errno = EOVERFLOW; return -1; } if (body->length + (size_t)needed + 1 > body->capacity) { capacity = body->capacity ? body->capacity : 512; while (capacity < body->length + (size_t)needed + 1) { if (capacity > SIZE_MAX / 2) { capacity = body->length + (size_t)needed + 1; break; } capacity *= 2; } data = realloc(body->data, capacity); if (!data) { va_end(args); return -1; } body->data = data; body->capacity = capacity; } vsnprintf(body->data + body->length, body->capacity - body->length, format, args); va_end(args); body->length += (size_t)needed; return 0; } static void send_page(const char *url, const char *mime, const void *body, size_t length) { const char *p; printf("\nContent-Type: %s\r\n\r\n", mime); if (length) fwrite(body, 1, length, stdout); fflush(stdout); } static int send_file(const char *url, const char *mime, const dat_archive_t *archive, const sleep_metadata_entry_t *entry) { sleep_feed_t feed; uint64_t block; size_t sent = 0; const char *p; if (entry->size > SIZE_MAX) { errno = EOVERFLOW; return -1; } if (sleep_feed_open(&feed, archive->dat_path, "content", NULL)) return -1; for (block = 0; block < entry->block_count; block++) { if (!sleep_feed_has(&feed, (size_t)entry->block_offset + (size_t)block)) { sleep_feed_close(&feed); errno = ENOENT; return -1; } } fputs("\nContent-Type: %s\r\n\r\n", mime); fflush(stdout); for (block = 0; block < entry->block_count; block++) { uint8_t *data = NULL; size_t length = 0; if (sleep_feed_read(&feed, (size_t)entry->block_offset + (size_t)block, &data, &length) || length > (size_t)entry->size - sent) { free(data); sleep_feed_close(&feed); errno = EIO; return -1; } if (length && fwrite(data, 1, length, stdout) != length) { free(data); sleep_feed_close(&feed); errno = EIO; return -1; } sent += length; free(data); } sleep_feed_close(&feed); fflush(stdout); if (sent != (size_t)entry->size) { errno = EPROTO; return -1; } return ferror(stdout) ? -1 : 0; } static void send_status(const char *message) { const char *p; fputs("", stdout); fflush(stdout); } static void send_reload(const char *url) { const char *p; fputs("", stdout); fflush(stdout); } static int html_append(body_buffer_t *body, const char *text) { const unsigned char *p = (const unsigned char *)(text ? text : ""); for (; *p; p++) { const char *escape = NULL; switch (*p) { case '&': escape = "&"; break; case '<': escape = "<"; break; case '>': escape = ">"; break; case '\"': escape = """; break; case '\'': escape = "'"; break; default: break; } if (escape) { if (body_appendf(body, "%s", escape)) return -1; } else if (body_appendf(body, "%c", *p)) return -1; } return 0; } static int url_append_path(body_buffer_t *body, const char *path) { static const char hex[] = "0123456789ABCDEF"; const unsigned char *p = (const unsigned char *)path; for (; *p; p++) { if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || strchr("/-_.~", *p)) { if (body_appendf(body, "%c", *p)) return -1; } else if (body_appendf(body, "%%%c%c", hex[*p >> 4], hex[*p & 15])) return -1; } return 0; } /* Dillo DPIP tags end in space, quote, greater-than. */ static int read_tag(FILE *in, char *tag, size_t cap) { int c, quote = 0; size_t n = 0; while ((c = fgetc(in)) != EOF) { if (!n && isspace((unsigned char)c)) continue; if (n + 1 >= cap) { errno = EOVERFLOW; return -1; } tag[n++] = (char)c; if (c == '\'') { if (quote) { c = fgetc(in); if (c == '\'') { if (n + 1 >= cap) { errno = EOVERFLOW; return -1; } tag[n++] = (char)c; continue; } if (c != EOF) ungetc(c, in); } quote = !quote; } if (n >= 2 && tag[n - 2] == '\'' && tag[n - 1] == '>') { tag[n] = 0; return 1; } } return n ? -1 : 0; } static char *attribute(const char *tag, const char *name) { char needle[64]; const char *p, *start, *end; char *out; size_t n, used = 0; if (snprintf(needle, sizeof(needle), " %s='", name) >= (int)sizeof(needle)) return NULL; p = strstr(tag, needle); if (p) start = p + strlen(needle); else if (tag[0] == '<' && !strncmp(tag + 1, needle + 1, strlen(needle) - 1)) start = tag + strlen(needle); else return NULL; end = start; while (*end) { if (*end == '\'' && end[1] != '\'') break; if (*end == '\'' && end[1] == '\'') end += 2; else end++; } if (!*end) return NULL; n = (size_t)(end - start); out = malloc(n + 1); if (!out) return NULL; while (start < end) { out[used++] = *start; if (*start == '\'' && start + 1 < end && start[1] == '\'') start++; start++; } out[used] = 0; return out; } static int authenticate(const char *tag) { char path[1024], line[512], secret[128]; FILE *f; char *msg; int ok = 0; const char *home = dat_home_directory(); if (!home || snprintf(path, sizeof(path), "%s/.dillo/dpid_comm_keys", home) >= (int)sizeof(path)) return 0; f = fopen(path, "r"); if (!f) return 0; if (fgets(line, sizeof(line), f) && sscanf(line, "%*u %127s", secret) == 1) { msg = attribute(tag, "msg"); if (msg && strlen(msg) == strlen(secret)) ok = memcmp(msg, secret, strlen(secret)) == 0; free(msg); } fclose(f); return ok; } static s7_pointer manifest_field(s7_pointer record, const char *name) { s7_pointer fields = record; while (s7_is_pair(fields)) { s7_pointer field = s7_car(fields), key, values; if (s7_is_pair(field)) { key = s7_car(field); values = s7_cdr(field); if (s7_is_symbol(key) && !strcmp(s7_symbol_name(key), name) && s7_is_pair(values)) return s7_car(values); } fields = s7_cdr(fields); } return NULL; } static void read_manifest(const dat_archive_t *archive, uint64_t version, dpi_archive_t *record) { dat_path_info_t info; int listing; uint8_t *data = NULL; size_t length = 0; if (!metadata_scheme) metadata_scheme = s7_init(); if (metadata_scheme && dat_archive_resolve_at(archive, version, "/dat.scm", 0, &info, &listing) == 0 && !listing && info.entry.type == SLEEP_ENTRY_FILE && dat_archive_read_file(archive, &info.entry, &data, &length) == 0 && length < 1024u * 1024u && !memchr(data, 0, length)) { char *source = malloc(length + 1); if (source) { s7_pointer port, parsed, title, description; memcpy(source, data, length); source[length] = '\0'; port = s7_open_input_string(metadata_scheme, source); parsed = port ? s7_read(metadata_scheme, port) : NULL; if (port) s7_close_input_port(metadata_scheme, port); title = parsed ? manifest_field(parsed, "title") : NULL; description = parsed ? manifest_field(parsed, "description") : NULL; if (title && s7_is_string(title)) snprintf(record->title, sizeof(record->title), "%s", s7_string(title)); if (description && s7_is_string(description)) snprintf(record->description, sizeof(record->description), "%s", s7_string(description)); free(source); } } free(data); if (!record->title[0] || !record->description[0]) { uint8_t *cname = NULL; size_t cname_length = 0; if (dat_archive_resolve_at(archive, version, "/CNAME", 0, &info, &listing) == 0 && !listing && info.entry.type == SLEEP_ENTRY_FILE && dat_archive_read_file(archive, &info.entry, &cname, &cname_length) == 0 && cname_length && cname_length < sizeof(record->title)) { while (cname_length && (cname[cname_length - 1] == '\n' || cname[cname_length - 1] == '\r')) cname_length--; if (!record->title[0]) { memcpy(record->title, cname, cname_length); record->title[cname_length] = '\0'; } } free(cname); } } static dpi_archive_t *remember_archive(const dat_archive_t *archive, const char *keyhex, const char *root, uint64_t version) { size_t i; dpi_archive_t *record = NULL; for (i = 0; i < DPI_ARCHIVES_MAX; i++) { if (open_archives[i].used && !strcmp(open_archives[i].key, keyhex)) { record = &open_archives[i]; break; } if (!open_archives[i].used && !record) record = &open_archives[i]; } if (!record) return NULL; if (!record->used) { memset(record, 0, sizeof(*record)); snprintf(record->key, sizeof(record->key), "%s", keyhex); snprintf(record->root, sizeof(record->root), "%s", root); snprintf(record->title, sizeof(record->title), "%s", keyhex); record->used = 1; } record->version = version; read_manifest(archive, version, record); return record; } static int parse_query_value(const char *url, const char *name, char *out, size_t capacity) { const char *query = strchr(url, '?'); size_t name_length = strlen(name); if (!query) return 0; query++; while (*query) { const char *end = strchr(query, '&'); size_t part_length = end ? (size_t)(end - query) : strlen(query); const char *equals = memchr(query, '=', part_length); if (equals && (size_t)(equals - query) == name_length && !memcmp(query, name, name_length)) { size_t value_length = part_length - name_length - 1; if (value_length >= capacity) { errno = ENAMETOOLONG; return -1; } memcpy(out, equals + 1, value_length); out[value_length] = '\0'; return 1; } if (!end) break; query = end + 1; } return 0; } static int error_page(const char *url, int status, const char *message) { const char *p; (void)status; printf("\nContent-Type: text/plain\r\n\r\n%s\n", message); fflush(stdout); return ferror(stdout) ? -1 : 0; } typedef struct { sleep_metadata_entry_t entry; char name[1024]; int type; } listing_context_t; typedef struct { listing_context_t *entries; size_t count; size_t capacity; } listing_accumulator_t; static int list_entry(const sleep_metadata_entry_t *entry, const char *name, void *context) { listing_accumulator_t *list = context; listing_context_t *next; if (list->count == list->capacity) { size_t capacity = list->capacity ? list->capacity * 2 : 16; next = realloc(list->entries, capacity * sizeof(*next)); if (!next) return -1; list->entries = next; list->capacity = capacity; } next = &list->entries[list->count++]; memset(next, 0, sizeof(*next)); next->entry = *entry; next->type = entry->type == SLEEP_ENTRY_DIRECTORY ? 3 : 2; if (strlen(name) >= sizeof(next->name)) { errno = ENAMETOOLONG; return -1; } strcpy(next->name, name); return 0; } static int compare_listing(const void *left, const void *right) { const listing_context_t *a = left, *b = right; if (a->type != b->type) return b->type - a->type; return -strcoll(a->name, b->name); } static int append_listing_row(body_buffer_t *body, const dat_url_t *url, const listing_context_t *entry, const char *link_path) { const char *icon = entry->type == 1 ? dat_dpi_icon_updir : entry->type == 3 ? dat_dpi_icon_directory : dat_dpi_icon_file; char date[64] = ""; if (entry->entry.mtime) { time_t seconds = (time_t)(entry->entry.mtime / 1000u); struct tm *tm = gmtime(&seconds); if (tm) strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", tm); } if (body_appendf(body, "\"%s\"type == 1 ? "up" : entry->type == 2 ? "directory" : "file", icon)) return -1; if (html_append(body, url->host)) return -1; if (url->has_version && body_appendf(body, "+%llu", (unsigned long long)url->version)) return -1; if (url_append_path(body, link_path) || body_appendf(body, "\">") || html_append(body, entry->name)) return -1; if (entry->type == 3 && body_appendf(body, "/")) return -1; if (body_appendf(body, "")) return -1; if (*date && html_append(body, date)) return -1; if (body_appendf(body, "")) return -1; if (entry->type == 2 && body_appendf(body, "%llu", (unsigned long long)entry->entry.size)) return -1; return body_appendf(body, "\n"); } static const char *dpi_content_type(const char *pathname) { const char *dot = strrchr(pathname, '.'); if (!dot) return "text/plain"; dot++; if (!strcmp(dot, "htm") || !strcmp(dot, "html")) return "text/html"; if (!strcmp(dot, "png")) return "image/png"; if (!strcmp(dot, "gif")) return "image/gif"; if (!strcmp(dot, "jpg")) return "image/jpeg"; return "text/plain"; } static int serve_not_found(const char *url) { static const char page[] = "

Not found

"; send_page(url, "text/html", page, sizeof(page) - 1); return -1; } static int serve_timeout(const char *url) { static const char page[] = "

Timed out

"; send_page(url, "text/html", page, sizeof(page) - 1); return -1; } static int serve_directory(const char *url_text, const dat_url_t *url, const dat_archive_t *archive, uint64_t version) { listing_accumulator_t entries = {0}; listing_context_t parent; body_buffer_t body = {0}; char parent_path[2048]; size_t i, n; int failed = 0; if (dat_archive_readdir_at(archive, version, url->path, list_entry, &entries)) { free(entries.entries); return serve_not_found(url_text); } qsort(entries.entries, entries.count, sizeof(*entries.entries), compare_listing); if (body_appendf(&body, "")) failed = 1; if (!failed && html_append(&body, url->path)) failed = 1; if (!failed && body_appendf(&body, "

")) failed = 1; if (!failed && html_append(&body, url->path)) failed = 1; if (!failed && body_appendf(&body, "

")) failed = 1; if (strcmp(url->path, "/")) { memset(&parent, 0, sizeof(parent)); parent.type = 1; strcpy(parent.name, ".."); strcpy(parent_path, url->path); n = strlen(parent_path); while (n && parent_path[n - 1] == '/') parent_path[--n] = '\0'; while (n && parent_path[n - 1] != '/') parent_path[--n] = '\0'; if (!n) strcpy(parent_path, "/"); else parent_path[n] = '\0'; if (!failed && append_listing_row(&body, url, &parent, parent_path)) failed = 1; } for (i = 0; !failed && i < entries.count; i++) { body_buffer_t href = {0}; const listing_context_t *entry = &entries.entries[i]; if (body_appendf(&href, "%s", url->path) || (href.length && href.data[href.length - 1] != '/' && body_appendf(&href, "/")) || body_appendf(&href, "%s%s", entry->name, entry->type == 3 ? "/" : "") || append_listing_row(&body, url, entry, href.data)) failed = 1; free(href.data); } if (!failed && body_appendf(&body, "
")) failed = 1; if (failed) { free(body.data); free(entries.entries); return error_page(url_text, 500, "Unable to build directory listing"); } send_page(url_text, "text/html", body.data, body.length); free(body.data); free(entries.entries); return 0; } static int json_append_string(body_buffer_t *body, const char *text) { const unsigned char *p = (const unsigned char *)(text ? text : ""); if (body_appendf(body, "\"")) return -1; for (; *p; p++) { if (*p == '\"' || *p == '\\') { if (body_appendf(body, "\\%c", *p)) return -1; } else if (*p < 0x20) { if (body_appendf(body, "\\u%04x", *p)) return -1; } else if (body_appendf(body, "%c", *p)) return -1; } return body_appendf(body, "\""); } static int serve(const char *url_text) { dat_url_t url; uint8_t key[DAT_KEY_BYTES]; const char *home = dat_home_directory(); char root[1200], keyhex[65], fetch_error[512]; dat_archive_t archive; dat_path_info_t info; int listing = 0; uint64_t version; if (dat_url_parse(url_text, &url)) { error_page(url_text, 400, "Invalid dat:// URL"); return -1; } send_status("resolving name"); if (dat_url_to_key(url.host, key) && dat_dns_resolve(url.host, key, NULL)) { error_page(url_text, 404, "Dat hostname has no usable datkey TXT record"); return -1; } if (!home) { error_page(url_text, 500, "User home directory is unavailable"); return -1; } if (dat_key_to_hex(key, keyhex)) { error_page(url_text, 500, "Unable to format archive key"); return -1; } if (snprintf(root, sizeof(root), "%s/.dillo/dat/archives/%.2s/%s", home, keyhex, keyhex + 2) >= (int)sizeof(root)) { error_page(url_text, 400, "Dillo archive cache path is too long"); return -1; } if (dat_archive_open(&archive, root)) { send_status("getting archive"); if (dat_fetch_archive(root, key, 10000u)) { int saved = errno; if (saved == ETIMEDOUT) return serve_timeout(url_text); const char *reason = saved == ENETUNREACH ? "No Cesspool bootstrap is configured" : saved == EHOSTUNREACH ? "Cesspool bootstrap did not answer" : saved == ENOENT ? "No peer is announcing this archive key" : strerror(saved); snprintf(fetch_error, sizeof(fetch_error), "Cesspool archive fetch failed: %s", reason); return error_page(url_text, 404, fetch_error); } if (dat_archive_open(&archive, root)) { snprintf(fetch_error, sizeof(fetch_error), "Fetched archive cannot be opened: %s", strerror(errno)); return error_page(url_text, 404, fetch_error); } } version = url.has_version ? url.version : dat_archive_version(&archive); if (version > dat_archive_version(&archive)) { dat_archive_close(&archive); return serve_not_found(url_text); } if (dat_archive_resolve_at(&archive, version, url.path, url.force_directory_listing, &info, &listing)) { int saved = errno; dat_archive_close(&archive); if (saved == ETIMEDOUT || saved == EAGAIN) return serve_timeout(url_text); return serve_not_found(url_text); } (void)remember_archive(&archive, keyhex, root, version); send_status("serving file"); if (url.wants_stat) { body_buffer_t stat = {0}; if (body_appendf(&stat, "{\n \"path\": ") || json_append_string(&stat, url.path) || body_appendf(&stat, ",\n \"directory\": %s,\n \"size\": %llu,\n \"mtime\": %llu\n}\n", info.is_directory ? "true" : "false", (unsigned long long)info.entry.size, (unsigned long long)info.entry.mtime)) { free(stat.data); dat_archive_close(&archive); return error_page(url_text, 500, "Stat response is too large"); } send_page(url_text, "text/plain", stat.data, stat.length); free(stat.data); } else if (listing) { serve_directory(url_text, &url, &archive, version); } else { if (info.entry.type != SLEEP_ENTRY_FILE) { dat_archive_close(&archive); return serve_not_found(url_text); } if (send_file(url_text, dpi_content_type(info.entry.path), &archive, &info.entry)) { dat_archive_close(&archive); return error_page(url_text, 500, "File content unavailable"); } } dat_archive_close(&archive); fflush(stdout); return ferror(stdout) ? -1 : 0; } static int serve_dashboard(const char *url) { char action[32], key[80], reload[2048]; int has_action, has_key, has_reload; body_buffer_t body = {0}; size_t i, count = 0; if (parse_query_value(url, "unswarm", action, sizeof(action)) > 0) { has_key = parse_query_value(url, "key", key, sizeof(key)); has_reload = parse_query_value(url, "reload", reload, sizeof(reload)); if (has_key > 0) { for (i = 0; i < DPI_ARCHIVES_MAX; i++) if (open_archives[i].used && !strcmp(open_archives[i].key, key)) memset(&open_archives[i], 0, sizeof(open_archives[i])); send_status("unswarming archive"); send_reload(has_reload > 0 ? reload : "dpi:/dat/"); return 0; } } has_action = parse_query_value(url, "quit", action, sizeof(action)); if (has_action > 0) { static const char quitting[] = "Quitting"; send_page(url, "text/plain", quitting, sizeof(quitting) - 1); quit_requested = 1; return 0; } if (body_appendf(&body, "Dat")) goto fail; if (body_appendf(&body, "

Archives

")) goto fail; for (i = 0; i < DPI_ARCHIVES_MAX; i++) if (open_archives[i].used) count++; if (!count && body_appendf(&body, "

None

")) goto fail; for (i = 0; i < DPI_ARCHIVES_MAX; i++) { dpi_archive_t *archive = &open_archives[i]; if (!archive->used) continue; if (body_appendf(&body, "

", archive->key) || html_append(&body, archive->title[0] ? archive->title : archive->key) || body_appendf(&body, "")) goto fail; if (archive->description[0] && (body_appendf(&body, " - ") || html_append(&body, archive->description))) goto fail; if (body_appendf(&body, "
version %llu

", archive->key, (unsigned long long)archive->version, (unsigned long long)archive->version) || body_appendf(&body, "

Peers

None")) goto fail; if (body_appendf(&body, "
", archive->key)) goto fail; } if (body_appendf(&body, "")) goto fail; send_page(url, "text/html", body.data, body.length); free(body.data); return 0; fail: free(body.data); return error_page(url, 500, "Unable to build Dat dashboard"); } static int serve_internal(const char *url) { const char *path = strstr(url, "://"); const char *query; char decoded[2048]; size_t n; if (!strncmp(url, "dpi:/", 5)) path = url + 4; else if (path) path = strchr(path + 3, '/'); if (!path) path = "/"; query = strchr(path, '?'); n = query ? (size_t)(query - path) : strlen(path); if (n >= sizeof(decoded)) return error_page(url, 400, "Internal DPI path is too long"); memcpy(decoded, path, n); decoded[n] = '\0'; if (!strcmp(decoded, "/dat/")) return serve_dashboard(url); return serve_not_found(url); } static int serve_stdio_requests(void) { char tag[TAG_CAP]; int result = read_tag(stdin, tag, sizeof(tag)); if (result <= 0 || !authenticate(tag)) return 1; while (!quit_requested) { char *cmd, *url; result = read_tag(stdin, tag, sizeof(tag)); if (result <= 0) break; cmd = attribute(tag, "cmd"); url = attribute(tag, "url"); if (cmd && !strcmp(cmd, "DpiBye")) { free(cmd); free(url); break; } if (cmd && !strcmp(cmd, "open_url") && url) { if (!strncmp(url, "dat:", 4)) serve(url); else if (!strncmp(url, "dpi:/", 5)) serve_internal(url); else error_page(url, 404, "Not found"); } free(cmd); free(url); } return 0; } static int serve_socket_requests(void) { int listener = dup(STDIN_FILENO), stopped = 0; if (listener < 0) return -1; while (!stopped) { int client = accept(listener, NULL, NULL); int saved_in, saved_out, stop_after_request = 0; char tag[TAG_CAP]; if (client < 0) { if (errno == EINTR) continue; close(listener); return -1; } saved_in = dup(STDIN_FILENO); saved_out = dup(STDOUT_FILENO); if (saved_in < 0 || saved_out < 0 || dup2(client, STDIN_FILENO) < 0 || dup2(client, STDOUT_FILENO) < 0) { if (saved_in >= 0) close(saved_in); if (saved_out >= 0) close(saved_out); close(client); close(listener); return -1; } clearerr(stdin); clearerr(stdout); if (read_tag(stdin, tag, sizeof(tag)) > 0 && authenticate(tag)) { while (!stop_after_request) { char *cmd, *url; int result = read_tag(stdin, tag, sizeof(tag)); if (result <= 0) break; cmd = attribute(tag, "cmd"); url = attribute(tag, "url"); if (cmd && !strcmp(cmd, "DpiBye")) { stopped = 1; stop_after_request = 1; } else if (cmd && !strcmp(cmd, "open_url") && url) { if (!strncmp(url, "dat:", 4)) serve(url); else if (!strncmp(url, "dpi:/", 5)) serve_internal(url); else error_page(url, 404, "Not found"); stop_after_request = 1; } free(cmd); free(url); } } fflush(stdout); /* The historical dillo-dat plugin ends the accepted socket per page. */ dup2(saved_in, STDIN_FILENO); dup2(saved_out, STDOUT_FILENO); close(saved_in); close(saved_out); close(client); clearerr(stdin); clearerr(stdout); } close(listener); return 0; } int main(int argc, char **argv) { int result; (void)setlocale(LC_COLLATE, ""); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); metadata_scheme = s7_init(); if (argc == 2 && !strcmp(argv[1], "--stdio-test")) result = serve_stdio_requests(); else result = serve_socket_requests(); if (metadata_scheme) s7_free(metadata_scheme); return result < 0 ? 1 : result; }