tildebin/tildebin.c

592 lines
16 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <getopt.h>
#include <pwd.h>
#ifdef __linux__
#define _GNU_SOURCE
#define __USE_GNU
#endif
#ifdef __OpenBSD__
#include <sys/ucred.h>
#endif
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/stat.h>
#include "utilities.h"
#define MAX_CLIENTS 100
#define MAX_BUFFER (1024 * 56)
#define SELECT_TIMEOUT 5
#define IDLE_TIME 100 * 1000
#define IDENTIFIER ".EOH."
#define OUTPUT_MASK 0755
#define SOCKET_MASK 0000
#define MEM_CATCH(_e) STD_CATCHER( _e, "memory allocation error")
// Defaults
#define DEFAULT_OUTPUTFILE "index.html"
#define DEFAULT_SOCKET_PATH "/tmp/tildebin.sock"
#define DEFAULT_OUTPUT_PATH "/tmp/tildebin"
struct uinfo
{
int uid, gid;
char name[32];
};
const char* const default_template = {
"<!DOCTYPE html>"
"<html>"
"<head>"
"<meta charset=\"utf-8\">"
"<link rel=\"icon\" type=\"/image/png\" href=\"/~{{user}}/favicon.png\"/>"
"<link rel=\"stylesheet\" href=\"/~{{user}}/assets/style.css\">"
"<title>{{client}}'s tildebin</title>"
"</head>"
"<body>"
"<pre>{{text}}</pre>"
"</body>"
"</html>"
};
static volatile bool keep_running;
static bool curate_html;
static int
server_socket,
client_sockets[MAX_CLIENTS];
static char client_buffer[MAX_BUFFER];
static const char
*output_dir,
*template,
*username,
*output_file_name;
static FILE *output_fd;
static struct passwd *passwd;
GENERIC_VECTOR(string, char)
static string output_path;
static string custom_template;
// Check if specified path is a directory
static bool
is_directory( const char *path )
{
struct stat st;
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
}
// Attempt to create directory in path if it
// doesn't exist
static void
create_directory( const char *path )
{
if(!is_directory(path))
STD_CATCHER_CRITICAL(mkdir(path, OUTPUT_MASK), "cannot create directory \"%s\"", path);
}
// Get user info based on socket
static int
get_uinfo( int fd, struct uinfo *uinfo )
{
memset(uinfo, 0, sizeof(*uinfo));
int len, ret;
struct passwd* pw;
struct ucred uc;
len = sizeof(uc);
if((ret = getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &uc, (socklen_t *)&len)) != 0)
{
ERROR("cannot get user data from socket %d", fd);
return -1;
}
#ifdef __linux__
if((pw = getpwuid(uc.uid)) == NULL)
{
ERROR("connection with invalid uid %d", uc.uid);
return -1;
}
uinfo->uid = uc.uid;
uinfo->gid = uc.gid;
strncpy(uinfo->name, pw->pw_name, GET_LEN(uinfo->name));
#elif __OpenBSD__
if((pw = getpwuid(uc.cr_uid)) == NULL)
{
ERROR("connection with invalid uid %d", uc.cr_uid);
return -1;
}
uinfo->uid = uc.cr_uid;
uinfo->gid = uc.cr_gid;
strncpy(uinfo->name, pw->pw_name, GET_LEN(uinfo->name));
#else
return 0;
#endif
return ret;
}
// I'm not even going to bother commenting this mess, it's
// easily the ugliest and pachiest parser I've ever written
// so I'll for sure be rewritting it in the future
// Get and parse user request/command
static int
serve( int fd, const struct uinfo *ui )
{
int i;
bool found_header;
ssize_t len, wlen;
size_t fn_size;
const char * fmt;
char *op, *cp, *np, *st, *id;
len = 0;
cp = np = id = st = op = NULL;
fn_size = strlen(output_file_name) + strlen(ui->name) +1;
memset(client_buffer, 0, MAX_BUFFER*sizeof(char));
len = read(fd, client_buffer, MAX_BUFFER);
if(len < 0)
{
STD_CATCHER(len, "cannot read socket for user \"%s\"", ui->name);
return -1;
}
if(len > 0)
{
INFO("received request from user \"%s\"", ui->name);
client_buffer[len -1] = '\0';
found_header = ((id = strstr(client_buffer, IDENTIFIER)) != NULL);
if(found_header)
{
INFO("found request header");
*id = '\0';
}
else
id = client_buffer;
fn_size += strlen(output_dir) + 2;
if(found_header)
{
cp = client_buffer;
if(
(cp = strstr(cp, ".name.")) != NULL &&
(np = strstr(cp, ".ename.")) != NULL
)
{
*np = '\0';
cp = &cp[GET_LEN(".name.")-1];
fn_size += strlen(cp) + strlen(output_dir) + 3;
}
}
// Make sure path string is big enough
if(fn_size > string_get_len(&output_path))
MEM_CATCH(string_reserve(&output_path, fn_size+1));
// Create user directory if it doesn't exist
fmt = cp == NULL ? "%s/%s/%s" : "%s/%s/%s/";
cp = cp == NULL ? "" : cp;
snprintf(string_get_data(&output_path), fn_size, fmt, output_dir, ui->name, cp);
create_directory(string_get_data(&output_path));
// Get full path
strcat(output_path.data, output_file_name);
// Open output file for writing
if(output_fd != NULL)
fclose(output_fd);
VALIDATE_STD(
(output_fd = fopen(string_get_data(&output_path), "w")) != NULL,
"cannot open \"%s\"", string_get_data(&output_path)
);
STD_CATCHER_CRITICAL(
chmod(output_path.data, OUTPUT_MASK),
"cannot set file permissions"
);
if(found_header)
st = &id[GET_LEN(IDENTIFIER) - 1];
else
st = id;
op = cp = (char *)template;
len = strlen(op);
while (cp != NULL && cp < &template[len-1])
{
op = cp;
if((cp = strstr(cp, "{{")) != NULL && (np = strstr(cp, "}}")) != NULL)
{
fwrite(op, sizeof(char), cp - op, output_fd);
cp += sizeof("{{") - 1*sizeof(char);
wlen = np - cp;
if(strncmp(cp, "client", wlen) == 0)
fprintf(output_fd, "%s", ui->name);
else if(strncmp(cp, "user", wlen) == 0)
fprintf(output_fd, "%s", username);
else if(strncmp(cp, "text", wlen) == 0)
{
if(curate_html)
{
// Make sure there aren't any funny business going on
while((cp = strchr(st, '<')) != NULL || (cp = strchr(st, '>')) != NULL)
{
i = *cp;
*cp = '\0';
fprintf(output_fd, "%s", st);
switch (i)
{
case '<':
fprintf(output_fd, "&lt;");
break;
case '>':
fprintf(output_fd, "&gt;");
break;
default:
continue;
}
*st = '\0';
st = &cp[1];
}
}
fprintf(output_fd, "%s", st);
}
cp = &np[GET_LEN("}}") -1];
}
else
{
fwrite(op, sizeof(char), strlen(op), output_fd);
break;
}
}
INFO("user \"%s\" created \"%s\"", ui->name, output_path.data);
fclose(output_fd);
output_fd = NULL;
}
else if (len == 0)
{
return -1;
}
return 0;
}
void
signal_handler( int sig )
{
const char *sn;
sn = NULL;
switch (sig)
{
case SIGINT:
sn = "SIGINT";
break;
case SIGTERM:
sn = "SIGTERM";
break;
default:
break;
}
sn = sn == NULL ? "UNKNOWN" : sn;
INFO("%s signal detected, terminating...", sn);
keep_running = false;
}
void
cleannup( void )
{
int i;
if(output_fd != NULL)
{
fclose(output_fd);
output_fd = NULL;
}
if(server_socket > 0)
{
close(server_socket);
server_socket = 0;
}
for(i = 0; i < MAX_CLIENTS; i++)
{
if(client_sockets[i] <= 0)
continue;
close(client_sockets[i]);
client_sockets[i] = 0;
}
string_free(&custom_template);
string_free(&output_path);
}
void
help( void )
{
printf(
"Usage: tildebin [OPTIONS]\n"
"Takes user's copy/paste requests from a socket and\n"
"saves them onto disk based on a user defined template\n\n"
"None of the options below are mandatory\n"
" -s socket path \tsets socket path\n"
" -t template path \tsets template path\n"
" -o output dir \tsets output directory\n"
" -n output filename \tsets user's output filename\n"
" -c \tenables html curation (set to true if no template is provided)\n"
" -h \tprints this message\n\n"
"Default socket path is set to /tmp/tildebin.socket and default output\n"
"file name is set to index.html. The user requests will be saved under /tmp/tildebin/.\n"
);
}
int
main( int argc, char const **argv )
{
int max_fd = 0, backlog = 0, i = 0, s = 0;
struct sockaddr_un s_addr;
struct timeval tv;
struct uinfo ui;
size_t len = 0;
const char* socket_path = NULL, *template_path = NULL;
FILE *template_file = NULL;
mode_t ou = 0;
fd_set client_sockets_set;
VALIDATE((passwd = getpwuid(getuid())) != NULL, "cannot get user info");
// Setup global vars and defaults
backlog = MAX_CLIENTS;
output_dir = DEFAULT_OUTPUT_PATH;
socket_path = DEFAULT_SOCKET_PATH;
output_file_name = DEFAULT_OUTPUTFILE;
output_fd = NULL;
template_path = NULL;
curate_html = false;
username = passwd->pw_name;
template = default_template;
string_init(&output_path);
string_init(&custom_template);
MEMSET_ZERO(client_buffer);
MEMSET_ZERO(client_sockets);
MEMSET_ZERO(s_addr);
MEMSET_ZERO(tv);
MEMSET_ZERO(ui);
MEMSET_ZERO(client_sockets_set);
// To stop gcc from giving me boggus warnings
UNUSED(string_shrink);
// Callback setup
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
atexit(cleannup);
while((i = getopt(argc, (char * const*)argv, "s:t:o:cn:h")) != -1)
{
switch(i)
{
case 's':
socket_path = optarg;
VALIDATE(strlen(socket_path) > 0, "socket path cannot be empty");
break;
case 't':
template_path = optarg;
VALIDATE(strlen(template_path) > 0, "template path cannot be empty");
break;
case 'o':
output_dir = optarg;
VALIDATE(strlen(output_dir) > 0, "output dir cannot be empty");
break;
case 'c':
curate_html = true;
break;
case 'n':
output_file_name = optarg;
VALIDATE(strlen(output_file_name) > 0, "output file name cannot be empty");
break;
case 'h':
help();
exit(0);
break;
default:
help();
exit(1);
break;
}
}
// If a template file was specified, save it in memory
if(template_path != NULL)
{
template_file = fopen(template_path, "r");
VALIDATE_STD(template_file != NULL, "cannot open template file");
fseek(template_file, 0L, SEEK_END);
len = ftell(template_file) +1;
fseek(template_file, 0L, SEEK_SET);
MEM_CATCH(string_reserve(&custom_template, len));
fread(custom_template.data, 1, len, template_file);
custom_template.data[len-1] = '\0';
template = custom_template.data;
fclose(template_file);
INFO("loaded template from file \"%s\"", template_path);
}
else
curate_html = true;
// Create output directory if it doesn't exists
create_directory(output_dir);
// Setup select timeout
tv.tv_sec = SELECT_TIMEOUT;
tv.tv_usec = 0;
// Create server unix socket
s_addr.sun_family = AF_UNIX;
CATCHER_CRITICAL(
server_socket = socket(AF_UNIX, SOCK_STREAM, 0),
"cannot create server socket"
);
// Apply socket mask to socket and bind it to socket path
strncpy(s_addr.sun_path, socket_path, GET_LEN(s_addr.sun_path));
unlink(socket_path);
ou = umask(SOCKET_MASK);
CATCHER_CRITICAL(
bind(server_socket, (const struct sockaddr *) &s_addr, sizeof(s_addr)),
"cannot bind network socket"
);
umask(ou);
// Listen to a backlog amount of connections
CATCHER_CRITICAL(
listen(server_socket, backlog),
"cannot set socket backlog"
);
INFO("listening to socket on \"%s\"", socket_path);
// Main loop
keep_running = true;
while(keep_running)
{
// Make sure the set's bits are clear and
// add the server socket to it
FD_ZERO(&client_sockets_set);
FD_SET(server_socket, &client_sockets_set);
max_fd = server_socket;
// Add the clients to the set as well
for(i = 0; i < MAX_CLIENTS; i++)
{
s = client_sockets[i];
if(s <= 0)
continue;
FD_SET(s, &client_sockets_set);
if(s > max_fd) max_fd = s;
}
// Get socket activity
s = select(max_fd+1, &client_sockets_set, NULL, NULL, &tv);
if(s < 0)
{
if(errno == EINTR)
continue;
STD_CATCHER_CRITICAL(-1, "server socket error");
}
// Accept an incomming connection
if(FD_ISSET(server_socket, &client_sockets_set))
{
STD_CATCHER(
s = accept(server_socket, (struct sockaddr *)&s_addr, (socklen_t*)&len),
"cannot accept incomming connection"
);
if(s < 0)
continue;
// We won't accept incomming connections from
// users we can't recognize
STD_CATCHER_CRITICAL(get_uinfo(s, &ui), "cannot get user info");
INFO("accepted connection from user \"%s\"", ui.name);
// Allocate sub socket into socket list
for(i = 0; i < MAX_CLIENTS; i++)
{
if(client_sockets[i] > 0)
continue;
client_sockets[i] = s;
break;
}
}
// Serve user(s)
for(i = 0; i < MAX_CLIENTS; i++)
{
if(client_sockets[i] <= 0 || !FD_ISSET(client_sockets[i], &client_sockets_set))
continue;
// TODO: Use the return values from serve
serve(client_sockets[i], &ui);
// Make sure we don't get more notifications form this socket
FD_CLR(client_sockets[i], &client_sockets_set);
// Close the socket, we won't keep the connections alive
// for more than one action/request
close(client_sockets[i]);
INFO("closed connection from user \"%s\"", ui.name);
client_sockets[i] = 0;
}
FD_ZERO(&client_sockets_set);
SLEEP_FOR_US(IDLE_TIME);
}
return 0;
}