Hello, this is my first time contributing to openbsd and this mailing
list, so please excuse any newbie blunders. I recently switched my
personal website to use httpd with statically generated HTML files, but
was unhappy to find that my HTML files could only be accessed by exact
filename, including the ".html" at the end. My site previously ran on
Apache with rewrite rules to ensure that "example.com/foo" would serve
the file "/foo.html" (when "/foo" didn't exist). I wanted to keep my
original URLs working and I aesthetically prefer URLs without ".html"
suffixes, so I looked around for different options with httpd. The best
option I could find was to create symbolic links from "/foo" to
"/foo.html" and set the default media type to text/html, but this
solution was cumbersome (I had to maintain all the symbolic links) and
had undesirable side effects (all extensionless files were treated as
text/html instead of text/plain).

I decided instead to look into modifying httpd to support implicit file
extensions. My basic idea was to add a configurable setting called
`implicit extension`. When a file is not found and the file doesn't
already have the implicit extension, then httpd will look for the same
filename but with the implicit extension added at the end, and reroute
to that file if it exists. This neatly solves my problem by adding
`implicit extension ".html"` to the top of my /etc/httpd.conf file, and
`implicit extension ".php"` to the section of my website that uses PHP
scripts. For simplicity (and because my website doesn't use both HTML
and PHP in the same subdomain), I opted to only support a single
implicit extension per config block, though in the future, supporting
globs as implicit extensions might also be nice(e.g. `implicit extension
".{html,php}"`).

I've been running the code on my personal website for a while now
without any issues. If this patch is accepted, I also have a separate
small patch that displays custom HTTP error pages if you have a
/<error code>[<implicit extension>] file (e.g. /404 or /404.html), which
works well in conjunction with this patch.

I'm happy to take any feedback or criticism on both the idea and my
implementation patch below. Hopefully other people find this useful as
well!

- Bruce Hill


Index: config.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/config.c,v
retrieving revision 1.61
diff -u -p -u -p -r1.61 config.c
--- config.c    21 Sep 2020 09:42:07 -0000      1.61
+++ config.c    26 Feb 2021 23:30:18 -0000
@@ -568,6 +568,13 @@ config_getserver_config(struct httpd *en
                            &parent->default_type, sizeof(struct media_type));
                }
