1 /**
2  * Модуль реализации Middleware CORS
3  *
4  * Copyright: (c) 2015-2020, Milofon Project.
5  * License: Subject to the terms of the BSD 3-Clause License, as written in the included LICENSE.md file.
6  * Author: <m.galanin@milofon.pro> Maksim Galanin
7  * Date: 2020-04-30
8  */
9 
10 module dango.web.middlewares.cors;
11 
12 private
13 {
14     import std.algorithm.iteration : splitter, filter;
15     import std.algorithm.searching : canFind;
16     import std.uni : toLower, toUpper;
17     import std.array : array, join;
18     import std.conv : to;
19 
20     import dango.inject : DependencyContainer;
21     import dango.system.exception : enforceConfig;
22     import dango.system.properties;
23     import dango.web.middleware;
24 }
25 
26 
27 /// Проверка
28 alias AllowChecker = bool delegate(string val) @safe;
29 
30 
31 /**
32  * Middleware позволяет реализовать CORS доступ
33  */
34 class CORSWebMiddleware : WebMiddleware
35 {
36     private
37     {
38         AllowChecker _originChecker;
39         AllowChecker _methodChecker;
40         AllowChecker _headerChecker;
41         ulong _maxAge;
42     }
43 
44     /**
45      * Main constructor
46      */
47     this(AllowChecker oCk, AllowChecker mCk, AllowChecker hCk, long maxAge) @safe
48     {
49         this._originChecker = oCk;
50         this._methodChecker = mCk;
51         this._headerChecker = hCk;
52         this._maxAge = maxAge;
53     }
54 
55     /**
56      * Обработка запроса
57      */
58     void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res,
59             HTTPServerRequestDelegate next) @safe
60     {
61         auto origin = "Origin" in req.headers;
62 
63         if (origin && _originChecker(*origin))
64         {
65             res.headers["Access-Control-Allow-Origin"] = *origin;
66             res.headers["Access-Control-Allow-Credentials"] = "true";
67         }
68 
69         next(req, res);
70     }
71 
72     /**
73      * Регистрация цепочек маршрутов middleware
74      */
75     void registerHandlers(HTTPMethod method, string path, RegisterHandlerCallback dg) @safe
76     {
77         dg(HTTPMethod.OPTIONS, path, &handleOptionsRequest);
78     }
79 
80 
81 private:
82 
83 
84     void handleOptionsRequest(scope HTTPServerRequest req, scope HTTPServerResponse res) @safe
85     {
86         auto origin = "Origin" in req.headers;
87 
88         if (origin && _originChecker(*origin))
89         {
90             res.headers["Access-Control-Allow-Origin"] = *origin;
91             res.headers["Access-Control-Allow-Credentials"] = "true";
92 
93             res.headers["Access-Control-Max-Age"] = _maxAge.to!string;
94 
95             auto method = "Access-Control-Request-Method" in req.headers;
96             if (method && _methodChecker(*method))
97                 res.headers["Access-Control-Allow-Methods"] = *method;
98 
99             if (auto headers = "Access-Control-Request-Headers" in req.headers)
100             {
101                 auto hds = splitter(*headers, ",").filter!(h=> _headerChecker(h));
102                 res.headers["Access-Control-Allow-Headers"] = hds.join(",");
103             }
104         }
105 
106         res.writeBody("");
107     }
108 }
109 
110 
111 /**
112  * Фабрика Middleware CORS
113  */
114 class CORSWebMiddlewareFactory : WebMiddlewareFactory
115 {
116     WebMiddleware createComponent(DependencyContainer cont, UniConf config) @safe
117     {
118         AllowChecker oCk = createOriginChecker(config.toSequence("origin"));
119         AllowChecker mCk = createMethodChecker(config.toSequence("method"));
120         AllowChecker hCk = createHeaderChecker(config.toSequence("header"));
121         long maxAge = config.getOrElse!long("maxAge", 300);
122         return new CORSWebMiddleware(oCk, mCk, hCk, maxAge);
123     }
124 
125 
126 private:
127 
128 
129     AllowChecker createOriginChecker(UniConf[] origins) @safe
130     {
131         import std.regex;
132 
133         enum replaceRx = ctRegex!(`\\\*`);
134         Regex!char[] regs;
135 
136         foreach (UniConf origin; origins)
137         {
138             auto origStr = origin.opt!string();
139             enforceConfig(!origStr.isNull, "Origin must be a string");
140 
141             string escapeOrig = origStr.get.escaper.array.to!string;
142             string repOrig = escapeOrig.replaceAll(replaceRx, "(.*?)");
143             string regexOrig = "^" ~ repOrig ~ "$";
144 
145             regs ~= regex(regexOrig);
146         }
147 
148         return (string val)
149         {
150             foreach (re; regs)
151             {
152                 if (!match(val, re).empty)
153                     return true;
154             }
155             return false;
156         };
157     }
158 
159 
160     AllowChecker createMethodChecker(UniConf[] methodProps) @safe
161     {
162         string[] methods;
163 
164         foreach (UniConf mp; methodProps)
165         {
166             auto m = mp.opt!string();
167             enforceConfig(!m.isNull, "Method must be a string");
168             methods ~= m.get.toUpper;
169         }
170 
171         return (string val)
172         {
173             return methods.canFind(val.toUpper);
174         };
175     }
176 
177 
178     AllowChecker createHeaderChecker(UniConf[] headerProps) @safe
179     {
180         string[] headers;
181 
182         foreach (UniConf hp; headerProps)
183         {
184             auto h = hp.opt!string();
185             enforceConfig(!h.isNull, "Header must be a string");
186             headers ~= h.get.toLower;
187         }
188 
189         return (string val)
190         {
191             return headers.canFind(val.toLower);
192         };
193     }
194 }
195