1 | "use strict"
|
2 |
|
3 | const path = require("path");
|
4 | const fs = require("fs");
|
5 |
|
6 | class Security{
|
7 | constructor(mscp){
|
8 | this.mscp = mscp
|
9 | this.cachedIPResult = {}
|
10 | this.cachedAccessKeyResult = {}
|
11 | this.accessKeyPromptHTMLPage = fs.readFileSync(path.join(__dirname, '/www/accesskeyprompt.html'))
|
12 | }
|
13 |
|
14 | async init(){
|
15 | this.setup = this.mscp.setupHandler.setup
|
16 | }
|
17 |
|
18 | async onRequest(req, res, next){
|
19 | let ip = '';
|
20 | if(this.setup.trustProxy === true)
|
21 | ip = req.ip
|
22 | else if(this.setup.useForwardedHeader === true)
|
23 | ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
24 | else if(this.setup.useRealIPHeader === true)
|
25 | ip = req.headers['x-real-ip'] || req.connection.remoteAddress;
|
26 | else
|
27 | ip = req.connection.remoteAddress;
|
28 |
|
29 | let data = this.mscp.server.extend({}, req.body||{}, req.query||{});
|
30 |
|
31 | let area = "static"
|
32 | if(req.path.startsWith("/mscp")){
|
33 | area = "manage"
|
34 | } else if(req.path.startsWith("/api")){
|
35 | area = "api"
|
36 | }
|
37 |
|
38 | let accessKey = data.accessKey
|
39 |
|
40 | let accessKeyCookieName = area == "api" ? "mscpAccessKeyAPI"
|
41 | : area == "manage" ? "mscpAccessKeyManage"
|
42 | : "mscpAccessKeyStatic";
|
43 | if(accessKey === undefined){
|
44 | accessKey = req.cookies[accessKeyCookieName];
|
45 | }
|
46 |
|
47 | if(accessKey !== undefined){
|
48 | if(this.setup.accessKeyExpirationDays){
|
49 | let today = new Date();
|
50 | res.cookie(accessKeyCookieName, accessKey, {expires: new Date(today.getFullYear(),today.getMonth(),today.getDate()+this.setup.accessKeyExpirationDays), httpOnly: false });
|
51 | } else {
|
52 | res.cookie(accessKeyCookieName, accessKey, {expires: new Date(Date.now() + 1500000000), httpOnly: false });
|
53 | }
|
54 | }
|
55 |
|
56 | req.mscp = {ip: ip, accessKey: accessKey, area: area}
|
57 |
|
58 | if(req.path.startsWith("/mscp/js/")
|
59 | || req.path.startsWith("/mscp/libs/")
|
60 | || req.path.startsWith("/mscp/apibrowser")
|
61 | || req.path.startsWith("/mscpui/static/")
|
62 | || req.path == "/api/browse" || req.path == "/api/browse/"
|
63 | || req.path == "/api" || req.path == "/api/")
|
64 | {
|
65 | next();
|
66 | } else if(this.validate(req.path, data, area, ip, accessKey)){
|
67 | next()
|
68 | } else if(req.get("Accept") !== undefined && req.get("Accept").indexOf("text/html") >= 0){
|
69 | res.writeHead(200, "text/html");
|
70 | res.end(this.accessKeyPromptHTMLPage)
|
71 | console.log("Denied request for " + req.path + " from IP " + ip + (accessKey !== undefined ? " and access key \"" + accessKey + "\"" : ""))
|
72 | } else {
|
73 | res.writeHead(403);
|
74 | res.end("You do not have access to this content.")
|
75 | console.log("Denied request for " + req.path + " from IP " + ip + (accessKey !== undefined ? " and access key \"" + accessKey + "\"" : ""))
|
76 | }
|
77 | }
|
78 |
|
79 | validate(path, data, area, ip, accessKey){
|
80 | let scheme = area == "api" ? this.setup.api_access_scheme
|
81 | : area == "manage" ? this.setup.manage_access_scheme
|
82 | : area == "static" ? this.setup.static_access_scheme
|
83 | : "deny_all"
|
84 |
|
85 | switch(scheme){
|
86 | case "full_access":
|
87 | return true
|
88 | case "deny_all":
|
89 | return false
|
90 | case "localhost":
|
91 | return ip == "127.0.0.1" || ip == "::ffff:127.0.0.1" || ip == "::1"
|
92 | case "access_rule":
|
93 | return this.validateAccess(path, data, area, accessKey, ip)
|
94 | case undefined:
|
95 | return true
|
96 | }
|
97 |
|
98 | return false;
|
99 |
|
100 | }
|
101 |
|
102 | validateAccess(path, data, area, accessKey, ip){
|
103 | if(this.setup.accessRules === undefined)
|
104 | return false
|
105 |
|
106 | let matchesIP = false;
|
107 | let matchesKey = false;
|
108 | for(let f of this.setup.accessRules){
|
109 | if(f.area != area && f.area != "all")
|
110 | continue
|
111 |
|
112 | matchesIP = false;
|
113 | matchesKey = false;
|
114 |
|
115 | if(f.ip === undefined || f.ip == null || f.ip === ""){
|
116 | matchesIP = true;
|
117 | } else {
|
118 | try{
|
119 | if(new RegExp(f.ip).test(ip)){
|
120 | matchesIP = true;
|
121 | }
|
122 | } catch(err){
|
123 | console.log("Error validating IP " + ip + " against regexp \"" + f.ip + "\"");
|
124 | console.log(err)
|
125 | }
|
126 | }
|
127 |
|
128 | if(!matchesIP)
|
129 | continue;
|
130 |
|
131 | if(f.require_access_key !== true) {
|
132 | matchesKey = true;
|
133 | } else if(accessKey && f.accessKeys !== undefined && f.accessKeys.findIndex((ak) => ak.key == accessKey) >= 0){
|
134 | matchesKey = true;
|
135 | }
|
136 |
|
137 | if(matchesIP && matchesKey && this.validateSubRules(path, data, f)){
|
138 | return true;
|
139 | }
|
140 | }
|
141 | return false;
|
142 | }
|
143 |
|
144 | validateSubRules(path, data, accessRule){
|
145 | let subRules = accessRule.subRules || []
|
146 | for(let sr of subRules){
|
147 |
|
148 | if(sr.path.endsWith('*')){
|
149 | let p = sr.path.substring(0, sr.path.length - 1)
|
150 | if(!path.startsWith(p))
|
151 | continue;
|
152 | } else {
|
153 | if(path != sr.path && path != sr.path + "/")
|
154 | continue;
|
155 | }
|
156 |
|
157 | if(accessRule.default_permission == "allow" && sr.permission == "allow")
|
158 | continue;
|
159 |
|
160 | if(accessRule.default_permission == "deny" && sr.permission == "deny")
|
161 | continue;
|
162 |
|
163 | if(typeof sr.parameters === "string" && sr.parameters != ""){
|
164 | let parmsMatch = true;
|
165 | let vars = sr.parameters.split("\n")
|
166 | for(let v of vars){
|
167 | if(v == "")
|
168 | continue;
|
169 |
|
170 | let vsplits = v.split("=")
|
171 | if(vsplits.length == 1 && data[vsplits[0]] === undefined){
|
172 |
|
173 | parmsMatch = false;
|
174 | break;
|
175 | }
|
176 |
|
177 | let varName = vsplits[0]
|
178 | let varVal = vsplits[1]
|
179 |
|
180 | if(varVal == "null" && data[varName] !== null){
|
181 | parmsMatch = false;
|
182 | break;
|
183 | }
|
184 |
|
185 | if(varVal == "undefined" && data[varName] !== undefined){
|
186 | parmsMatch = false;
|
187 | break;
|
188 | }
|
189 |
|
190 | if(data[varName] === undefined){
|
191 | parmsMatch = false;
|
192 | break;
|
193 | }
|
194 |
|
195 | if(varVal.startsWith("$")){
|
196 | try{
|
197 | if(!new RegExp(varVal.substring(1)).test(data[varName])){
|
198 | parmsMatch = false;
|
199 | break;
|
200 | }
|
201 | } catch(err){
|
202 | parmsMatch = false;
|
203 | }
|
204 | } else {
|
205 | if(varVal != data[varName]){
|
206 | parmsMatch = false;
|
207 | break;
|
208 | }
|
209 | }
|
210 | }
|
211 |
|
212 | if(!parmsMatch){
|
213 | continue;
|
214 | }
|
215 | }
|
216 |
|
217 | return sr.permission == "allow" ? true : false;
|
218 | }
|
219 |
|
220 | return accessRule.default_permission == "allow" ? true : false;
|
221 | }
|
222 |
|
223 | }
|
224 |
|
225 | module.exports = Security
|