summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--include/gemini.h27
-rw-r--r--include/server.h22
-rw-r--r--src/server.c186
3 files changed, 216 insertions, 19 deletions
diff --git a/include/gemini.h b/include/gemini.h
new file mode 100644
index 0000000..dccaf4e
--- /dev/null
+++ b/include/gemini.h
@@ -0,0 +1,27 @@
+#ifndef GMNISRV_GEMINI_H
+#define GMNISRV_GEMINI_H
+#define GEMINI_MAX_URL 1024
+
+enum gemini_status {
+ GEMINI_STATUS_NONE = 0,
+ GEMINI_STATUS_INPUT = 10,
+ GEMINI_STATUS_SENSITIVE_INPUT = 11,
+ GEMINI_STATUS_SUCCESS = 20,
+ GEMINI_STATUS_REDIRECT_TEMPORARY = 30,
+ GEMINI_STATUS_REDIRECT_PERMANENT = 31,
+ GEMINI_STATUS_TEMPORARY_FAILURE = 40,
+ GEMINI_STATUS_SERVER_UNAVAILABLE = 41,
+ GEMINI_STATUS_CGI_ERROR = 42,
+ GEMINI_STATUS_PROXY_ERROR = 43,
+ GEMINI_STATUS_SLOW_DOWN = 44,
+ GEMINI_STATUS_PERMANENT_FAILURE = 50,
+ GEMINI_STATUS_NOT_FOUND = 51,
+ GEMINI_STATUS_GONE = 52,
+ GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53,
+ GEMINI_STATUS_BAD_REQUEST = 59,
+ GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60,
+ GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61,
+ GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62,
+};
+
+#endif
diff --git a/include/server.h b/include/server.h
index 5624b52..0317de9 100644
--- a/include/server.h
+++ b/include/server.h
@@ -2,21 +2,39 @@
#define GMNISRV_SERVER
#include <openssl/ssl.h>
#include <poll.h>
+#include <time.h>
#include <stdbool.h>
+#include "gemini.h"
+#include "url.h"
-#define GEMINI_MAX_URL 1024
+struct gmnisrv_server;
+
+enum response_state {
+ RESPOND_HEADER,
+ RESPOND_BODY,
+};
struct gmnisrv_client {
+ struct gmnisrv_server *server;
+ struct timespec ctime;
struct sockaddr addr;
socklen_t addrlen;
int sockfd;
+ struct pollfd *pollfd;
SSL *ssl;
- BIO *bio;
+ BIO *bio, *sbio;
char buf[GEMINI_MAX_URL + 3];
+ size_t bufix, bufln;
+ enum response_state state;
+ enum gemini_status status;
+ char *meta;
+ int bodyfd;
+
struct gmnisrv_host *host;
+ char *path;
};
struct gmisrv_config;
diff --git a/src/server.c b/src/server.c
index b7d580e..9e612e6 100644
--- a/src/server.c
+++ b/src/server.c
@@ -11,9 +11,11 @@
#include <sys/socket.h>
#include <unistd.h>
#include "config.h"
+#include "gemini.h"
#include "log.h"
#include "server.h"
#include "tls.h"
+#include "url.h"
int
server_init(struct gmnisrv_server *server, struct gmnisrv_config *conf)
@@ -144,17 +146,49 @@ accept_client(struct gmnisrv_server *server, int fd)
}
struct gmnisrv_client *client = &server->clients[server->nclients++];
+ memset(client, 0, sizeof(*client));
client->sockfd = sockfd;
+ client->pollfd = pollfd;
client->addrlen = addrlen;
+ client->server = server;
+ clock_gettime(CLOCK_MONOTONIC, &client->ctime);
memcpy(&client->addr, &addr, sizeof(addr));
+
pollfd->fd = sockfd;
pollfd->events = POLLIN;
}
+
+static void
+timespec_diff(struct timespec *start,
+ struct timespec *stop, struct timespec *out)
+{
+ if ((stop->tv_nsec - start->tv_nsec) < 0) {
+ out->tv_sec = stop->tv_sec - start->tv_sec - 1;
+ out->tv_nsec = stop->tv_nsec - start->tv_nsec + 1000000000;
+ } else {
+ out->tv_sec = stop->tv_sec - start->tv_sec;
+ out->tv_nsec = stop->tv_nsec - start->tv_nsec;
+ }
+}
+
static void
disconnect_client(struct gmnisrv_server *server, struct gmnisrv_client *client)
{
+ if (client->status != GEMINI_STATUS_NONE) {
+ struct timespec now, diff;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ timespec_diff(&client->ctime, &now, &diff);
+ int ms = diff.tv_sec * 1000 + (int)(diff.tv_nsec / 1.0e6);
+ client_log(&client->addr, "%dms %s %s %02d %s", ms,
+ client->host ? client->host->hostname : "(none)",
+ client->path ? client->path : "(none)",
+ (int)client->status, client->meta);
+ }
close(client->sockfd);
+ free(client->meta);
+ // TODO: Close bios, body, etc
+
size_t index = (client - server->clients) / sizeof(struct gmnisrv_client);
memmove(client, &client[1], &server->clients[server->clientsz] - client);
memmove(&server->fds[server->nlisten + index],
@@ -187,20 +221,104 @@ client_init_ssl(struct gmnisrv_server *server, struct gmnisrv_client *client)
return 1;
}
- BIO *sbio = BIO_new(BIO_f_ssl());
- BIO_set_ssl(sbio, client->ssl, 0);
+ client->sbio = BIO_new(BIO_f_ssl());
+ BIO_set_ssl(client->sbio, client->ssl, 0);
client->bio = BIO_new(BIO_f_buffer());
- BIO_push(client->bio, sbio);
+ BIO_push(client->bio, client->sbio);
return 0;
}
static void
+client_submit_response(struct gmnisrv_client *client,
+ enum gemini_status status, const char *meta, int bodyfd)
+{
+ client->status = status;
+ client->meta = strdup(meta);
+ client->bodyfd = bodyfd;
+ client->pollfd->events = POLLOUT;
+}
+
+static void
+client_oom(struct gmnisrv_client *client)
+{
+ const char *error = "Out of memory";
+ client_submit_response(client,
+ GEMINI_STATUS_TEMPORARY_FAILURE, error, -1);
+}
+
+static bool
+request_validate(struct gmnisrv_client *client, char **path)
+{
+ struct Curl_URL *url = curl_url();
+ if (!url) {
+ client_oom(client);
+ return false;
+ }
+ if (curl_url_set(url, CURLUPART_URL, client->buf, 0) != CURLUE_OK) {
+ const char *error = "Protocol error: invalid URL";
+ client_submit_response(client,
+ GEMINI_STATUS_BAD_REQUEST, error, -1);
+ goto exit;
+ }
+
+ char *part;
+ if (curl_url_get(url, CURLUPART_SCHEME, &part, 0) != CURLUE_OK) {
+ const char *error = "Protocol error: invalid URL (expected scheme)";
+ client_submit_response(client,
+ GEMINI_STATUS_BAD_REQUEST, error, -1);
+ goto exit;
+ } else if (strcmp(part, "gemini") != 0) {
+ free(part);
+ const char *error = "Refusing proxy to non-gemini URL";
+ client_submit_response(client,
+ GEMINI_STATUS_PROXY_REQUEST_REFUSED, error, -1);
+ goto exit;
+ }
+ free(part);
+
+ if (curl_url_get(url, CURLUPART_HOST, &part, 0) != CURLUE_OK) {
+ const char *error = "Protocol error: invalid URL (expected host)";
+ client_submit_response(client,
+ GEMINI_STATUS_BAD_REQUEST, error, -1);
+ goto exit;
+ } else if (strcmp(part, client->host->hostname) != 0) {
+ free(part);
+ const char *error = "Protocol error: hostname does not match SNI";
+ client_submit_response(client,
+ GEMINI_STATUS_BAD_REQUEST, error, -1);
+ goto exit;
+ }
+ free(part);
+
+ if (curl_url_get(url, CURLUPART_PATH, &part, 0) != CURLUE_OK) {
+ const char *error = "Protocol error: invalid URL (expected path)";
+ client_submit_response(client,
+ GEMINI_STATUS_BAD_REQUEST, error, -1);
+ goto exit;
+ }
+ // NOTE: curl_url_set(..., CURLUPART_URL, ..., 0) will consoldate .. and
+ // . to prevent directory traversal without additional code.
+ *path = part;
+
+exit:
+ curl_url_cleanup(url);
+ return true;
+}
+
+static void
client_readable(struct gmnisrv_server *server, struct gmnisrv_client *client)
{
if (!client->ssl && client_init_ssl(server, client) != 0) {
return;
}
+ if (!client->host) {
+ const char *error = "This server requires clients to support the TLS SNI (server name identification) extension";
+ client_submit_response(client,
+ GEMINI_STATUS_BAD_REQUEST, error, -1);
+ return;
+ }
+ // XXX: Can buf be statically allocated?
int r = BIO_gets(client->bio, client->buf, sizeof(client->buf));
if (r <= 0) {
r = SSL_get_error(client->ssl, r);
@@ -214,32 +332,66 @@ client_readable(struct gmnisrv_server *server, struct gmnisrv_client *client)
}
client->buf[r] = '\0';
- if (!client->host) {
- // TODO: We can do a friendly disconnect at this point
- client_error(&client->addr,
- "client did not perform SNI, disconnecting");
- disconnect_client(server, client);
- return;
- }
-
char *newline = strstr(client->buf, "\r\n");
if (!newline) {
- // TODO: We can do a friendly disconnect at this point
- client_error(&client->addr, "protocol error, disconnecting");
- disconnect_client(server, client);
+ const char *error = "Protocol error: malformed request";
+ client_submit_response(client,
+ GEMINI_STATUS_BAD_REQUEST, error, -1);
return;
}
*newline = 0;
- client_log(&client->addr, "%s", client->buf);
+ if (!request_validate(client, &client->path)) {
+ return;
+ }
+
+ // TODO: prep response
+ const char *error = "TODO: Finish implementation";
+ client_submit_response(client,
+ GEMINI_STATUS_TEMPORARY_FAILURE, error, -1);
}
static void
client_writable(struct gmnisrv_server *server, struct gmnisrv_client *client)
{
- // TODO
+ int r;
+ ssize_t n;
+ switch (client->state) {
+ case RESPOND_HEADER:
+ if (client->bufix == 0) {
+ assert(strlen(client->meta) <= 1024);
+ n = snprintf(client->buf, sizeof(client->buf),
+ "%02d %s\r\n", (int)client->status,
+ client->meta);
+ assert(n > 0);
+ client->bufln = n;
+ }
+ r = BIO_write(client->sbio, &client->buf[client->bufix],
+ client->bufln - client->bufix);
+ if (r <= 0) {
+ r = SSL_get_error(client->ssl, r);
+ if (r == SSL_ERROR_WANT_WRITE) {
+ return;
+ }
+ client->status = GEMINI_STATUS_NONE;
+ client_error(&client->addr, "SSL read error %s, disconnecting",
+ ERR_error_string(r, NULL));
+ disconnect_client(server, client);
+ return;
+ }
+ client->bufix += r;
+ if (client->bufix >= client->bufln) {
+ // TODO: Start sending response body as well
+ disconnect_client(server, client);
+ return;
+ }
+ break;
+ case RESPOND_BODY:
+ assert(0);
+ break;
+ }
+
(void)server;
- (void)client;
}
static long