@reldens/server-utils 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,22 +1,313 @@
1
1
  # Reldens - Server Utils
2
2
 
3
- A set of helpers for Node.js to create a server and handle files.
4
-
3
+ A Node.js server toolkit providing secure application server creation, file handling, encryption, and file upload capabilities for production-ready applications.
5
4
 
6
5
  [![Reldens - GitHub - Release](https://www.dwdeveloper.com/media/reldens/reldens-mmorpg-platform.png)](https://github.com/damian-pastorini/reldens)
7
6
 
8
- # Reldens - Server Utils
7
+ ## Features
9
8
 
10
- ### Features
9
+ ### AppServerFactory
10
+ - Complete Express.js server configuration with security defaults
11
+ - HTTPS/HTTP server creation with SSL certificate management
12
+ - SNI (Server Name Indication) support for multi-domain hosting
13
+ - Virtual host management with domain mapping
14
+ - Development mode detection with appropriate configurations
15
+ - CORS configuration with flexible origin management
16
+ - Rate limiting with customizable thresholds
17
+ - Security headers and XSS protection
18
+ - Helmet integration for enhanced security
19
+ - Protocol enforcement (HTTP to HTTPS redirection)
20
+ - Trusted proxy configuration
21
+ - Request parsing with size limits and validation
22
+ - Static file serving with security headers
23
+ - Input validation utilities
11
24
 
12
- - File handler.
13
- - Express server factory.
14
- - Multer uploader factory.
15
- - Password manager.
25
+ ### FileHandler
26
+ - Secure file system operations with path validation
27
+ - File and folder creation, copying, and removal
28
+ - JSON file parsing and validation
29
+ - File type detection based on magic numbers
30
+ - Secure filename generation
31
+ - Path sanitization and traversal protection
32
+ - File permissions checking
33
+ - Folder content listing and filtering
34
+ - Temporary file creation
35
+ - File quarantine functionality for security threats
36
+ - Binary file head reading for type detection
37
+ - Comprehensive error handling with detailed context
16
38
 
17
- Need something specific?
39
+ ### Encryptor
40
+ - Password hashing using PBKDF2 with configurable iterations
41
+ - Password validation against stored hashes
42
+ - AES-256-GCM data encryption and decryption
43
+ - Secure token generation with customizable length
44
+ - TOTP (Time-based One-Time Password) generation
45
+ - Data hashing with multiple algorithms (SHA-256, SHA-512, MD5)
46
+ - HMAC generation and verification
47
+ - Constant-time string comparison for security
48
+ - Cryptographically secure random value generation
18
49
 
19
- [Request a feature here: https://www.reldens.com/features-request](https://www.reldens.com/features-request)
50
+ ### UploaderFactory
51
+ - Multer-based file upload handling with security validation
52
+ - Multiple file upload support with field mapping
53
+ - File type validation using MIME types and extensions
54
+ - Filename security validation and sanitization
55
+ - File size limits and upload count restrictions
56
+ - Secure filename generation option
57
+ - File content validation based on magic numbers
58
+ - Dangerous file extension filtering
59
+ - Automatic file cleanup on validation failure
60
+ - Custom error response handling
61
+ - Upload destination mapping per field
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ npm install @reldens/server-utils
67
+ ```
68
+
69
+ ## Quick Start
70
+
71
+ ### Basic Server Setup
72
+
73
+ ```javascript
74
+ const { AppServerFactory } = require('@reldens/server-utils');
75
+
76
+ let appServerFactory = new AppServerFactory();
77
+ let serverResult = appServerFactory.createAppServer({
78
+ port: 3000,
79
+ useHttps: false,
80
+ autoListen: true
81
+ });
82
+
83
+ if(serverResult){
84
+ let { app, appServer } = serverResult;
85
+ console.log('Server running on port 3000');
86
+ }
87
+ ```
88
+
89
+ ### File Operations
90
+
91
+ ```javascript
92
+ const { FileHandler } = require('@reldens/server-utils');
93
+
94
+ // Read a JSON configuration file
95
+ let config = FileHandler.fetchFileJson('/path/to/config.json');
96
+ if(config){
97
+ console.log('Configuration loaded:', config);
98
+ }
99
+
100
+ // Create a folder securely
101
+ if(FileHandler.createFolder('/path/to/new/folder')){
102
+ console.log('Folder created successfully');
103
+ }
104
+
105
+ // Generate a secure filename
106
+ let secureFilename = FileHandler.generateSecureFilename('user-upload.jpg');
107
+ console.log('Secure filename:', secureFilename);
108
+ ```
109
+
110
+ ### Password Encryption
111
+
112
+ ```javascript
113
+ const { Encryptor } = require('@reldens/server-utils');
114
+
115
+ // Hash a password
116
+ let hashedPassword = Encryptor.encryptPassword('userPassword123');
117
+ if(hashedPassword){
118
+ console.log('Password hashed:', hashedPassword);
119
+ }
120
+
121
+ // Validate password
122
+ let isValid = Encryptor.validatePassword('userPassword123', hashedPassword);
123
+ console.log('Password valid:', isValid);
124
+
125
+ // Generate secure token
126
+ let secureToken = Encryptor.generateSecureToken(32);
127
+ console.log('Secure token:', secureToken);
128
+ ```
129
+
130
+ ### File Upload Configuration
131
+
132
+ ```javascript
133
+ const { UploaderFactory } = require('@reldens/server-utils');
134
+
135
+ let uploaderFactory = new UploaderFactory({
136
+ maxFileSize: 10 * 1024 * 1024, // 10MB
137
+ mimeTypes: {
138
+ image: ['image/jpeg', 'image/png', 'image/gif'],
139
+ document: ['application/pdf', 'text/plain']
140
+ },
141
+ allowedExtensions: {
142
+ image: ['.jpg', '.jpeg', '.png', '.gif'],
143
+ document: ['.pdf', '.txt']
144
+ },
145
+ applySecureFileNames: true
146
+ });
147
+
148
+ let uploader = uploaderFactory.createUploader(
149
+ [{ name: 'avatar' }, { name: 'document' }],
150
+ { avatar: '/uploads/avatars', document: '/uploads/docs' },
151
+ { avatar: 'image', document: 'document' }
152
+ );
153
+
154
+ // Use with Express
155
+ app.post('/upload', uploader, (req, res) => {
156
+ console.log('Files uploaded:', req.files);
157
+ res.json({ success: true });
158
+ });
159
+ ```
160
+
161
+ ## Advanced Configuration
162
+
163
+ ### HTTPS Server with Multiple Domains
164
+
165
+ ```javascript
166
+ let appServerFactory = new AppServerFactory();
167
+
168
+ appServerFactory.addDomain({
169
+ hostname: 'example.com',
170
+ keyPath: '/ssl/example.com.key',
171
+ certPath: '/ssl/example.com.crt',
172
+ aliases: ['www.example.com']
173
+ });
174
+
175
+ appServerFactory.addDomain({
176
+ hostname: 'api.example.com',
177
+ keyPath: '/ssl/api.example.com.key',
178
+ certPath: '/ssl/api.example.com.crt'
179
+ });
180
+
181
+ let serverResult = appServerFactory.createAppServer({
182
+ useHttps: true,
183
+ useVirtualHosts: true,
184
+ keyPath: '/ssl/default.key',
185
+ certPath: '/ssl/default.crt',
186
+ port: 443
187
+ });
188
+ ```
189
+
190
+ ### Development Mode Configuration
191
+
192
+ ```javascript
193
+ let appServerFactory = new AppServerFactory();
194
+
195
+ // Add development domains
196
+ appServerFactory.addDevelopmentDomain('localhost');
197
+ appServerFactory.addDevelopmentDomain('dev.myapp.local');
198
+
199
+ let serverResult = appServerFactory.createAppServer({
200
+ port: 3000,
201
+ corsOrigin: ['http://localhost:3000', 'http://dev.myapp.local:3000'],
202
+ developmentMultiplier: 5, // More lenient rate limiting in dev
203
+ });
204
+ ```
205
+
206
+ ### Custom Security Configuration
207
+
208
+ ```javascript
209
+ let appServerFactory = new AppServerFactory();
210
+
211
+ let serverResult = appServerFactory.createAppServer({
212
+ useHelmet: true,
213
+ helmetConfig: {
214
+ contentSecurityPolicy: {
215
+ directives: {
216
+ defaultSrc: ["'self'"],
217
+ styleSrc: ["'self'", "'unsafe-inline'"],
218
+ scriptSrc: ["'self'"]
219
+ }
220
+ }
221
+ },
222
+ globalRateLimit: 100, // requests per window
223
+ windowMs: 60000, // 1 minute
224
+ maxRequests: 30,
225
+ trustedProxy: '127.0.0.1'
226
+ });
227
+ ```
228
+
229
+ ## API Reference
230
+
231
+ ### AppServerFactory Methods
232
+
233
+ - `createAppServer(config)` - Creates and configures Express server
234
+ - `addDomain(domainConfig)` - Adds domain configuration for virtual hosting
235
+ - `addDevelopmentDomain(domain)` - Adds development domain pattern
236
+ - `setDomainMapping(mapping)` - Sets domain to configuration mapping
237
+ - `enableServeHome(app, callback)` - Enables homepage serving
238
+ - `serveStatics(app, staticPath)` - Serves static files
239
+ - `serveStaticsPath(app, route, staticPath)` - Serves static files on specific route
240
+ - `validateInput(input, type)` - Validates input against predefined patterns
241
+ - `enableCSP(cspOptions)` - Enables Content Security Policy
242
+ - `listen(port)` - Starts server listening
243
+ - `close()` - Gracefully closes server
244
+
245
+ ### FileHandler Methods
246
+
247
+ - `exists(path)` - Checks if file or folder exists
248
+ - `createFolder(path)` - Creates folder with recursive option
249
+ - `remove(path)` - Removes file or folder recursively
250
+ - `copyFile(source, destination)` - Copies file to destination
251
+ - `copyFolderSync(source, destination)` - Copies folder recursively
252
+ - `readFile(path)` - Reads file contents as string
253
+ - `writeFile(path, content)` - Writes content to file
254
+ - `fetchFileJson(path)` - Reads and parses JSON file
255
+ - `fetchFileContents(path)` - Reads file with validation
256
+ - `updateFileContents(path, content)` - Updates existing file
257
+ - `isFile(path)` - Checks if path is file
258
+ - `isFolder(path)` - Checks if path is folder
259
+ - `getFilesInFolder(path, extensions)` - Lists files with optional filtering
260
+ - `validateFileType(path, type, allowedTypes, maxSize)` - Validates file type and size
261
+ - `detectFileType(path)` - Detects MIME type from file signature
262
+ - `generateSecureFilename(originalName)` - Generates cryptographically secure filename
263
+ - `quarantineFile(path, reason)` - Moves file to quarantine folder
264
+ - `createTempFile(prefix, extension)` - Creates temporary file path
265
+
266
+ ### Encryptor Methods
267
+
268
+ - `encryptPassword(password)` - Hashes password with salt
269
+ - `validatePassword(password, hash)` - Validates password against hash
270
+ - `generateSecretKey()` - Generates 256-bit secret key
271
+ - `encryptData(data, key)` - Encrypts data with AES-256-GCM
272
+ - `decryptData(encryptedData, key)` - Decrypts AES-256-GCM data
273
+ - `generateSecureToken(length)` - Generates base64url token
274
+ - `generateTOTP(secret, timeStep)` - Generates time-based OTP
275
+ - `hashData(data, algorithm)` - Hashes data with specified algorithm
276
+ - `generateHMAC(data, secret, algorithm)` - Generates HMAC signature
277
+ - `verifyHMAC(data, secret, signature, algorithm)` - Verifies HMAC signature
278
+ - `constantTimeCompare(a, b)` - Performs constant-time string comparison
279
+
280
+ ### UploaderFactory Methods
281
+
282
+ - `createUploader(fields, buckets, allowedTypes)` - Creates multer upload middleware
283
+ - `validateFilenameSecurity(filename)` - Validates filename for security
284
+ - `validateFile(file, allowedType, callback)` - Validates file during upload
285
+ - `validateFileContents(file, allowedType)` - Validates file content after upload
286
+ - `cleanupFiles(files)` - Removes uploaded files on error
287
+
288
+ ## Security Features
289
+
290
+ ### Path Traversal Protection
291
+ All file operations include comprehensive path validation to prevent directory traversal attacks and access to system files.
292
+
293
+ ### Secure File Upload
294
+ File uploads are validated at multiple levels including filename, MIME type, file extension, file size, and content validation using magic number detection.
295
+
296
+ ### Rate Limiting
297
+ Configurable rate limiting with development mode detection for appropriate thresholds in different environments.
298
+
299
+ ### HTTPS Support
300
+ Full SSL/TLS support with SNI for multi-domain hosting and automatic certificate management.
301
+
302
+ ### Input Validation
303
+ Built-in validators for common input types including email, username, strong passwords, alphanumeric strings, and IP addresses.
304
+
305
+ ### Cryptographic Security
306
+ Industry-standard encryption using PBKDF2 for passwords, AES-256-GCM for data encryption, and secure random generation for tokens.
307
+
308
+ ## Error Handling
309
+
310
+ All methods include comprehensive error handling with detailed error objects containing context information. Errors are logged appropriately and never expose sensitive system information.
20
311
 
21
312
  ---
22
313
 
@@ -24,6 +315,9 @@ Need something specific?
24
315
 
25
316
  [https://www.reldens.com/documentation/utils/](https://www.reldens.com/documentation/utils/)
26
317
 
318
+ Need something specific?
319
+
320
+ [Request a feature here: https://www.reldens.com/features-request](https://www.reldens.com/features-request)
27
321
 
28
322
  ---
29
323
 
@@ -0,0 +1,80 @@
1
+ /**
2
+ *
3
+ * Reldens - CorsConfigurer
4
+ *
5
+ */
6
+
7
+ const cors = require('cors');
8
+
9
+ class CorsConfigurer
10
+ {
11
+
12
+ constructor()
13
+ {
14
+ this.isDevelopmentMode = false;
15
+ this.useCors = true;
16
+ this.corsOrigin = '*';
17
+ this.corsMethods = ['GET','POST'];
18
+ this.corsHeaders = ['Content-Type','Authorization'];
19
+ this.developmentCorsOrigins = [];
20
+ this.developmentPorts = [3000, 8080, 8081];
21
+ }
22
+
23
+ setup(app, config)
24
+ {
25
+ this.isDevelopmentMode = config.isDevelopmentMode || false;
26
+ this.useCors = config.useCors !== false;
27
+ this.corsOrigin = config.corsOrigin || this.corsOrigin;
28
+ this.corsMethods = config.corsMethods || this.corsMethods;
29
+ this.corsHeaders = config.corsHeaders || this.corsHeaders;
30
+ this.developmentPorts = config.developmentPorts || this.developmentPorts;
31
+ if(!this.useCors){
32
+ return;
33
+ }
34
+ if(this.isDevelopmentMode && config.domainMapping){
35
+ this.developmentCorsOrigins = this.extractDevelopmentOrigins(config.domainMapping);
36
+ }
37
+ let corsOptions = {
38
+ origin: this.corsOrigin,
39
+ methods: this.corsMethods,
40
+ allowedHeaders: this.corsHeaders,
41
+ credentials: true
42
+ };
43
+ if(this.isDevelopmentMode && 0 < this.developmentCorsOrigins.length){
44
+ corsOptions.origin = (origin, callback) => {
45
+ if(!origin){
46
+ return callback(null, true);
47
+ }
48
+ if(-1 !== this.developmentCorsOrigins.indexOf(origin)){
49
+ return callback(null, true);
50
+ }
51
+ if('*' === this.corsOrigin){
52
+ return callback(null, true);
53
+ }
54
+ return callback(null, false);
55
+ };
56
+ }
57
+ app.use(cors(corsOptions));
58
+ }
59
+
60
+ extractDevelopmentOrigins(domainMapping)
61
+ {
62
+ let origins = [];
63
+ let mappingKeys = Object.keys(domainMapping);
64
+ for(let domain of mappingKeys){
65
+ origins.push('http://'+domain);
66
+ origins.push('https://'+domain);
67
+ if(domain.includes(':')){
68
+ continue;
69
+ }
70
+ for(let port of this.developmentPorts){
71
+ origins.push('http://'+domain+':'+port);
72
+ origins.push('https://'+domain+':'+port);
73
+ }
74
+ }
75
+ return origins;
76
+ }
77
+
78
+ }
79
+
80
+ module.exports.CorsConfigurer = CorsConfigurer;
@@ -0,0 +1,75 @@
1
+ /**
2
+ *
3
+ * Reldens - DevelopmentModeDetector
4
+ *
5
+ */
6
+
7
+ class DevelopmentModeDetector
8
+ {
9
+
10
+ constructor()
11
+ {
12
+ this.developmentPatterns = [
13
+ 'localhost',
14
+ '127.0.0.1',
15
+ // domain ends:
16
+ '.local',
17
+ '.test',
18
+ '.dev',
19
+ '.acc',
20
+ '.staging',
21
+ // sub-domains:
22
+ 'local.',
23
+ 'test.',
24
+ 'dev.',
25
+ 'acc.',
26
+ 'staging.'
27
+ ];
28
+ this.developmentEnvironments = ['development', 'dev', 'test'];
29
+ }
30
+
31
+ detect(config = {})
32
+ {
33
+ if(config.developmentPatterns){
34
+ this.developmentPatterns = config.developmentPatterns;
35
+ }
36
+ if(config.developmentEnvironments){
37
+ this.developmentEnvironments = config.developmentEnvironments;
38
+ }
39
+ let env = process.env.NODE_ENV;
40
+ if(this.developmentEnvironments.includes(env)){
41
+ return true;
42
+ }
43
+ if(config.developmentDomains && 0 < config.developmentDomains.length){
44
+ for(let domain of config.developmentDomains){
45
+ if(this.matchesPattern(domain)){
46
+ return true;
47
+ }
48
+ }
49
+ }
50
+ if(config.domains && 0 < config.domains.length){
51
+ for(let domainConfig of config.domains){
52
+ if(!domainConfig.hostname){
53
+ continue;
54
+ }
55
+ if(this.matchesPattern(domainConfig.hostname)){
56
+ return true;
57
+ }
58
+ }
59
+ }
60
+ return false;
61
+ }
62
+
63
+ matchesPattern(domain)
64
+ {
65
+ for(let pattern of this.developmentPatterns){
66
+ if(domain.includes(pattern)){
67
+ return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ }
74
+
75
+ module.exports.DevelopmentModeDetector = DevelopmentModeDetector;
@@ -0,0 +1,48 @@
1
+ /**
2
+ *
3
+ * Reldens - ProtocolEnforcer
4
+ *
5
+ */
6
+
7
+ class ProtocolEnforcer
8
+ {
9
+
10
+ constructor()
11
+ {
12
+ this.isDevelopmentMode = false;
13
+ this.useHttps = false;
14
+ this.enforceProtocol = true;
15
+ }
16
+
17
+ setup(app, config)
18
+ {
19
+ this.isDevelopmentMode = config.isDevelopmentMode || false;
20
+ this.useHttps = config.useHttps || false;
21
+ this.enforceProtocol = config.enforceProtocol !== false;
22
+ app.use((req, res, next) => {
23
+ let protocol = req.get('X-Forwarded-Proto') || req.protocol;
24
+ let host = req.get('host');
25
+ if(this.isDevelopmentMode){
26
+ res.removeHeader('Origin-Agent-Cluster');
27
+ res.removeHeader('Strict-Transport-Security');
28
+ res.removeHeader('upgrade-insecure-requests');
29
+ res.set('Origin-Agent-Cluster', '?0');
30
+ if(this.enforceProtocol && host){
31
+ if(!this.useHttps && 'https' === protocol){
32
+ let redirectUrl = 'http://'+host+req.url;
33
+ return res.redirect(301, redirectUrl);
34
+ }
35
+ if(this.useHttps && 'http' === protocol){
36
+ let redirectUrl = 'https://'+host+req.url;
37
+ return res.redirect(301, redirectUrl);
38
+ }
39
+ }
40
+ }
41
+ res.set('X-Forwarded-Proto', protocol);
42
+ next();
43
+ });
44
+ }
45
+
46
+ }
47
+
48
+ module.exports.ProtocolEnforcer = ProtocolEnforcer;
@@ -0,0 +1,75 @@
1
+ /**
2
+ *
3
+ * Reldens - RateLimitConfigurer
4
+ *
5
+ */
6
+
7
+ const rateLimit = require('express-rate-limit');
8
+
9
+ class RateLimitConfigurer
10
+ {
11
+
12
+ constructor()
13
+ {
14
+ this.isDevelopmentMode = false;
15
+ this.globalRateLimit = 0;
16
+ this.windowMs = 60000;
17
+ this.maxRequests = 30;
18
+ this.developmentMultiplier = 10;
19
+ this.applyKeyGenerator = false;
20
+ this.tooManyRequestsMessage = 'Too many requests, please try again later.';
21
+ this.rateLimit = rateLimit;
22
+ }
23
+
24
+ setup(app, config)
25
+ {
26
+ this.isDevelopmentMode = config.isDevelopmentMode || false;
27
+ this.globalRateLimit = config.globalRateLimit || 0;
28
+ this.windowMs = config.windowMs || this.windowMs;
29
+ this.maxRequests = config.maxRequests || this.maxRequests;
30
+ this.developmentMultiplier = config.developmentMultiplier || this.developmentMultiplier;
31
+ this.applyKeyGenerator = config.applyKeyGenerator || false;
32
+ this.tooManyRequestsMessage = config.tooManyRequestsMessage || this.tooManyRequestsMessage;
33
+ if(!this.globalRateLimit){
34
+ return;
35
+ }
36
+ let limiterParams = {
37
+ windowMs: this.windowMs,
38
+ max: this.maxRequests,
39
+ standardHeaders: true,
40
+ legacyHeaders: false,
41
+ message: this.tooManyRequestsMessage
42
+ };
43
+ if(this.isDevelopmentMode){
44
+ limiterParams.max = this.maxRequests * this.developmentMultiplier;
45
+ }
46
+ if(this.applyKeyGenerator){
47
+ limiterParams.keyGenerator = function(req){
48
+ return req.ip;
49
+ };
50
+ }
51
+ app.use(this.rateLimit(limiterParams));
52
+ }
53
+
54
+ createHomeLimiter()
55
+ {
56
+ let limiterParams = {
57
+ windowMs: this.windowMs,
58
+ max: this.maxRequests,
59
+ standardHeaders: true,
60
+ legacyHeaders: false
61
+ };
62
+ if(this.isDevelopmentMode){
63
+ limiterParams.max = this.maxRequests * this.developmentMultiplier;
64
+ }
65
+ if(this.applyKeyGenerator){
66
+ limiterParams.keyGenerator = function(req){
67
+ return req.ip;
68
+ };
69
+ }
70
+ return this.rateLimit(limiterParams);
71
+ }
72
+
73
+ }
74
+
75
+ module.exports.RateLimitConfigurer = RateLimitConfigurer;
@@ -0,0 +1,157 @@
1
+ /**
2
+ *
3
+ * Reldens - SecurityConfigurer
4
+ *
5
+ */
6
+
7
+ const helmet = require('helmet');
8
+ const sanitizeHtml = require('sanitize-html');
9
+
10
+ class SecurityConfigurer
11
+ {
12
+
13
+ constructor()
14
+ {
15
+ this.isDevelopmentMode = false;
16
+ this.useHelmet = true;
17
+ this.useXssProtection = true;
18
+ this.helmetConfig = false;
19
+ this.sanitizeOptions = {allowedTags: [], allowedAttributes: {}};
20
+ }
21
+
22
+ setupHelmet(app, config)
23
+ {
24
+ this.isDevelopmentMode = config.isDevelopmentMode || false;
25
+ this.useHelmet = config.useHelmet !== false;
26
+ this.helmetConfig = config.helmetConfig || false;
27
+ if(!this.useHelmet){
28
+ return;
29
+ }
30
+ let helmetOptions = {
31
+ crossOriginEmbedderPolicy: false,
32
+ crossOriginOpenerPolicy: false,
33
+ crossOriginResourcePolicy: false,
34
+ originAgentCluster: false
35
+ };
36
+ if(this.isDevelopmentMode){
37
+ helmetOptions.contentSecurityPolicy = false;
38
+ helmetOptions.hsts = false;
39
+ helmetOptions.noSniff = false;
40
+ } else {
41
+ helmetOptions.contentSecurityPolicy = {
42
+ directives: {
43
+ defaultSrc: ["'self'"],
44
+ scriptSrc: ["'self'"],
45
+ scriptSrcElem: ["'self'"],
46
+ styleSrc: ["'self'", "'unsafe-inline'"],
47
+ styleSrcElem: ["'self'", "'unsafe-inline'"],
48
+ imgSrc: ["'self'", "data:", "https:"],
49
+ fontSrc: ["'self'"],
50
+ connectSrc: ["'self'"],
51
+ frameAncestors: ["'none'"],
52
+ baseUri: ["'self'"],
53
+ formAction: ["'self'"]
54
+ }
55
+ };
56
+ if(config.developmentExternalDomains){
57
+ this.addExternalDomainsToCsp(helmetOptions.contentSecurityPolicy.directives, config.developmentExternalDomains);
58
+ }
59
+ }
60
+ if(this.helmetConfig){
61
+ Object.assign(helmetOptions, this.helmetConfig);
62
+ }
63
+ app.use(helmet(helmetOptions));
64
+ }
65
+
66
+ addExternalDomainsToCsp(directives, externalDomains)
67
+ {
68
+ let keys = Object.keys(externalDomains);
69
+ for(let directiveKey of keys){
70
+ let domains = externalDomains[directiveKey];
71
+ if(!Array.isArray(domains)){
72
+ continue;
73
+ }
74
+ for(let domain of domains){
75
+ if(directives[directiveKey]){
76
+ directives[directiveKey].push(domain);
77
+ }
78
+ let elemKey = directiveKey.replace('-src', '-src-elem');
79
+ if(directives[elemKey]){
80
+ directives[elemKey].push(domain);
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ setupXssProtection(app, config)
87
+ {
88
+ this.useXssProtection = config.useXssProtection !== false;
89
+ this.sanitizeOptions = config.sanitizeOptions || this.sanitizeOptions;
90
+ if(!this.useXssProtection){
91
+ return;
92
+ }
93
+ app.use((req, res, next) => {
94
+ if(!req.body){
95
+ return next();
96
+ }
97
+ if('object' === typeof req.body){
98
+ this.sanitizeRequestBody(req.body);
99
+ }
100
+ next();
101
+ });
102
+ }
103
+
104
+ sanitizeRequestBody(body)
105
+ {
106
+ let bodyKeys = Object.keys(body);
107
+ for(let i = 0; i < bodyKeys.length; i++){
108
+ let key = bodyKeys[i];
109
+ if('string' === typeof body[key]){
110
+ body[key] = sanitizeHtml(body[key], this.sanitizeOptions);
111
+ continue;
112
+ }
113
+ if('object' === typeof body[key] && null !== body[key]){
114
+ this.sanitizeRequestBody(body[key]);
115
+ }
116
+ }
117
+ }
118
+
119
+ enableCSP(app, cspOptions)
120
+ {
121
+ let defaults = {
122
+ 'default-src': ["'self'"],
123
+ 'script-src': ["'self'"],
124
+ 'style-src': ["'self'", "'unsafe-inline'"],
125
+ 'img-src': ["'self'", "data:", "https:"],
126
+ 'font-src': ["'self'"],
127
+ 'connect-src': ["'self'"],
128
+ 'frame-ancestors': ["'none'"],
129
+ 'base-uri': ["'self'"],
130
+ 'form-action': ["'self'"]
131
+ };
132
+ if(this.isDevelopmentMode){
133
+ defaults['script-src'].push("'unsafe-eval'");
134
+ defaults['connect-src'].push("ws:");
135
+ defaults['connect-src'].push("wss:");
136
+ }
137
+ let csp = Object.assign({}, defaults, cspOptions);
138
+ let policyString = '';
139
+ let keys = Object.keys(csp);
140
+ for(let i = 0; i < keys.length; i++){
141
+ let directive = keys[i];
142
+ let sources = csp[directive];
143
+ if(0 < i){
144
+ policyString += '; ';
145
+ }
146
+ policyString += directive + ' ' + sources.join(' ');
147
+ }
148
+ app.use((req, res, next) => {
149
+ res.setHeader('Content-Security-Policy', policyString);
150
+ next();
151
+ });
152
+ return true;
153
+ }
154
+
155
+ }
156
+
157
+ module.exports.SecurityConfigurer = SecurityConfigurer;
@@ -5,15 +5,16 @@
5
5
  */
6
6
 
7
7
  const { FileHandler } = require('./file-handler');
8
+ const { DevelopmentModeDetector } = require('./app-server-factory/development-mode-detector');
9
+ const { ProtocolEnforcer } = require('./app-server-factory/protocol-enforcer');
10
+ const { SecurityConfigurer } = require('./app-server-factory/security-configurer');
11
+ const { CorsConfigurer } = require('./app-server-factory/cors-configurer');
12
+ const { RateLimitConfigurer } = require('./app-server-factory/rate-limit-configurer');
8
13
  const http = require('http');
9
14
  const https = require('https');
10
15
  const express = require('express');
11
16
  const bodyParser = require('body-parser');
12
17
  const session = require('express-session');
13
- const rateLimit = require('express-rate-limit');
14
- const cors = require('cors');
15
- const helmet = require('helmet');
16
- const sanitizeHtml = require('sanitize-html');
17
18
 
18
19
  class AppServerFactory
19
20
  {
@@ -25,7 +26,6 @@ class AppServerFactory
25
26
  this.session = session;
26
27
  this.appServer = false;
27
28
  this.app = express();
28
- this.rateLimit = rateLimit;
29
29
  this.useCors = true;
30
30
  this.useExpressJson = true;
31
31
  this.useUrlencoded = true;
@@ -68,6 +68,26 @@ class AppServerFactory
68
68
  res.set('X-Frame-Options', 'DENY');
69
69
  }
70
70
  };
71
+ this.isDevelopmentMode = false;
72
+ this.developmentDomains = [];
73
+ this.domainMapping = {};
74
+ this.enforceProtocol = true;
75
+ this.developmentPatterns = [
76
+ 'localhost',
77
+ '127.0.0.1',
78
+ '.local',
79
+ '.test',
80
+ '.dev',
81
+ '.staging'
82
+ ];
83
+ this.developmentEnvironments = ['development', 'dev', 'test'];
84
+ this.developmentPorts = [3000, 8080, 8081];
85
+ this.developmentMultiplier = 10;
86
+ this.developmentModeDetector = new DevelopmentModeDetector();
87
+ this.protocolEnforcer = new ProtocolEnforcer();
88
+ this.securityConfigurer = new SecurityConfigurer();
89
+ this.corsConfigurer = new CorsConfigurer();
90
+ this.rateLimitConfigurer = new RateLimitConfigurer();
71
91
  }
72
92
 
73
93
  createAppServer(appServerConfig)
@@ -75,45 +95,104 @@ class AppServerFactory
75
95
  if(appServerConfig){
76
96
  Object.assign(this, appServerConfig);
77
97
  }
78
- if(this.useHelmet){
79
- this.app.use(this.helmetConfig ? helmet(this.helmetConfig) : helmet());
80
- }
81
- if(this.useVirtualHosts){
82
- this.setupVirtualHosts();
98
+ this.detectDevelopmentMode();
99
+ this.setupDevelopmentConfiguration();
100
+ this.setupProtocolEnforcement();
101
+ this.setupSecurity();
102
+ this.setupVirtualHosts();
103
+ this.setupCors();
104
+ this.setupRateLimiting();
105
+ this.setupRequestParsing();
106
+ this.setupTrustedProxy();
107
+ this.appServer = this.createServer();
108
+ if(!this.appServer){
109
+ this.error = {message: 'Failed to create app server'};
110
+ return false;
83
111
  }
84
- if(this.useCors){
85
- let corsOptions = {
86
- origin: this.corsOrigin,
87
- methods: this.corsMethods,
88
- allowedHeaders: this.corsHeaders
89
- };
90
- this.app.use(cors(corsOptions));
112
+ if(this.autoListen){
113
+ this.listen();
91
114
  }
92
- if(this.globalRateLimit){
93
- let limiterParams = {
94
- windowMs: this.windowMs,
95
- max: this.maxRequests,
96
- standardHeaders: true,
97
- legacyHeaders: false,
98
- message: this.tooManyRequestsMessage
99
- };
100
- if(this.applyKeyGenerator){
101
- limiterParams.keyGenerator = function(req){
102
- return req.ip;
103
- };
104
- }
105
- this.app.use(this.rateLimit(limiterParams));
115
+ return {app: this.app, appServer: this.appServer};
116
+ }
117
+
118
+ detectDevelopmentMode()
119
+ {
120
+ this.isDevelopmentMode = this.developmentModeDetector.detect({
121
+ developmentPatterns: this.developmentPatterns,
122
+ developmentEnvironments: this.developmentEnvironments,
123
+ developmentDomains: this.developmentDomains,
124
+ domains: this.domains
125
+ });
126
+ }
127
+
128
+ setupDevelopmentConfiguration()
129
+ {
130
+ if(!this.isDevelopmentMode){
131
+ return;
106
132
  }
107
- if(this.useXssProtection){
108
- this.app.use((req, res, next) => {
109
- if(!req.body){
110
- return next();
111
- }
112
- if('object' === typeof req.body){
113
- this.sanitizeRequestBody(req.body);
114
- }
115
- next();
116
- });
133
+ this.staticOptions.setHeaders = (res, path) => {
134
+ res.set('X-Content-Type-Options', 'nosniff');
135
+ res.set('X-Frame-Options', 'SAMEORIGIN');
136
+ res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
137
+ res.set('Pragma', 'no-cache');
138
+ res.set('Expires', '0');
139
+ };
140
+ }
141
+
142
+ setupProtocolEnforcement()
143
+ {
144
+ this.protocolEnforcer.setup(this.app, {
145
+ isDevelopmentMode: this.isDevelopmentMode,
146
+ useHttps: this.useHttps,
147
+ enforceProtocol: this.enforceProtocol
148
+ });
149
+ }
150
+
151
+ setupSecurity()
152
+ {
153
+ this.securityConfigurer.setupHelmet(this.app, {
154
+ isDevelopmentMode: this.isDevelopmentMode,
155
+ useHelmet: this.useHelmet,
156
+ helmetConfig: this.helmetConfig,
157
+ developmentExternalDomains: this.developmentExternalDomains
158
+ });
159
+ this.securityConfigurer.setupXssProtection(this.app, {
160
+ useXssProtection: this.useXssProtection,
161
+ sanitizeOptions: this.sanitizeOptions
162
+ });
163
+ }
164
+
165
+ setupCors()
166
+ {
167
+ this.corsConfigurer.setup(this.app, {
168
+ isDevelopmentMode: this.isDevelopmentMode,
169
+ useCors: this.useCors,
170
+ corsOrigin: this.corsOrigin,
171
+ corsMethods: this.corsMethods,
172
+ corsHeaders: this.corsHeaders,
173
+ domainMapping: this.domainMapping,
174
+ developmentPorts: this.developmentPorts
175
+ });
176
+ }
177
+
178
+ setupRateLimiting()
179
+ {
180
+ this.rateLimitConfigurer.setup(this.app, {
181
+ isDevelopmentMode: this.isDevelopmentMode,
182
+ globalRateLimit: this.globalRateLimit,
183
+ windowMs: this.windowMs,
184
+ maxRequests: this.maxRequests,
185
+ developmentMultiplier: this.developmentMultiplier,
186
+ applyKeyGenerator: this.applyKeyGenerator,
187
+ tooManyRequestsMessage: this.tooManyRequestsMessage
188
+ });
189
+ }
190
+
191
+ setupRequestParsing()
192
+ {
193
+ if(this.maxRequestSize){
194
+ this.jsonLimit = this.maxRequestSize;
195
+ this.urlencodedLimit = this.maxRequestSize;
117
196
  }
118
197
  if(this.useExpressJson){
119
198
  this.app.use(this.applicationFramework.json({
@@ -127,32 +206,12 @@ class AppServerFactory
127
206
  limit: this.urlencodedLimit
128
207
  }));
129
208
  }
130
- if('' !== this.trustedProxy){
131
- this.app.enable('trust proxy', this.trustedProxy);
132
- }
133
- this.appServer = this.createServer();
134
- if(!this.appServer){
135
- this.error = {message: 'Failed to create app server'};
136
- return false;
137
- }
138
- if(this.autoListen){
139
- this.listen();
140
- }
141
- return {app: this.app, appServer: this.appServer};
142
209
  }
143
210
 
144
- sanitizeRequestBody(body)
211
+ setupTrustedProxy()
145
212
  {
146
- let bodyKeys = Object.keys(body);
147
- for(let i = 0; i < bodyKeys.length; i++){
148
- let key = bodyKeys[i];
149
- if('string' === typeof body[key]){
150
- body[key] = sanitizeHtml(body[key], this.sanitizeOptions);
151
- continue;
152
- }
153
- if('object' === typeof body[key] && null !== body[key]){
154
- this.sanitizeRequestBody(body[key]);
155
- }
213
+ if('' !== this.trustedProxy){
214
+ this.app.enable('trust proxy', this.trustedProxy);
156
215
  }
157
216
  }
158
217
 
@@ -172,7 +231,7 @@ class AppServerFactory
172
231
 
173
232
  setupVirtualHosts()
174
233
  {
175
- if(0 === this.domains.length){
234
+ if(!this.useVirtualHosts || 0 === this.domains.length){
176
235
  return;
177
236
  }
178
237
  this.app.use((req, res, next) => {
@@ -306,18 +365,7 @@ class AppServerFactory
306
365
 
307
366
  async enableServeHome(app, homePageLoadCallback)
308
367
  {
309
- let limiterParams = {
310
- windowMs: this.windowMs,
311
- max: this.maxRequests,
312
- standardHeaders: true,
313
- legacyHeaders: false
314
- };
315
- if(this.applyKeyGenerator){
316
- limiterParams.keyGenerator = function(req){
317
- return req.ip;
318
- };
319
- }
320
- let limiter = this.rateLimit(limiterParams);
368
+ let limiter = this.rateLimitConfigurer.createHomeLimiter();
321
369
  app.post('/', limiter);
322
370
  app.post('/', async (req, res, next) => {
323
371
  if('/' === req._parsedUrl.pathname){
@@ -376,6 +424,24 @@ class AppServerFactory
376
424
  return true;
377
425
  }
378
426
 
427
+ addDevelopmentDomain(domain)
428
+ {
429
+ if(!domain || 'string' !== typeof domain){
430
+ return false;
431
+ }
432
+ this.developmentDomains.push(domain);
433
+ return true;
434
+ }
435
+
436
+ setDomainMapping(mapping)
437
+ {
438
+ if(!mapping || 'object' !== typeof mapping){
439
+ return false;
440
+ }
441
+ this.domainMapping = mapping;
442
+ return true;
443
+ }
444
+
379
445
  async close()
380
446
  {
381
447
  if(!this.appServer){
@@ -386,33 +452,7 @@ class AppServerFactory
386
452
 
387
453
  enableCSP(cspOptions)
388
454
  {
389
- let defaults = {
390
- 'default-src': ["'self'"],
391
- 'script-src': ["'self'"],
392
- 'style-src': ["'self'", "'unsafe-inline'"],
393
- 'img-src': ["'self'", "data:", "https:"],
394
- 'font-src': ["'self'"],
395
- 'connect-src': ["'self'"],
396
- 'frame-ancestors': ["'none'"],
397
- 'base-uri': ["'self'"],
398
- 'form-action': ["'self'"]
399
- };
400
- let csp = Object.assign({}, defaults, cspOptions);
401
- let policyString = '';
402
- let keys = Object.keys(csp);
403
- for(let i = 0; i < keys.length; i++){
404
- let directive = keys[i];
405
- let sources = csp[directive];
406
- if(0 < i){
407
- policyString += '; ';
408
- }
409
- policyString += directive + ' ' + sources.join(' ');
410
- }
411
- this.app.use((req, res, next) => {
412
- res.setHeader('Content-Security-Policy', policyString);
413
- next();
414
- });
415
- return true;
455
+ return this.securityConfigurer.enableCSP(this.app, cspOptions);
416
456
  }
417
457
 
418
458
  validateInput(input, type)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/server-utils",
3
3
  "scope": "@reldens",
4
- "version": "0.18.0",
4
+ "version": "0.20.0",
5
5
  "description": "Reldens - Server Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -38,10 +38,10 @@
38
38
  "body-parser": "2.2.0",
39
39
  "cors": "2.8.5",
40
40
  "express": "4.21.2",
41
- "express-rate-limit": "7.5.0",
41
+ "express-rate-limit": "7.5.1",
42
42
  "express-session": "1.18.1",
43
43
  "helmet": "8.1.0",
44
- "multer": "2.0.0",
45
- "sanitize-html": "^2.17.0"
44
+ "multer": "2.0.1",
45
+ "sanitize-html": "2.17.0"
46
46
  }
47
47
  }