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 #include +#include #include 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 = §ion[hostln + 1]; + break; + case '~': + routing = ROUTE_REGEX; + spec = §ion[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)) {