1 module handy_httpd.handlers.file_resolving_handler;
2 
3 import std.stdio;
4 
5 import handy_httpd.handler;
6 import handy_httpd.request;
7 import handy_httpd.response;
8 import handy_httpd.responses;
9 
10 /** 
11  * Request handler that resolves files within a given base path.
12  */
13 class FileResolvingHandler : HttpRequestHandler {
14     /** 
15      * The base path within which to resolve files.
16      */
17     private string basePath;
18 
19     /** 
20      * Associative array containing mime type mappings for file extensions.
21      */
22     private string[string] mimeTypes;
23 
24     /** 
25      * Constructs the request handler.
26      * Params:
27      *   basePath = The path to use to resolve files in.
28      */
29     this(string basePath = ".") {
30         this.basePath = basePath;
31         this.mimeTypes = [
32             ".html": "text/html",
33             ".js": "text/javascript",
34             ".css": "text/css",
35             ".json": "application/json",
36             ".png": "image/png",
37             ".jpg": "image/jpg",
38             ".gif": "image/gif",
39             ".webp": "image/webp",
40             ".wav": "audio/wav",
41             ".ogg": "audio/ogg",
42             ".mp3": "audio/mpeg",
43             ".mp4": "video/mp4",
44             ".woff": "application/font-woff",
45             ".ttf": "application/font-ttf",
46             ".eot": "application/vnd.ms-fontobject",
47             ".otf": "application/font-otf",
48             ".svg": "application/image/svg+xml",
49             ".wasm": "application/wasm",
50             ".pdf": "application/pdf",
51             ".txt": "text/plain",
52             ".xml": "application/xml"
53         ];
54     }
55 
56     void handle(ref HttpRequest request, ref HttpResponse response) {
57         if (request.server.isVerbose()) {
58             writefln!"Resolving file for url %s..."(request.url);
59         }
60         string path = sanitizeRequestPath(request.url);
61         if (path != null) {
62             response.fileResponse(path, getMimeType(path));
63         } else {
64             if (request.server.isVerbose()) {
65                 writefln!"Could not resolve file for url %s. Maybe it doesn't exist?"(request.url);
66             }
67             response.notFound();
68         }
69     }
70 
71     /** 
72      * Registers a new mime type for this handler.
73      * Params:
74      *   fileExtension = The file extension to use, including the '.' separator.
75      *   mimeType = The mime type that will be assigned to the given file extension.
76      * Returns: The handler, for method chaining.
77      */
78     public FileResolvingHandler registerMimeType(string fileExtension, string mimeType) {
79         mimeTypes[fileExtension] = mimeType;
80         return this;
81     }
82 
83     /** 
84      * Sanitizes a request url such that it points to a file within the
85      * configured base path for this handler.
86      * Params:
87      *   url = The url to sanitize.
88      * Returns: A string representing the file pointed to by the given url,
89      * or null if no valid file could be found.
90      */
91     private string sanitizeRequestPath(string url) {
92         import std.path : buildNormalizedPath;
93         import std.file : exists, isDir;
94         import std.regex;
95         if (url.length == 0 || url == "/") return this.basePath ~ "/index.html";
96         string normalized = this.basePath ~ "/" ~ buildNormalizedPath(url[1 .. $]);
97         
98         if (!exists(normalized)) return null;
99         // If the user has requested a directory, try and serve "index.html" from it.
100         if (isDir(normalized)) {
101             normalized ~= "/index.html";
102             if (!exists(normalized)) return null;
103         }
104         return normalized;
105     }
106 
107     /** 
108     * Tries to determine the mime type of a file. Defaults to "text/html" for
109     * files of an unknown type.
110     * Params:
111     *   filename = The name of the file to determine mime type for.
112     * Returns: A mime type string.
113     */
114     private string getMimeType(string filename) {
115         import std.string : lastIndexOf;
116         import std.uni : toLower;
117         auto p = filename.lastIndexOf('.');
118         if (p == -1) return "text/html";
119         string extension = filename[p..$].toLower();
120         if (extension !in this.mimeTypes) {
121             writefln!"Warning: Unknown mime type for file extension %s"(extension);
122             return "text/plain";
123         }
124         return this.mimeTypes[extension];
125     }
126 }