std1.0.0-beta-7 SafeC docs

Source http/static_files.sc

View in the repository

// SafeC Standard Library — static file serving (see static_files.h).
#pragma once
#include <std/http/static_files.h>
#include <std/http/http.h>
#include <std/http/date.h>
#include <std/encoding/url.h>
#include <std/collections/string.h>
#include <std/io_file.h>
#include <std/mem.h>
#include <std/str.h>

namespace std {

// ── File metadata ─────────────────────────────────────────────────────────
// struct stat differs by OS and architecture, and glibc before 2.33 has no
// exported 'stat' at all (only __xstat), so Linux goes through the raw
// newfstatat system call. The offsets below are each ABI's documented
// struct layout (only mode, size and mtime are read).
#if defined(_WIN32)
extern int _stat64(const char* path, unsigned char* buf);
#define HTTP_ST_MODE_U16_ 6
#define HTTP_ST_SIZE_     24
#define HTTP_ST_MTIME_    40
#elif defined(__APPLE__)
extern int stat(const char* path, unsigned char* buf);
#define HTTP_ST_MODE_U16_ 4
#define HTTP_ST_SIZE_     96
#define HTTP_ST_MTIME_    48
#elif defined(__linux__)
extern long syscall(long number, ...);
#if defined(__aarch64__) || defined(__riscv)
#define HTTP_SYS_NEWFSTATAT_ 79
#define HTTP_ST_MODE_U32_    16
#else
#define HTTP_SYS_NEWFSTATAT_ 262
#define HTTP_ST_MODE_U32_    24
#endif
#define HTTP_ST_SIZE_     48
#define HTTP_ST_MTIME_    88
#else
// FreeBSD 12+ (ino64 layout).
extern int stat(const char* path, unsigned char* buf);
#define HTTP_ST_MODE_U16_ 24
#define HTTP_ST_SIZE_     112
#define HTTP_ST_MTIME_    64
#endif

int http_file_info(const &char path, &long long size, &long long mtime) {
    unsigned char st[256];
    int i = 0;
    while (i < 256) { st[i] = (unsigned char)0; i = i + 1; }
    int rc;
    unsigned int mode = 0U;
    unsafe {
#if defined(_WIN32)
        rc = _stat64(path, (unsigned char*)&st[0]);
#elif defined(__APPLE__)
        rc = stat(path, (unsigned char*)&st[0]);
#elif defined(__linux__)
        rc = (int)syscall((long)HTTP_SYS_NEWFSTATAT_, -100L, path, (unsigned char*)&st[0], 0L);
#else
        rc = stat(path, (unsigned char*)&st[0]);
#endif
        if (rc != 0) { return 0; }
#ifdef HTTP_ST_MODE_U32_
        mode = *(unsigned int*)&st[HTTP_ST_MODE_U32_];
#else
        mode = (unsigned int)*(unsigned short*)&st[HTTP_ST_MODE_U16_];
#endif
        *size = *(long long*)&st[HTTP_ST_SIZE_];
        *mtime = *(long long*)&st[HTTP_ST_MTIME_];
    }
    unsigned int fmt = mode & 0xF000U;
    if (fmt == 0x4000U) { return 2; }
    if (fmt == 0x8000U) { return 1; }
    return 3;   // something else (device, fifo): not served
}

// ── MIME types ────────────────────────────────────────────────────────────

static int sf_ext_is_(const char* ext, unsigned long n, const char* want) {
    unsafe {
        unsigned long i = 0UL;
        while (i < n) {
            char c = ext[i];
            if (c >= 'A' && c <= 'Z') { c = (char)(c + 32); }
            if (want[i] == (char)0 || want[i] != c) { return 0; }
            i = i + 1UL;
        }
        return want[i] == (char)0;
    }
}

const &char http_mime_type(const &char path) {
    unsigned long n = str_len(path);
    unsigned long dot = n;
    unsafe {
        unsigned long i = n;
        while (i > 0UL) {
            i = i - 1UL;
            if (path[i] == '.') { dot = i; break; }
            if (path[i] == '/' || path[i] == '\\') { break; }
        }
    }
    if (dot == n) { return "application/octet-stream"; }
    const char* e;
    unsafe { e = &path[dot + 1UL]; }
    unsigned long el = n - dot - 1UL;
    if (sf_ext_is_(e, el, "html") || sf_ext_is_(e, el, "htm")) { return "text/html; charset=utf-8"; }
    if (sf_ext_is_(e, el, "css")) { return "text/css; charset=utf-8"; }
    if (sf_ext_is_(e, el, "js") || sf_ext_is_(e, el, "mjs")) { return "text/javascript; charset=utf-8"; }
    if (sf_ext_is_(e, el, "json") || sf_ext_is_(e, el, "map")) { return "application/json"; }
    if (sf_ext_is_(e, el, "wasm")) { return "application/wasm"; }
    if (sf_ext_is_(e, el, "txt") || sf_ext_is_(e, el, "text")) { return "text/plain; charset=utf-8"; }
    if (sf_ext_is_(e, el, "md") || sf_ext_is_(e, el, "markdown")) { return "text/markdown; charset=utf-8"; }
    if (sf_ext_is_(e, el, "csv")) { return "text/csv; charset=utf-8"; }
    if (sf_ext_is_(e, el, "xml")) { return "application/xml"; }
    if (sf_ext_is_(e, el, "svg")) { return "image/svg+xml"; }
    if (sf_ext_is_(e, el, "png")) { return "image/png"; }
    if (sf_ext_is_(e, el, "jpg") || sf_ext_is_(e, el, "jpeg")) { return "image/jpeg"; }
    if (sf_ext_is_(e, el, "gif")) { return "image/gif"; }
    if (sf_ext_is_(e, el, "webp")) { return "image/webp"; }
    if (sf_ext_is_(e, el, "avif")) { return "image/avif"; }
    if (sf_ext_is_(e, el, "ico")) { return "image/x-icon"; }
    if (sf_ext_is_(e, el, "bmp")) { return "image/bmp"; }
    if (sf_ext_is_(e, el, "woff")) { return "font/woff"; }
    if (sf_ext_is_(e, el, "woff2")) { return "font/woff2"; }
    if (sf_ext_is_(e, el, "ttf")) { return "font/ttf"; }
    if (sf_ext_is_(e, el, "otf")) { return "font/otf"; }
    if (sf_ext_is_(e, el, "pdf")) { return "application/pdf"; }
    if (sf_ext_is_(e, el, "zip")) { return "application/zip"; }
    if (sf_ext_is_(e, el, "gz")) { return "application/gzip"; }
    if (sf_ext_is_(e, el, "tar")) { return "application/x-tar"; }
    if (sf_ext_is_(e, el, "mp3")) { return "audio/mpeg"; }
    if (sf_ext_is_(e, el, "wav")) { return "audio/wav"; }
    if (sf_ext_is_(e, el, "ogg")) { return "audio/ogg"; }
    if (sf_ext_is_(e, el, "mp4")) { return "video/mp4"; }
    if (sf_ext_is_(e, el, "webm")) { return "video/webm"; }
    if (sf_ext_is_(e, el, "webmanifest")) { return "application/manifest+json"; }
    if (sf_ext_is_(e, el, "sc") || sf_ext_is_(e, el, "h") || sf_ext_is_(e, el, "c")) { return "text/plain; charset=utf-8"; }
    return "application/octet-stream";
}

static int sf_starts_(const &char s, const &char prefix) {
    unsafe {
        unsigned long i = 0UL;
        while (prefix[i] != (char)0) {
            if (s[i] != prefix[i]) { return 0; }
            i = i + 1UL;
        }
    }
    return 1;
}

int http_mime_is_compressible(const &char mime) {
    unsafe {
        if (sf_starts_(mime, "text/")) { return 1; }
        if (sf_starts_(mime, "application/json") || sf_starts_(mime, "application/javascript") ||
            sf_starts_(mime, "application/xml") || sf_starts_(mime, "application/wasm") ||
            sf_starts_(mime, "application/manifest+json") || sf_starts_(mime, "image/svg+xml")) {
            return 1;
        }
    }
    return 0;
}

// ── Options, paths ────────────────────────────────────────────────────────

struct StaticOptions StaticOptions::defaults() {
    struct StaticOptions o;
    o.etag = 1;
    o.last_modified = 1;
    o.ranges = 1;
    o.max_age = -1LL;
    o.index_html = 1;
    o.spa_fallback = 0;
    o.dotfiles = 0;
    return o;
}

int http_safe_join(const &char root, const &char url_path, int allow_dotfiles, &String out) {
    struct String path = String::from(url_path);
    long long q = path.find_char('?');
    if (q >= 0LL) { path.truncate((unsigned long)q); }
    long long h = path.find_char('#');
    if (h >= 0LL) { path.truncate((unsigned long)h); }
    int ok = 0;
    struct String dec = url_decode(path.as_ptr(), &ok);
    path.free();
    if (!ok || dec.find_char('\\') >= 0LL) { dec.free(); return 0; }
    // A NUL inside the decoded path ("%00") would truncate it.
    if (str_len(dec.as_ptr()) != dec.length()) { dec.free(); return 0; }
    struct String joined = String::from(root);
    while (joined.length() > 1UL && joined.ends_with("/")) { joined.truncate(joined.length() - 1UL); }
    unsigned long i = 0UL;
    int good = 1;
    unsafe {
        const char* d = dec.as_ptr();
        unsigned long n = dec.length();
        while (i < n) {
            while (i < n && d[i] == '/') { i = i + 1UL; }
            unsigned long s = i;
            while (i < n && d[i] != '/') { i = i + 1UL; }
            unsigned long len = i - s;
            if (len == 0UL) { continue; }
            if (len == 1UL && d[s] == '.') { continue; }
            if (len == 2UL && d[s] == '.' && d[s + 1UL] == '.') { good = 0; break; }
            if (d[s] == '.' && !allow_dotfiles) { good = 0; break; }
            if (!joined.ends_with("/")) { joined.push_char('/'); }
            joined.push_n_(&d[s], len);
        }
    }
    dec.free();
    if (!good) { joined.free(); return 0; }
    out.clear();
    out.push_str(&joined);
    joined.free();
    return 1;
}

// ── Serving ───────────────────────────────────────────────────────────────

static struct String sf_etag_(long long size, long long mtime) {
    struct String e = String::from("\"");
    char hex[32];
    unsigned long long v = (unsigned long long)size;
    int n = 0;
    if (v == 0ULL) { hex[0] = '0'; n = 1; }
    while (v > 0ULL && n < 32) {
        unsigned int d = (unsigned int)(v & 15ULL);
        unsafe { hex[n] = (char)(d < 10U ? (int)'0' + (int)d : (int)'a' + (int)d - 10); }
        v = v >> 4;
        n = n + 1;
    }
    while (n > 0) { n = n - 1; unsafe { e.push_char(hex[n]); } }
    e.push_char('-');
    v = (unsigned long long)mtime;
    n = 0;
    if (v == 0ULL) { hex[0] = '0'; n = 1; }
    while (v > 0ULL && n < 32) {
        unsigned int d = (unsigned int)(v & 15ULL);
        unsafe { hex[n] = (char)(d < 10U ? (int)'0' + (int)d : (int)'a' + (int)d - 10); }
        v = v >> 4;
        n = n + 1;
    }
    while (n > 0) { n = n - 1; unsafe { e.push_char(hex[n]); } }
    e.push_char('"');
    return e;
}

// Does the If-None-Match list contain 'etag' (weak comparison) or "*"?
static int sf_etag_listed_(const &String list, const &String etag) {
    struct String trimmed = list.trim();
    int star = trimmed.eq_cstr("*");
    trimmed.free();
    if (star) { return 1; }
    struct String items[64];
    unsigned long n = list.split(",", items[0..64]);
    int found = 0;
    unsigned long i = 0UL;
    while (i < n) {
        struct String t = items[i].trim();
        struct String s = t.strip_prefix("W/");
        if (s.eq(etag)) { found = 1; }
        s.free();
        t.free();
        unsafe { items[i].free(); }   // the first n slots hold Strings
        i = i + 1UL;
    }
    unsafe { forget(items); }        // each filled slot was freed above
    return found;
}

// Parses a single "bytes=" range against 'size'. 1 with [*start, *end]
// (inclusive) set, 0 to ignore the header (serve 200), -1 unsatisfiable.
static int sf_parse_range_(const &String hdr, long long size, &long long start, &long long end) {
    if (!hdr.starts_with("bytes=")) { return 0; }
    struct String spec = hdr.substr(6UL, hdr.length());
    if (spec.find_char(',') >= 0LL) { spec.free(); return 0; }   // multiple ranges: send it all
    struct String t = spec.trim();
    spec.free();
    long long dash = t.find_char('-');
    if (dash < 0LL) { t.free(); return 0; }
    struct String a = t.substr(0UL, (unsigned long)dash);
    struct String b = t.substr((unsigned long)dash + 1UL, t.length());
    t.free();
    int aok = 0;
    int bok = 0;
    long long av = a.is_empty() ? -1LL : a.parse_int(&aok);
    long long bv = b.is_empty() ? -1LL : b.parse_int(&bok);
    int aempty = a.is_empty();
    int bempty = b.is_empty();
    a.free();
    b.free();
    if ((!aempty && !aok) || (!bempty && !bok) || (aempty && bempty)) { return 0; }
    if (aempty) {
        if (bv <= 0LL) { return -1; }
        if (bv > size) { bv = size; }
        *start = size - bv;
        *end = size - 1LL;
        return size > 0LL ? 1 : -1;
    }
    if (av >= size) { return -1; }
    *start = av;
    *end = (bempty || bv >= size) ? size - 1LL : bv;
    if (*end < *start) { return 0; }
    return 1;
}

static struct StaticOptions sf_opts_(const ?&StaticOptions opts) {
    match (opts) {
        case some(o): { return *o; }
        case null: { }
    }
    return StaticOptions::defaults();
}

struct HttpResponse http_serve_file(const &HttpRequest req, const &char file_path,
                                    const ?&StaticOptions optsIn) {
    struct StaticOptions o = sf_opts_(optsIn);
    if (!req.method.eq_cstr("GET") && !req.method.eq_cstr("HEAD")) {
        struct HttpResponse r = HttpResponse::error(405);
        r.set_header("Allow", "GET, HEAD");
        return r;
    }
    long long size = 0LL;
    long long mtime = 0LL;
    if (http_file_info(file_path, &size, &mtime) != 1) { return HttpResponse::error(404); }
    struct String etag = sf_etag_(size, mtime);
    struct String lastMod = http_date_format(mtime);
    struct HttpResponse r = HttpResponse::new(200);
    r.add_header("Content-Type", http_mime_type(file_path));
    if (o.etag) { r.add_header("ETag", etag.as_ptr()); }
    if (o.last_modified) { r.add_header("Last-Modified", lastMod.as_ptr()); }
    if (o.ranges) { r.add_header("Accept-Ranges", "bytes"); }
    if (o.max_age >= 0LL) {
        struct String cc = String::from("max-age=");
        cc.push_int(o.max_age);
        r.add_header("Cache-Control", cc.as_ptr());
        cc.free();
    }
    // Conditional requests: If-None-Match wins over If-Modified-Since.
    int notModified = 0;
    struct String inm = req.header("If-None-Match");
    if (o.etag && !inm.is_empty()) {
        notModified = sf_etag_listed_(&inm, &etag);
    } else if (o.last_modified) {
        struct String ims = req.header("If-Modified-Since");
        if (!ims.is_empty()) {
            int dok = 0;
            long long t = http_date_parse(ims.as_ptr(), &dok);
            if (dok && mtime <= t) { notModified = 1; }
        }
        ims.free();
    }
    inm.free();
    if (notModified) {
        r.status = 304;
        r.remove_header("Content-Type");
        etag.free();
        lastMod.free();
        return r;
    }
    long long start = 0LL;
    long long end = size - 1LL;
    int partial = 0;
    struct String range = req.header("Range");
    if (o.ranges && !range.is_empty()) {
        int useRange = 1;
        struct String ifRange = req.header("If-Range");
        if (!ifRange.is_empty()) {
            if (ifRange.starts_with("\"") || ifRange.starts_with("W/")) {
                useRange = ifRange.eq(&etag);
            } else {
                int dok = 0;
                long long t = http_date_parse(ifRange.as_ptr(), &dok);
                useRange = dok && t >= mtime;
            }
        }
        ifRange.free();
        if (useRange) {
            int pr = sf_parse_range_(&range, size, &start, &end);
            if (pr == -1) {
                range.free();
                etag.free();
                lastMod.free();
                r.free();
                struct HttpResponse u = HttpResponse::error(416);
                struct String cr = String::from("bytes */");
                cr.push_int(size);
                u.set_header("Content-Range", cr.as_ptr());
                cr.free();
                return u;
            }
            if (pr == 1) { partial = 1; }
        }
    }
    range.free();
    etag.free();
    lastMod.free();
    if (partial) {
        r.status = 206;
        struct String cr = String::from("bytes ");
        cr.push_int(start);
        cr.push_char('-');
        cr.push_int(end);
        cr.push_char('/');
        cr.push_int(size);
        r.add_header("Content-Range", cr.as_ptr());
        cr.free();
    }
    unsigned long want = size > 0LL ? (unsigned long)(end - start + 1LL) : 0UL;
    if (req.method.eq_cstr("HEAD")) {
        // No body: an explicit Content-Length tells the server (which
        // honors it for HEAD) what a GET would have sent.
        struct String cl = String::new();
        cl.push_uint((unsigned long long)want);
        r.set_header("Content-Length", cl.as_ptr());
        cl.free();
        return r;
    }
    ?&File f = File::open(file_path, "rb");
    match (f) {
        case null: {
            r.free();
            return HttpResponse::error(404);
        }
        case some(file) : {
            if (start > 0LL) { file.seek(start, 0); }
            if (!r.body.reserve(want)) { file.close(); r.free(); return HttpResponse::error(500); }
            unsigned char buf[16384];
            unsigned long left = want;
            while (left > 0UL) {
                unsigned long ask = left < 16384UL ? left : 16384UL;
                long long got = file.read(buf[0..ask]);
                if (got <= 0LL) { break; }
                unsafe { r.body.push_n_((const char*)&buf[0], (unsigned long)got); }
                left = left - (unsigned long)got;
            }
            file.close();
            if (left > 0UL) { r.free(); return HttpResponse::error(500); }
        }
    }
    return r;
}

struct HttpResponse http_serve_dir(const &HttpRequest req, const &char root, const &char url_path,
                                   const ?&StaticOptions optsIn) {
    struct StaticOptions o = sf_opts_(optsIn);
    struct String full = String::new();
    if (!http_safe_join(root, url_path, o.dotfiles, &full)) {
        full.free();
        return HttpResponse::error(403);
    }
    long long size = 0LL;
    long long mtime = 0LL;
    int kind = http_file_info(full.as_ptr(), &size, &mtime);
    if (kind == 2) {
        // Directory: redirect to the slash form, then serve its index.
        struct String up = String::from(url_path);
        long long q = up.find_char('?');
        struct String query = String::new();
        if (q >= 0LL) {
            unsafe { query.push(&up.as_ptr()[q]); }
            up.truncate((unsigned long)q);
        }
        if (!up.ends_with("/")) {
            up.push_char('/');
            up.push_str(&query);
            struct HttpResponse r = HttpResponse::redirect(301, up.as_ptr());
            up.free();
            query.free();
            full.free();
            return r;
        }
        up.free();
        query.free();
        if (!o.index_html) { full.free(); return HttpResponse::error(404); }
        full.push("/index.html");
        kind = http_file_info(full.as_ptr(), &size, &mtime);
    }
    if (kind != 1 && o.spa_fallback) {
        full.clear();
        full.push(root);
        full.push("/index.html");
        kind = http_file_info(full.as_ptr(), &size, &mtime);
    }
    if (kind != 1) { full.free(); return HttpResponse::error(404); }
    struct HttpResponse r = http_serve_file(req, full.as_ptr(), optsIn);
    full.free();
    return r;
}

} // namespace std