+ f = SRVFLAG_IMPLICIT_EXT;
+               if ((srv_conf->flags & f) == 0) {
+                       srv_conf->flags |= parent->flags & f;
+                       memcpy(&srv_conf->implicit_extension,
+                           &parent->implicit_extension, 
sizeof(parent->implicit_extension));
+               }
+
                f = SRVFLAG_PATH_REWRITE|SRVFLAG_NO_PATH_REWRITE;
                if ((srv_conf->flags & f) == 0) {
                        srv_conf->flags |= parent->flags & f;
Index: httpd.h
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/httpd.h,v
retrieving revision 1.153
diff -u -p -u -p -r1.153 httpd.h
--- httpd.h     29 Oct 2020 12:30:52 -0000      1.153
+++ httpd.h     26 Feb 2021 23:30:19 -0000
@@ -57,6 +57,7 @@
 #define HTTPD_REALM_MAX                255
 #define HTTPD_LOCATION_MAX     255
 #define HTTPD_DEFAULT_TYPE     { "bin", "application", "octet-stream", NULL }
+#define HTTPD_IMPLICIT_EXT     ""
 #define HTTPD_LOGVIS           VIS_NL|VIS_TAB|VIS_CSTYLE
 #define HTTPD_TLS_CERT         "/etc/ssl/server.crt"
 #define HTTPD_TLS_KEY          "/etc/ssl/private/server.key"
@@ -391,6 +392,7 @@ SPLAY_HEAD(client_tree, client);
 #define SRVFLAG_DEFAULT_TYPE   0x00800000
 #define SRVFLAG_PATH_REWRITE   0x01000000
 #define SRVFLAG_NO_PATH_REWRITE        0x02000000
+#define SRVFLAG_IMPLICIT_EXT   0x04000000
 #define SRVFLAG_LOCATION_FOUND 0x40000000
 #define SRVFLAG_LOCATION_NOT_FOUND 0x80000000
@@ -399,8 +401,8 @@ SPLAY_HEAD(client_tree, client);
        "\05ROOT\06LOCATION\07FCGI\10NO_FCGI\11LOG\12NO_LOG"          \
        "\14SYSLOG\15NO_SYSLOG\16TLS\17ACCESS_LOG\20ERROR_LOG"                \
        "\21AUTH\22NO_AUTH\23BLOCK\24NO_BLOCK\25LOCATION_MATCH"               \
-       "\26SERVER_MATCH\27SERVER_HSTS\30DEFAULT_TYPE\31PATH\32NO_PATH" \
-       "\37LOCATION_FOUND\40LOCATION_NOT_FOUND"
+       "\26SERVER_MATCH\27SERVER_HSTS\30DEFAULT_TYPE\31PATH\32NO_PATH"       \
+       "\33IMPLICIT_EXTENSION\37LOCATION_FOUND\40LOCATION_NOT_FOUND"
#define TCPFLAG_NODELAY 0x01
 #define TCPFLAG_NNODELAY       0x02
@@ -481,6 +483,7 @@ struct server_config {
        char                     accesslog[PATH_MAX];
        char                     errorlog[PATH_MAX];
        struct media_type        default_type;
+       char                     implicit_extension[MEDIATYPE_NAMEMAX];
struct sockaddr_storage fastcgi_ss; @@ -594,6 +597,7 @@ struct httpd {
        struct serverlist       *sc_servers;
        struct mediatypes       *sc_mediatypes;
        struct media_type        sc_default_type;
+       char                    *sc_implicit_extension;
        struct serverauth       *sc_auth;
struct privsep *sc_ps;
Index: parse.y
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/parse.y,v
retrieving revision 1.120
diff -u -p -u -p -r1.120 parse.y
--- parse.y     29 Oct 2020 12:30:52 -0000      1.120
+++ parse.y     26 Feb 2021 23:30:19 -0000
@@ -138,7 +138,8 @@ typedef struct {
 %}
%token ACCESS ALIAS AUTO BACKLOG BODY BUFFER CERTIFICATE CHROOT CIPHERS COMMON
-%token COMBINED CONNECTION DHE DIRECTORY ECDHE ERR FCGI INDEX IP KEY LIFETIME
+%token COMBINED CONNECTION DHE DIRECTORY ECDHE ERR EXTENSION FCGI IMPLICIT
+%token INDEX IP KEY LIFETIME
 %token LISTEN LOCATION LOG LOGDIR MATCH MAXIMUM NO NODELAY OCSP ON PORT PREFORK
 %token PROTOCOLS REQUESTS ROOT SACK SERVER SOCKET STRIP STYLE SYSLOG TCP TICKET
 %token TIMEOUT TLS TYPE TYPES HSTS MAXAGE SUBDOMAINS DEFAULT PRELOAD REQUEST
@@ -222,6 +223,9 @@ main                : PREFORK NUMBER        {
                        memcpy(&conf->sc_default_type, &media,
                            sizeof(struct media_type));
                }
+               | IMPLICIT EXTENSION STRING     {
+                       conf->sc_implicit_extension = $3;
+               }
                ;
server : SERVER optmatch STRING {
@@ -636,6 +640,16 @@ serveroptsl        : LISTEN ON STRING opttls po
                        memcpy(&srv_conf->default_type, &media,
                            sizeof(struct media_type));
                }
+               | IMPLICIT EXTENSION STRING     {
+                       if (strlcpy(srv_conf->implicit_extension, $3,
+                           sizeof(srv_conf->implicit_extension)) >=
+                           sizeof(srv_conf->implicit_extension)) {
+                               yyerror("implicit extension truncated");
+                               free($3);
+                               YYERROR;
+                       }
+                       srv_conf->flags |= SRVFLAG_IMPLICIT_EXT;
+               }
                | include
                | hsts                          {
                        if (parentsrv != NULL) {
@@ -1397,10 +1411,12 @@ lookup(char *s)
                { "drop",             DROP },
                { "ecdhe",            ECDHE },
                { "error",            ERR },
+               { "extension",                EXTENSION },
                { "fastcgi",          FCGI },
                { "forwarded",                FORWARDED },
                { "found",            FOUND },
                { "hsts",             HSTS },
+               { "implicit",         IMPLICIT },
                { "include",          INCLUDE },
                { "index",            INDEX },
                { "ip",                       IP },
@@ -1807,6 +1823,8 @@ parse_config(const char *filename, struc
/* Set default media type */
        memcpy(&conf->sc_default_type, &dflt, sizeof(struct media_type));
+
+       conf->sc_implicit_extension = HTTPD_IMPLICIT_EXT;
errors = 0; Index: server_file.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/server_file.c,v
retrieving revision 1.68
diff -u -p -u -p -r1.68 server_file.c
--- server_file.c       22 May 2020 07:18:17 -0000      1.68
+++ server_file.c       26 Feb 2021 23:30:19 -0000
@@ -59,13 +59,43 @@ server_file_access(struct httpd *env, st
        struct server_config    *srv_conf = clt->clt_srv_conf;
        struct stat              st;
        struct kv               *r, key;
-       char                    *newpath, *encodedpath;
+       char                    *newpath, *encodedpath, *implicit_extension;
        int                      ret;
errno = 0; if (access(path, R_OK) == -1) {
-               goto fail;
+               /* If the file does exist, use whatever failure happened. */
+               if (errno != ENOENT)
+                       goto fail;
+
+               /* Try to use the implicit extension, if set. */
+               implicit_extension = srv_conf->flags & SRVFLAG_IMPLICIT_EXT ?
+                   srv_conf->implicit_extension : env->sc_implicit_extension;
+               if (!implicit_extension || !implicit_extension[0])
+                       goto fail;
+
+               /* Abort if the file already has the implicit extension. */
+               if (strlen(path) >= strlen(implicit_extension) &&
+                   strcmp(&path[strlen(path)-strlen(implicit_extension)],
+                              implicit_extension) == 0)
+                       goto fail;
+
+               if (strlcat(path, implicit_extension, len) >= len)
+                       return (500);
+
+               if (asprintf(&newpath, "%s%s", desc->http_path,
+                   implicit_extension) == -1)
+                       return (500);
+
+               desc->http_path_alias = newpath;
+
+               if (server_getlocation(clt, newpath) != srv_conf) {
+                       /* Appending the extension has changed the location. */
+                       return (server_file(env, clt));
+               }
+
+               return (server_file_access(env, clt, path, len));
        } else if (stat(path, &st) == -1) {
                goto fail;
        } else if (S_ISDIR(st.st_mode)) {

Reply via email to