1
0
mirror of https://git.sr.ht/~sircmpwn/gmnisrv synced 2024-06-08 17:30:43 +00:00

Initial implementation of a routing table

This commit is contained in:
Drew DeVault 2020-10-25 21:46:01 -04:00
parent 1fe107875b
commit 8baeb5a51c
4 changed files with 183 additions and 25 deletions

View File

@ -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

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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)) {