summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDrew DeVault <sir@cmpwn.com>2020-10-25 21:46:01 -0400
committerDrew DeVault <sir@cmpwn.com>2020-10-25 21:46:01 -0400
commit8baeb5a51c4dfa03956887ade2ef77295f17c95e (patch)
tree533a043ed887483b905f6a684986ebaf9072d11c
parent1fe107875b05cc07cf62c714c0136026eef7b93a (diff)
downloadgmnisrv-8baeb5a51c4dfa03956887ade2ef77295f17c95e.tar.gz
gmnisrv-8baeb5a51c4dfa03956887ade2ef77295f17c95e.tar.xz
gmnisrv-8baeb5a51c4dfa03956887ade2ef77295f17c95e.zip
Initial implementation of a routing table
-rw-r--r--doc/gmnisrvini.scd41
-rw-r--r--include/config.h23
-rw-r--r--src/config.c91
-rw-r--r--src/serve.c53
4 files changed, 183 insertions, 25 deletions
diff --git a/doc/gmnisrvini.scd b/doc/gmnisrvini.scd
index a140ad0..73049a9 100644
--- a/doc/gmnisrvini.scd
+++ b/doc/gmnisrvini.scd
@@ -42,15 +42,46 @@ The following keys are accepted under the *[:tls]* section:
the name of the organization responsible for the host and it will be
filled in as the X.509 /O name.
-## HOST KEYS
+## ROUTING KEYS
-Hosts that *gmnisrv* is to serve shall be defined in *gmnisrv.ini* by
-introducing config sections named after each host to provide service for. The
-following keys apply:
+To configure *gmnisrv* to service requests, routing keys must be defined. The
+name of the configuration section is used to determine what kinds of requests it
+configures.
+
+The format of the section name is the _hostname_ to be serviced, followed by a
+token which defines the routing strategy, and a string whose format is specific
+to each routing strategy. The token and match string may be omitted
+(i.e. [_hostname_] alone), which implies path routing against "/".
+
+|] *:*
+:< Route by path prefix. The URL path is compared to "_string_/".
+| *=*
+: Exact match. The URL path must exactly match the string.
+| *~*
+: Regular expression routing. The string is a JavaScript-compatible regular
+ expression which is tested against the URL path.
+
+Some example section names and examples of matching paths:
+
+|[ *[example.org:/foo]*
+:< /foo, /foo/bar, /foo/bar/baz
+| *[example.org=/foo.txt]*
+: /foo.txt
+| *[example.org~/[a-z]+\\.(png|jpg|webp)*
+: /foo.png, /bar.webp
+
+Routes should be ordered from least to most specific. The matching algorithm
+attempts to match the URL against each route in reverse order, and chooses the
+first route which matches.
+
+Within each routing section, the following keys are used to configure how
+*gmnisrv* will respond to matching requests:
*root*
Configures the path on disk from which files shall be served for this
- host.
+ host. If using path prefix matching, the prefix is trimmed, so if
+ example.org/foo/bar.txt is requested and matches *[example.org:/foo]*,
+ "bar.txt" will be appended to the root to form the file path.
*index*
Configures the name of the index file which shall be served in the event
diff --git a/include/config.h b/include/config.h
index e0d7947..a489f2d 100644
--- a/include/config.h
+++ b/include/config.h
@@ -2,6 +2,7 @@
#define GMNISRV_CONFIG
#include <arpa/inet.h>
#include <openssl/x509.h>
+#include <regex.h>
#include <stdbool.h>
struct gmnisrv_tls {
@@ -10,15 +11,33 @@ struct gmnisrv_tls {
SSL_CTX *ssl_ctx;
};
-struct gmnisrv_host {
- char *hostname;
+enum gmnisrv_routing {
+ ROUTE_PATH,
+ ROUTE_REGEX,
+};
+
+struct gmnisrv_route {
+ enum gmnisrv_routing routing;
+ char *spec;
+ union {
+ char *path;
+ regex_t *regex;
+ };
+
char *root;
char *index;
bool autoindex;
+ struct gmnisrv_route *next;
+};
+
+struct gmnisrv_host {
+ char *hostname;
X509 *x509;
EVP_PKEY *pkey;
+ struct gmnisrv_route *routes;
+
struct gmnisrv_host *next;
};
diff --git a/src/config.c b/src/config.c
index 36060cf..693766e 100644
--- a/src/config.c
+++ b/src/config.c
@@ -21,6 +21,21 @@ gmnisrv_config_get_host(struct gmnisrv_config *conf, const char *hostname)
return NULL;
}
+struct gmnisrv_route *
+gmnisrv_host_get_route(struct gmnisrv_host *host,
+ enum gmnisrv_routing routing, const char *spec)
+{
+ struct gmnisrv_route *route = host->routes;
+ while (route) {
+ if (route->routing == routing
+ && strcmp(route->spec, spec) == 0) {
+ return route;
+ }
+ route = route->next;
+ }
+ return NULL;
+}
+
static int
parse_listen(struct gmnisrv_config *conf, const char *value)
{
@@ -133,7 +148,29 @@ conf_ini_handler(void *user, const char *section,
return 0;
}
- struct gmnisrv_host *host = gmnisrv_config_get_host(conf, section);
+ const char *spec;
+ char hostname[1024 + 1];
+ enum gmnisrv_routing routing;
+ size_t hostln = strcspn(section, ":~");
+ switch (section[hostln]) {
+ case '\0':
+ routing = ROUTE_PATH;
+ spec = "/";
+ break;
+ case ':':
+ routing = ROUTE_PATH;
+ spec = &section[hostln + 1];
+ break;
+ case '~':
+ routing = ROUTE_REGEX;
+ spec = &section[hostln + 1];
+ break;
+ }
+ assert(hostln < sizeof(hostname));
+ strncpy(hostname, section, hostln);
+ hostname[hostln] = '\0';
+
+ struct gmnisrv_host *host = gmnisrv_config_get_host(conf, hostname);
if (!host) {
host = calloc(1, sizeof(struct gmnisrv_host));
assert(host);
@@ -142,33 +179,52 @@ conf_ini_handler(void *user, const char *section,
conf->hosts = host;
}
+ struct gmnisrv_route *route =
+ gmnisrv_host_get_route(host, routing, spec);
+ if (!route) {
+ route = calloc(1, sizeof(struct gmnisrv_route));
+ assert(route);
+ route->spec = strdup(spec);
+ route->routing = routing;
+ route->next = host->routes;
+ host->routes = route;
+
+ switch (route->routing) {
+ case ROUTE_PATH:
+ route->path = strdup(spec);
+ break;
+ case ROUTE_REGEX:
+ assert(0); // TODO
+ }
+ }
+
struct {
char *name;
char **value;
- } host_strvars[] = {
- { "root", &host->root },
- { "index", &host->index },
+ } route_strvars[] = {
+ { "root", &route->root },
+ { "index", &route->index },
};
struct {
char *name;
bool *value;
- } host_bvars[] = {
- { "autoindex", &host->autoindex },
+ } route_bvars[] = {
+ { "autoindex", &route->autoindex },
};
- for (size_t i = 0; i < sizeof(host_strvars) / sizeof(host_strvars[0]); ++i) {
- if (strcmp(host_strvars[i].name, name) != 0) {
+ for (size_t i = 0; i < sizeof(route_strvars) / sizeof(route_strvars[0]); ++i) {
+ if (strcmp(route_strvars[i].name, name) != 0) {
continue;
}
- *host_strvars[i].value = strdup(value);
+ *route_strvars[i].value = strdup(value);
return 1;
}
- for (size_t i = 0; i < sizeof(host_bvars) / sizeof(host_bvars[0]); ++i) {
- if (strcmp(host_bvars[i].name, name) != 0) {
+ for (size_t i = 0; i < sizeof(route_bvars) / sizeof(route_bvars[0]); ++i) {
+ if (strcmp(route_bvars[i].name, name) != 0) {
continue;
}
- *host_bvars[i].value =
+ *route_bvars[i].value =
strcasecmp(value, "yes") == 0 ||
strcasecmp(value, "true") == 0 ||
strcasecmp(value, "on") == 0;
@@ -233,8 +289,15 @@ config_finish(struct gmnisrv_config *conf)
while (host) {
struct gmnisrv_host *next = host->next;
free(host->hostname);
- free(host->root);
- free(host->index);
+
+ struct gmnisrv_route *route = host->routes;
+ while (route) {
+ struct gmnisrv_route *rnext = route->next;
+ free(route->root);
+ free(route->index);
+ free(route);
+ route = rnext;
+ }
free(host);
host = next;
}
diff --git a/src/serve.c b/src/serve.c
index b798e7b..18c2993 100644
--- a/src/serve.c
+++ b/src/serve.c
@@ -117,15 +117,60 @@ internal_error:
goto exit;
}
+static bool
+route_match(struct gmnisrv_route *route, const char *path, const char **revised)
+{
+ switch (route->routing) {
+ case ROUTE_PATH:;
+ size_t l = strlen(route->path);
+ if (strncmp(path, route->path, l) != 0) {
+ return false;
+ }
+ if (route->path[l-1] != '/' && path[l] != '\0' && path[l] != '/') {
+ // Prevents path == "/foobar" from matching
+ // route == "/foo":
+ return false;
+ }
+ if (route->path[l-1] == '/') {
+ *revised = &path[l-1];
+ } else {
+ *revised = &path[l];
+ }
+ return true;
+ case ROUTE_REGEX:
+ assert(0); // TODO
+ }
+
+ assert(0); // Invariant
+}
+
void
serve_request(struct gmnisrv_client *client)
{
struct gmnisrv_host *host = client->host;
assert(host);
- assert(host->root); // TODO: reverse proxy support
+ struct gmnisrv_route *route = host->routes;
+ assert(route);
+
+ const char *url_path;
+ while (route) {
+ if (route_match(route, client->path, &url_path)) {
+ break;
+ }
+
+ route = route->next;
+ }
+
+ if (!route) {
+ client_submit_response(client,
+ GEMINI_STATUS_NOT_FOUND, "Not found", NULL);
+ return;
+ }
+
+ assert(route->root); // TODO: reverse proxy support
char path[PATH_MAX + 1];
- int n = snprintf(path, sizeof(path), "%s%s", host->root, client->path);
+ int n = snprintf(path, sizeof(path), "%s%s", route->root, url_path);
if ((size_t)n >= sizeof(path)) {
client_submit_response(client, GEMINI_STATUS_PERMANENT_FAILURE,
"Request path exceeds PATH_MAX", NULL);
@@ -142,12 +187,12 @@ serve_request(struct gmnisrv_client *client)
}
if (S_ISDIR(st.st_mode)) {
- if (host->autoindex) {
+ if (route->autoindex) {
serve_autoindex(client, path);
return;
} else {
strncat(path,
- host->index ? host->index : "index.gmi",
+ route->index ? route->index : "index.gmi",
sizeof(path) - 1);
}
} else if (S_ISLNK(st.st_mode)) {