/*
* Copyright (C) 2022 Camden Dixie O'Brien
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this program. If not, see
* .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define HOST "::1"
#define PORT 7070
#define RESP_TERM ".\r\n"
#define RESP_TERM_LEN 3 /* strlen(".\r\n") */
#define PBUF_SIZE 1024
#define RBUF_SIZE 1024
#define SBUF_SIZE 1024
#define FBUF_SIZE 1024
/*
* Enumeration of directory entry types.
*
* The numeric values are significant here (hence them being set
* explicitly): they match those defined in RFC 1436.
*/
enum etype { ETYPE_FILE = 0, ETYPE_DIR = 1 };
static bool exit_requested = false;
void handle_exit_signal(int signum)
{
(void)signum;
exit_requested = true;
}
static int append_selector_part(const char *part, size_t partlen, char *buf,
size_t bufsize)
{
if (partlen == 0)
return 0;
if (strncmp(part, "..", partlen) == 0) {
fprintf(stderr, "Selector string contains \"..\"\n");
return -1;
} else if (partlen + 1 > bufsize) {
fprintf(stderr, "Path buffer too small\n");
return -1;
}
*buf++ = '/';
memcpy(buf, part, partlen);
return partlen + 1;
}
int main(int argc, char *argv[])
{
(void)argc;
(void)argv;
int res = EXIT_SUCCESS;
static char pbuf[PBUF_SIZE], rbuf[RBUF_SIZE], sbuf[SBUF_SIZE],
fbuf[FBUF_SIZE];
/*
* Get srvroot path from arguments and copy into pbuf.
*
* The srvroot being at the start of pbuf should be maintained
* through the whole application. Any trailing '/' is removed if
* present.
*/
if (argc < 2) {
fprintf(stderr, "Usage: %s srvroot\n", argv[0]);
return EXIT_FAILURE;
}
size_t srvroot_len = strlen(argv[1]);
if (argv[1][srvroot_len - 1] == '/')
--srvroot_len;
if (srvroot_len > PBUF_SIZE) {
fprintf(stderr, "srvroot path is too long\n");
return EXIT_FAILURE;
}
memcpy(pbuf, argv[1], srvroot_len);
/*
* Register signal handler for SIGTERM and SIGINT.
*
* The behaviour of signal() is not entirely consistent across
* different implementations, so this should ideally be rewritten
* to use signaction() instead at some point.
*/
if (signal(SIGTERM, handle_exit_signal) == SIG_ERR) {
fprintf(stderr, "Failed to register SIGTERM signal handler\n");
return EXIT_FAILURE;
}
if (signal(SIGINT, handle_exit_signal) == SIG_ERR) {
fprintf(stderr, "Failed to register SIGINT signal handler\n");
return EXIT_FAILURE;
}
/*
* Initialise socket.
*
* Currently only supporting IPv6, and hard-coding address and
* port. Should eventully get address and port from arguments, and
* support IPv4 as well.
*/
int sfd = socket(AF_INET6, SOCK_STREAM, 0);
if (sfd == -1) {
fprintf(stderr, "Failed to open socket\n");
return EXIT_FAILURE;
}
const struct sockaddr_in6 haddr = {
.sin6_family = AF_INET6,
.sin6_port = htons(PORT),
.sin6_addr = IN6ADDR_LOOPBACK_INIT,
};
if (bind(sfd, (const struct sockaddr *)&haddr, sizeof(haddr)) == -1) {
fprintf(stderr, "Error binding socket to address\n");
res = EXIT_FAILURE;
goto close_server_socket;
}
if (listen(sfd, 8) == -1) {
fprintf(stderr, "Error attempting to listen on socket\n");
res = EXIT_FAILURE;
goto close_server_socket;
}
struct sockaddr_in6 paddr;
socklen_t paddr_size = sizeof(paddr);
int cfd;
ssize_t n, slen, rlen;
while (!exit_requested) {
n = slen = rlen = 0;
/*
* Accept incoming connection.
*
* If this is interrupted we go to the next loop iteration
* rather than retrying directly in case the received signal
* was requesting that the server exits.
*
* System calls after this point should be retried before
* checking if we need to exit, as we don't want to leave the
* client hanging.
*/
cfd = accept(sfd, (struct sockaddr *)&paddr, &paddr_size);
if (cfd == -1) {
if (errno != EINTR)
fprintf(stderr, "Error accepting connection\n");
continue;
}
/*
* Read selector from client socket.
*
* For now, assuming that the selector is less than RDBUF
* bytes long.
*/
do {
errno = 0;
n = read(cfd, sbuf, SBUF_SIZE);
} while (errno == EINTR);
if (n == -1) {
fprintf(stderr, "Error reading selector from client\n");
goto close_client_socket;
}
/*
* Locate the end of the selector.
*
* The end of the selector string is indicated with CRLF. Most
* of the time this will be at the end of the received data,
* so might as well start from there.
*/
slen = n;
for (unsigned i = n - 2; i < n; --i) {
if (sbuf[i] == 0x0d && sbuf[i + 1] == 0x0a)
slen = i;
}
if (slen == n) {
fprintf(stderr, "Received invalid selector (no CRLF)\n");
goto close_client_socket;
}
/*
* Construct the full path to the requested resource.
*
* This is needed in order to pass it to stat() and fopen() or
* opendir() later. It must be null-terminated. We also need
* to make sure that none of the path's parts are "..", as
* this could be used to escape the srvroot directory.
*/
char *sp = sbuf;
unsigned splen = 0, ppos = srvroot_len;
for (unsigned i = 0; i < slen; ++i) {
if (sbuf[i] != '/') {
++splen;
continue;
}
n = append_selector_part(sp, splen, &pbuf[ppos], PBUF_SIZE - ppos);
if (n == -1)
goto send_response;
ppos += n;
sp = &sbuf[i + 1];
splen = 0;
}
n = append_selector_part(sp, splen, &pbuf[ppos], PBUF_SIZE - ppos);
if (n == -1)
goto send_response;
ppos += n;
/*
* Determine whether the resource is a file or a directory.
*
* The path passed to stat() has to be null-terminated, which
* pbuf is not, so a trailing null byte must first be added
* there.
*/
if (ppos + 1 > PBUF_SIZE) {
fprintf(stderr, "Path buffer is too small\n");
goto send_response;
}
pbuf[ppos] = '\0';
struct stat rstat;
if (stat(pbuf, &rstat) == -1) {
fprintf(stderr, "Failed to stat() path \"%s\"\n", pbuf);
goto send_response;
}
if (S_ISREG(rstat.st_mode)) {
/*
* Open the file.
*
* The path is already null-terminated from the earlier call
* to stat().
*/
FILE *rf;
do {
errno = 0;
rf = fopen(pbuf, "r");
} while (errno == EINTR);
if (rf == NULL) {
fprintf(stderr, "Failed to open %s\n", pbuf);
goto send_response;
}
/*
* Construct response in rbuf.
*
* The response should be the file contents, but with CRLF
* line endings instead of just LF. Replacing the line
* endings is done with a buffered reading approach to
* minimize syscalls (as opposed to using fgetc()).
*/
while (true) {
/* Fill fbuf from file. */
n = fread(fbuf, sizeof(*fbuf), FBUF_SIZE, rf);
if (n != FBUF_SIZE && ferror(rf)) {
fprintf(stderr, "Failed to read from file %s\n", pbuf);
goto send_response;
}
/* Copy from fbuf to rbuf, replacing LF with CRLF. */
for (unsigned fpos = 0; fpos < n; ++fpos) {
if (fbuf[fpos] == '\n') {
if (RBUF_SIZE - rlen < 2) {
fprintf(stderr, "Response buffer is too small");
goto send_response;
}
rbuf[rlen++] = '\r';
rbuf[rlen++] = '\n';
} else {
if (RBUF_SIZE - rlen < 1) {
fprintf(stderr, "Response buffer is too small");
goto send_response;
}
rbuf[rlen++] = fbuf[fpos];
}
}
if (feof(rf))
break;
}
fclose(rf);
} else if (S_ISDIR(rstat.st_mode)) {
/*
* Open the directory.
*
* The path is already null-terminated from the earlier call
* to stat().
*/
DIR *rdir;
do {
errno = 0;
rdir = opendir(pbuf);
} while (errno == EINTR);
if (rdir == NULL) {
fprintf(stderr, "Failed to open %s\n", pbuf);
goto send_response;
}
/*
* Write a line for each entry in the directory to the client.
*/
struct dirent *ent;
unsigned namelen;
while ((ent = readdir(rdir)) != NULL) {
/*
* Skip . and .. entries.
*/
if (strcmp(ent->d_name, ".") == 0
|| strcmp(ent->d_name, "..") == 0)
continue;
/*
* Construct full path to entry in pbuf.
*
* This is needed in order to pass it to stat() later. The
* path must be null-terminated.
*/
namelen = strlen(ent->d_name);
if (ppos + namelen + 2 > PBUF_SIZE) {
fprintf(stderr, "Path buffer is too small\n");
goto send_response;
}
pbuf[ppos] = '/';
memcpy(&pbuf[ppos + 1], ent->d_name, namelen);
pbuf[ppos + 1 + namelen] = '\0';
/*
* Identify entry type from file inode information.
*/
if (stat(pbuf, &rstat) == -1) {
fprintf(stderr,
"Failed to stat() path \"%s\", skipping entry\n",
pbuf);
continue;
}
enum etype type;
if (S_ISREG(rstat.st_mode))
type = ETYPE_FILE;
else if (S_ISDIR(rstat.st_mode))
type = ETYPE_DIR;
else
continue;
/*
* Format response line for current entry.
*/
n = snprintf(&rbuf[rlen], RBUF_SIZE - rlen,
"%1u%s\t%s\t%s\t%u\r\n", type, ent->d_name,
&pbuf[srvroot_len], HOST, PORT);
if (n >= RBUF_SIZE - rlen) {
fprintf(stderr, "Response buffer was too small\n");
goto send_response;
}
rlen += n;
}
closedir(rdir);
} else {
fprintf(stderr,
"Requested resource \"%s\" was not a directory or a "
"regular file\n",
pbuf);
goto send_response;
}
/*
* Terminate and send the response.
*/
send_response:
if (RBUF_SIZE - rlen < RESP_TERM_LEN) {
fprintf(stderr, "Response buffer is too small");
goto close_client_socket;
}
memcpy(&rbuf[rlen], RESP_TERM, RESP_TERM_LEN);
rlen += RESP_TERM_LEN;
do {
errno = 0;
n = write(cfd, rbuf, rlen);
} while (errno == EINTR);
if (n == -1) {
fprintf(stderr, "Failed to write response to client\n");
goto close_client_socket;
}
close_client_socket:
close(cfd);
}
close_server_socket:
close(sfd);
return res;
}