summaryrefslogtreecommitdiff
path: root/libphysfs-serve.c
diff options
context:
space:
mode:
Diffstat (limited to 'libphysfs-serve.c')
-rw-r--r--libphysfs-serve.c237
1 files changed, 237 insertions, 0 deletions
diff --git a/libphysfs-serve.c b/libphysfs-serve.c
new file mode 100644
index 0000000..eed3429
--- /dev/null
+++ b/libphysfs-serve.c
@@ -0,0 +1,237 @@
+#include "libphysfs-serve.h"
+#include <physfs.h>
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <unistd.h>
+#include <err.h>
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+
+static void ignore_ret(bool b, ...) {};
+#define ignore_ret(x) ignore_ret(true, x);
+
+static void
+response_501(struct physfs_serve *serve, const int fd) {
+ const char buf[] = "HTTP/1.1 501 Not Implemented\r\n" \
+ "Server: libphysfs-serve\r\n" \
+ "Connection: close\r\n";
+ ignore_ret(write(fd, buf, sizeof(buf) - 1));
+}
+
+static void
+response_404(struct physfs_serve *serve, const int fd) {
+ const char buf[] = "HTTP/1.1 404 Not Found\r\n" \
+ "Server: libphysfs-serve\r\n" \
+ "Connection: close\r\n";
+ ignore_ret(write(fd, buf, sizeof(buf) - 1));
+}
+
+static bool
+get(struct physfs_serve *serve, const int fd, const char *path) {
+ struct PHYSFS_Stat st = {0};
+ const int exists = PHYSFS_stat(path, &st);
+ st.filetype = (exists ? st.filetype : -1);
+
+ char fullpath[1024];
+ snprintf(fullpath, sizeof(fullpath), "%s", path);
+ if (serve->path_rewrite(serve, fullpath, sizeof(fullpath), &st)) {
+ if (!PHYSFS_stat(fullpath, &st)) {
+ response_404(serve, fd);
+ return false;
+ }
+ }
+
+ PHYSFS_File *f;
+ if (!(f = PHYSFS_openRead(fullpath))) {
+ response_404(serve, fd);
+ return false;
+ }
+
+ char buf[1024];
+ snprintf(buf, sizeof(buf),
+ "HTTP/1.1 200 OK\r\n" \
+ "Server: libphysfs-serve\r\n" \
+ "Content-Type: %s\r\n" \
+ "Content-Length: %zu\r\n\r\n", serve->content_type(serve, fullpath), (size_t)st.filesize);
+ ignore_ret(write(fd, buf, strlen(buf)));
+ for (int ret = 0; (ret = PHYSFS_readBytes(f, buf, sizeof(buf))) > 0;)
+ ignore_ret(write(fd, buf, ret));
+
+ PHYSFS_close(f);
+ return true;
+}
+
+static char*
+strchr_safe(const char *s, int c) {
+ char *ret = strchr(s, c);
+ return (ret ? ret : (char*)s + strlen(s));
+}
+
+static bool
+urldecode(const char *in, char *out) {
+ static const char tbl[256] = {
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1,
+ -1,10,11,12,13,14,15,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,10,11,12,13,14,15,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1
+ };
+ for (char c; (c = *in++); *out++ = c) {
+ if (c != '%') continue;
+ char v1, v2;
+ if ((v1 = tbl[(uint8_t)*in++]) < 0 || (v2 = tbl[(uint8_t)*in++]) < 0)
+ return false;
+ c = (v1 << 4) | v2;
+ }
+ *out = 0;
+ return true;
+}
+
+static bool
+handle_request(struct physfs_serve *serve, const int fd) {
+ char buf[1024];
+ size_t len;
+ if (!(len = read(fd, buf, sizeof(buf) - 1)))
+ return false;
+
+ buf[len] = 0;
+ const char method[] = "GET";
+ if (strncmp(method, buf, sizeof(method) - 1)) {
+ response_501(serve, fd);
+ return false;
+ } else {
+ const char *path = buf + sizeof(method);
+ *strchr_safe(path, ' ') = 0;
+ char decoded[1024];
+ assert(strlen(path) < sizeof(decoded));
+ urldecode(path, decoded);
+ *strchr_safe(decoded, '?') = 0;
+ get(serve, fd, decoded);
+ }
+ return true;
+}
+
+union sockaddr_u {
+ struct sockaddr out;
+ struct sockaddr_in in;
+};
+
+bool
+physfs_serve_init(struct physfs_serve *serve, const unsigned int port, const char *addr) {
+ int listen_fd, conn_fd;
+ if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
+ warn("socket");
+ goto fail;
+ }
+
+ union sockaddr_u server = {0};
+ inet_pton(AF_INET, addr, &server.in);
+ server.in.sin_port = htons(port);
+ setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, (int[]){1}, sizeof(int));
+
+ if (bind(listen_fd, &server.out, sizeof(server.out)) == -1) {
+ warn("bind");
+ goto fail2;
+ }
+
+ if (listen(listen_fd, 0) == -1) {
+ warn("listen");
+ goto fail2;
+ }
+
+ if (!serve->content_type) serve->content_type = physfs_default_content_type;
+ if (!serve->path_rewrite) serve->path_rewrite = physfs_default_path_rewrite;
+ memset(serve->fd, -1, sizeof(serve->fd));
+ serve->fd[serve->nfds++] = listen_fd;
+ return true;
+
+fail2:
+ close(listen_fd);
+fail:
+ PHYSFS_deinit();
+ return false;
+}
+
+bool
+physfs_serve_event(struct physfs_serve *serve, const int index) {
+ if (index == 0) {
+ if (serve->nfds >= ARRAY_SIZE(serve->fd)) {
+ warnx("max connections reached");
+ return true;
+ }
+ union sockaddr_u client = {0};
+ if ((serve->fd[serve->nfds++] = accept(serve->fd[index], &client.out, (socklen_t[]){sizeof(client.out)})) == -1) {
+ warn("accept");
+ return false;
+ }
+ } else {
+ assert(serve->nfds > 1);
+ if (!handle_request(serve, serve->fd[index])) {
+ close(serve->fd[index]);
+ serve->fd[index] = serve->fd[--serve->nfds];
+ serve->fd[serve->nfds] = -1;
+ }
+ }
+ return true;
+}
+
+void
+physfs_serve_free(struct physfs_serve *serve) {
+ if (!serve) return;
+ for (int i = 0; i < ARRAY_SIZE(serve->fd); ++i)
+ if (serve->fd[i] != -1) close(serve->fd[i]);
+ *serve = (struct physfs_serve){0};
+ memset(serve->fd, -1, sizeof(serve->fd));
+}
+
+const char*
+physfs_default_content_type(struct physfs_serve *serve, const char *path) {
+ if (physfs_ends_with(path, ".html")) return "text/html";
+ else if (physfs_ends_with(path, ".js")) return "application/javascript";
+ else if (physfs_ends_with(path, ".json")) return "application/json";
+ else if (physfs_ends_with(path, ".css")) return "text/css";
+ else if (physfs_ends_with(path, ".ttf")) return "font/ttf";
+ else if (physfs_ends_with(path, ".ico")) return "image/x-icon";
+ else if (physfs_ends_with(path, ".png")) return "image/png";
+ else if (physfs_ends_with(path, ".jpg")) return "image/jpg";
+ return ""; // let browser do its thing
+}
+
+bool
+physfs_default_path_rewrite(struct physfs_serve *serve, char buf[], const size_t bufsz, struct PHYSFS_Stat *st) {
+ if (st->filetype == PHYSFS_FILETYPE_DIRECTORY) {
+ physfs_append(buf, bufsz, "/index.html");
+ return true;
+ }
+ return false;
+}
+
+bool
+physfs_ends_with(const char *str, const char *suf) {
+ const size_t strn = strlen(str), sufn = strlen(suf);
+ if (strn < sufn) return false;
+ return !strcmp(str + strn - sufn, suf);
+}
+
+void
+physfs_append(char buf[], const size_t bufsz, const char *str) {
+ const size_t len = strlen(buf), left = bufsz - len, want = strlen(str);
+ const size_t need = (left > want ? want : left);
+ memcpy(buf + len, str, need);
+ buf[len + need] = 0;
+}