758 lines
25 KiB
C
758 lines
25 KiB
C
#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 <ctype.h>
|
||
|
|
#include <errno.h>
|
||
|
|
#include <locale.h>
|
||
|
|
#include <stdarg.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <strings.h>
|
||
|
|
#include <sys/socket.h>
|
||
|
|
#include <sys/stat.h>
|
||
|
|
#include <time.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
|
||
|
|
#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("<cmd='start_send_page' url='");
|
||
|
|
for (p = url; *p; p++) { putchar(*p); if (*p == '\'') putchar(*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("<cmd='start_send_page' url='", stdout);
|
||
|
|
for (p = url; *p; p++) { putchar(*p); if (*p == '\'') putchar(*p); }
|
||
|
|
printf("' '>\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("<cmd='send_status_message' msg='", stdout);
|
||
|
|
for (p = message; *p; p++) { putchar(*p); if (*p == '\'') putchar(*p); }
|
||
|
|
fputs("' '>", stdout);
|
||
|
|
fflush(stdout);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void send_reload(const char *url)
|
||
|
|
{
|
||
|
|
const char *p;
|
||
|
|
fputs("<cmd='reload_request' url='", stdout);
|
||
|
|
for (p = url; *p; p++) { putchar(*p); if (*p == '\'') putchar(*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("<cmd='start_send_page' url='");
|
||
|
|
for (p = url; *p; p++) { putchar(*p); if (*p == '\'') putchar(*p); }
|
||
|
|
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, "<tr><td><img alt=\"%s\" src=\"data:image/png;base64,%s\"></td><td><a href=\"dat://",
|
||
|
|
entry->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, "</a></td><td>")) return -1;
|
||
|
|
if (*date && html_append(body, date)) return -1;
|
||
|
|
if (body_appendf(body, "</td><td align=\"right\">")) return -1;
|
||
|
|
if (entry->type == 2 && body_appendf(body, "%llu", (unsigned long long)entry->entry.size)) return -1;
|
||
|
|
return body_appendf(body, "</td></tr>\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[] = "<h3>Not found</h3>";
|
||
|
|
send_page(url, "text/html", page, sizeof(page) - 1);
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int serve_timeout(const char *url)
|
||
|
|
{
|
||
|
|
static const char page[] = "<h3>Timed out</h3>";
|
||
|
|
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, "<!doctype html><html><head><title>")) failed = 1;
|
||
|
|
if (!failed && html_append(&body, url->path)) failed = 1;
|
||
|
|
if (!failed && body_appendf(&body, "</title></head><body><h4>")) failed = 1;
|
||
|
|
if (!failed && html_append(&body, url->path)) failed = 1;
|
||
|
|
if (!failed && body_appendf(&body, "</h4><table>")) 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, "</table></body></html>")) 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, "<!doctype html><html><head><title>Dat</title></head><body>")) goto fail;
|
||
|
|
if (body_appendf(&body, "<form action=\"dpi:/dat/\"><input type=\"submit\" name=\"quit\" value=\"Quit\"></form><h3>Archives</h3>")) goto fail;
|
||
|
|
for (i = 0; i < DPI_ARCHIVES_MAX; i++) if (open_archives[i].used) count++;
|
||
|
|
if (!count && body_appendf(&body, "<p>None</p>")) 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, "<p><a href=\"dat://%s\">", archive->key) ||
|
||
|
|
html_append(&body, archive->title[0] ? archive->title : archive->key) ||
|
||
|
|
body_appendf(&body, "</a>")) goto fail;
|
||
|
|
if (archive->description[0] &&
|
||
|
|
(body_appendf(&body, " - ") || html_append(&body, archive->description))) goto fail;
|
||
|
|
if (body_appendf(&body, "<br>version <a href=\"dat://%s+%llu\">%llu</a></p>",
|
||
|
|
archive->key, (unsigned long long)archive->version,
|
||
|
|
(unsigned long long)archive->version) ||
|
||
|
|
body_appendf(&body, "<blockquote><h3>Peers</h3>None")) goto fail;
|
||
|
|
if (body_appendf(&body, "<form action=\"dpi:/dat/\"><input type=\"hidden\" name=\"key\" value=\"%s\"><input type=\"hidden\" name=\"reload\" value=\"dpi:/dat/\"><input type=\"submit\" name=\"unswarm\" value=\"unswarm\"></form></blockquote>", archive->key)) goto fail;
|
||
|
|
}
|
||
|
|
if (body_appendf(&body, "</body></html>")) 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;
|
||
|
|
}
|