tilde.club CGI application with local storage

2025-04-03

Many lives have been gone since pet played with CGI. Tilde club leaves no chance to avoid this forgotten craft.

Basically, there was no strong need to write this note. If pet was able find a solution in five minutes, humnans definitely can do better. But as long as CGI tutorial does exist, let this be an amendment.

Pet wanted simple log on the server written from client side with JavaScript. Thus, pet needed a storage in its home directory writable by CGI program. However, they say that CGI scripts are running with NGNX credentials which means they cannot write to pet's home directory by default.

The group of home directory is club and NGINX is not in it. Neither can pet set nginx group for particular directory as an unprivileged user.

The only way to make a directory writable for CGI is to give these permissions to everyone. Pet believes tilde.club is a friendly community, but minimal security is worth to apply. It's not complicated, just two points.

The very basic thing is putting all publicly writable subdirectories under a directory for which read permissions are disabled, i.e.

chmod 701 /home/petbrain/publicly-private

Everyone can go through such directory but cannot list its content. Well-known subdirectories are still vulnerable, but if a subdirectory has long enough random name, it could be a perfect private storage.

That's all.

Finally, here's the first in pet's currrent live CGI program. It simply appends a record to a file and returns response in JSON format.

Although responses contain neither quotes nor newlines, pet assumes strerror may return anything. For this reason print_error performs the minimal escaping.

#ifndef _GNU_SOURCE
// for vasprintf
#define _GNU_SOURCE
#endif

#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

char log_filename[] = "/home/petbrain/public_html/tw.myaw/test/visitors.myaw";
//char log_filename[] = "visitors.myaw";

extern char **environ;

char error_begin[] = "Status: 500\nContent-Type: application/json\n\n{\"status\": \"error\", \"description\": \"";
char error_end[] = "\"}\n";

void print_error(char* fmt, ...)
{
    fputs(error_begin, stdout);
    char* msg;
    va_list ap;
    va_start(ap);
    int msg_len = vasprintf(&msg, fmt, ap);
    va_end(ap);
    if (msg_len == -1) {
        fputs("Out of memory", stdout);
    } else {
        // escape double quotes, backslashes, and newlines for JSON output
        for(int i = 0; i < msg_len; i++) {
            char c = msg[i];
            if (c == '"') {
                putchar('\\');
                putchar(c);
            } else if (c == '\\') {
                putchar('\\');
                putchar('\\');
            } else if (c == '\n') {
                putchar('\\');
                putchar('n');
            } else {
                putchar(c);
            }
        }
        free(msg);
    }
    fputs(error_end, stdout);
}

int main(int argc, char* argv[])
{
    FILE* log = fopen(log_filename, "a");
    if (!log) {
        print_error("Cannot open %s", log_filename);
        return 0;
    }
    time_t t = time(NULL);
    struct tm* tm = gmtime(&t);
    if (tm == NULL) {
        print_error("localtime: %s", strerror(errno));
        return 0;
    }

    fprintf(log, "\n  - ts::isodate: %04d-%02d-%02dT%02d:%02d:%02dZ\n",
            tm->tm_year + 1900,
            tm->tm_mon + 1,
            tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
    fprintf(log, "    data:\n      type: log\n      content:\n");

    for (char** env = environ;;) {
        char* var = *env++;
        if (var == nullptr) {
            break;
        }
        fputs("        ", log);  // indent
        // print NAME=VALUE as NAME: VALUE
        for (;;) {
            char c = *var++;
            if (c == 0) {
                break;
            }
            if (c == '=') {
                fputc(':', log);
                fputc(' ', log);
            } else {
                fputc(c, log);
            }
        }
        fputc('\n', log);
    }
    fclose(log);

    puts("Status: 200\nContent-Type: application/json\n\n{\"status\": \"ok\"}\n");
    return 0;
}