1 module vibeauth.router.resources.routes;
2 
3 import std.socket;
4 import std.net.curl;
5 import std.file;
6 import std.path;
7 import std.digest.md;
8 import std.algorithm;
9 import std.algorithm.mutation : copy;
10 
11 import core.time;
12 
13 import vibe.core.log;
14 import vibe.core.file;
15 import vibe.http.server;
16 import vibe.stream.operations;
17 import vibeauth.configuration;
18 
19 version(unittest) {
20   import vibe.http.router;
21   import fluentasserts.vibe.request;
22   import fluentasserts.vibe.json;
23   import fluent.asserts;
24 }
25 
26 enum bootstrapStyleUrl = "https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css";
27 enum localBootstrapStyle = "tmp/assets/bootstrap.min.css";
28 
29 enum bootstrapJsUrl = "https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.bundle.min.js";
30 enum localBootstrapJs = "tmp/assets/bootstrap.min.js";
31 
32 enum jqueryUrl = "https://code.jquery.com/jquery-3.3.1.slim.min.js";
33 enum localJquery = "tmp/assets/jquery.min.js";
34 
35 shared static this() {
36   if(!localBootstrapStyle.exists) {
37     mkdirRecurse(localBootstrapStyle.dirName);
38     download(bootstrapStyleUrl, localBootstrapStyle);
39   }
40 
41 
42   if(!localBootstrapJs.exists) {
43     mkdirRecurse(localBootstrapJs.dirName);
44     download(bootstrapJsUrl, localBootstrapJs);
45   }
46 
47   if(!localJquery.exists) {
48     mkdirRecurse(localJquery.dirName);
49     download(jqueryUrl, localJquery);
50   }
51 
52   version(unittest) {
53     std.file.write("tmp/test.css", "data");
54     std.file.write("tmp/test.js", "data");
55   }
56 }
57 
58 /// Static resource
59 struct Resource(string path) {
60   static immutable {
61     string etag;
62     string data;
63     string mime;
64   }
65 
66   ///
67   static this() {
68     auto ctx = makeDigest!MD5();
69 
70     data = readText(path);
71     copy(data, &ctx); //Note: You must pass a pointer to copy!
72     etag = ctx.finish().toHexString.idup;
73 
74     if(path.endsWith(".css")) {
75       mime = "text/css";
76     }
77 
78     if(path.endsWith(".js")) {
79       mime = "text/javascript";
80     }
81   }
82 
83   ///
84   void handler(HTTPServerRequest req, HTTPServerResponse res) {
85     res.headers["Cache-Control"] = "max-age=86400";
86     res.headers["ETag"] = etag;
87 
88     if("If-None-Match" in req.headers && req.headers["If-None-Match"] == etag) {
89       res.statusCode = 304;
90       res.writeVoidBody;
91       return;
92     }
93 
94     res.statusCode = 200;
95     res.writeBody(data, mime);
96   }
97 }
98 
99 /// It should store the the valid properties
100 unittest {
101   Resource!"tmp/test.css" resource;
102   resource.data.should.equal("data");
103   resource.mime.should.equal("text/css");
104   resource.etag.should.equal("8D777F385D3DFEC8815D20F7496026DC");
105 
106   Resource!"tmp/test.js" resourceJs;
107   resourceJs.mime.should.equal("text/javascript");
108 }
109 
110 /// It should get a resource with the right headers
111 unittest {
112   Resource!"tmp/test.css" resource;
113 
114   auto router = new URLRouter();
115   router.get("/resource", &resource.handler);
116 
117   router
118     .request
119     .get("/resource")
120     .expectHeader("ETag", resource.etag)
121     .expectHeader("Content-Type", "text/css")
122     .expectStatusCode(200)
123     .end((Response response) => {
124       response.bodyString.should.equal("data");
125     });
126 }
127 
128 /// It should not get a resource with the etag matches
129 unittest {
130   Resource!"tmp/test.css" resource;
131 
132   auto router = new URLRouter();
133   router.get("/resource", &resource.handler);
134 
135   router
136     .request
137     .get("/resource")
138     .header("If-None-Match", resource.etag)
139     .expectHeader("ETag", resource.etag)
140     .expectStatusCode(304)
141     .end((Response response) => {
142       response.bodyString.should.equal("");
143     });
144 }
145 
146 /// Handle resources served by vibe auth
147 class ResourceRoutes {
148 
149   private {
150     const {
151       ServiceConfiguration configuration;
152     }
153 
154     Resource!localBootstrapStyle _bootstrapStyle;
155     Resource!localBootstrapJs _bootstrapJs;
156     Resource!localJquery _jquery;
157   }
158 
159   ///
160   this(const ServiceConfiguration configuration) {
161     this.configuration = configuration;
162     _bootstrapStyle = Resource!localBootstrapStyle();
163     _bootstrapJs = Resource!localBootstrapJs();
164     _jquery = Resource!localJquery();
165   }
166 
167   /// Handle the requests
168   void handler(HTTPServerRequest req, HTTPServerResponse res) {
169     if(req.method == HTTPMethod.GET && req.path == configuration.paths.resources.bootstrapStyle) {
170       _bootstrapStyle.handler(req, res);
171     }
172 
173     if(req.method == HTTPMethod.GET && req.path == configuration.paths.resources.bootstrapJs) {
174       _bootstrapJs.handler(req, res);
175     }
176 
177     if(req.method == HTTPMethod.GET && req.path == configuration.paths.resources.jquery) {
178       _jquery.handler(req, res);
179     }
180   }
181 }
182