592 lines
16 KiB
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, "<");
|
|
break;
|
|
case '>':
|
|
fprintf(output_fd, ">");
|
|
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;
|
|
}
|