woodland 22.0.3 → 22.0.5
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 +123 -37
- package/dist/cli.cjs +117 -105
- package/dist/woodland.cjs +402 -288
- package/dist/woodland.js +402 -288
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
Secure HTTP framework for Node.js. Express-compatible with built-in security, no performance tradeoff.
|
|
7
7
|
|
|
8
8
|
[](https://badge.fury.io/js/woodland)
|
|
9
|
-
[](https://nodejs.org/)
|
|
10
10
|
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
11
|
-
[](https://github.com/avoidwork/woodland)
|
|
11
|
+
[](https://github.com/avoidwork/woodland)
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
@@ -54,9 +54,10 @@ createServer(app.route).listen(3000, () => console.log("Server running at http:/
|
|
|
54
54
|
- **Built-in security** - CORS, path traversal, XSS prevention (no additional packages)
|
|
55
55
|
- **Secure by default** - CORS deny-all, path traversal blocked, HTML escaping automatic
|
|
56
56
|
- **TypeScript first** - Full type definitions included
|
|
57
|
-
- **No performance tradeoff** - Security features add
|
|
57
|
+
- **No performance tradeoff** - Security features add minimal overhead
|
|
58
58
|
- **Lightweight** - Minimal dependencies (6 packages)
|
|
59
59
|
- **Dual module support** - CommonJS and ESM
|
|
60
|
+
- **Production ready** - Event emitters for custom monitoring, examples for graceful shutdown
|
|
60
61
|
|
|
61
62
|
**Built-in Security Features:**
|
|
62
63
|
|
|
@@ -69,21 +70,53 @@ createServer(app.route).listen(3000, () => console.log("Server running at http:/
|
|
|
69
70
|
|
|
70
71
|
## Common Patterns
|
|
71
72
|
|
|
72
|
-
### REST API
|
|
73
|
+
### REST API with Error Handling
|
|
73
74
|
|
|
74
75
|
```javascript
|
|
75
76
|
const users = new Map();
|
|
76
77
|
|
|
77
|
-
app.get("/users", (req, res) =>
|
|
78
|
+
app.get("/users", (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const list = Array.from(users.values());
|
|
81
|
+
res.json(list);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
res.error(500, error.message);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
78
87
|
app.get("/users/:id", (req, res) => {
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
try {
|
|
89
|
+
const user = users.get(req.params.id);
|
|
90
|
+
if (!user) {
|
|
91
|
+
return res.error(404, "User not found");
|
|
92
|
+
}
|
|
93
|
+
res.json(user);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
res.error(500, error.message);
|
|
96
|
+
}
|
|
81
97
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
|
|
99
|
+
app.post("/users", async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const id = Date.now().toString();
|
|
102
|
+
const user = { ...req.body, id };
|
|
103
|
+
users.set(id, user);
|
|
104
|
+
res.status(201).json(user);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
res.error(400, error.message);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Health Check Endpoint
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
app.get("/health", (req, res) => {
|
|
115
|
+
res.json({
|
|
116
|
+
status: "healthy",
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
uptime: process.uptime(),
|
|
119
|
+
});
|
|
87
120
|
});
|
|
88
121
|
```
|
|
89
122
|
|
|
@@ -108,17 +141,17 @@ app.files("/static", "./public");
|
|
|
108
141
|
```javascript
|
|
109
142
|
// Global middleware
|
|
110
143
|
app.always((req, res, next) => {
|
|
111
|
-
|
|
112
|
-
|
|
144
|
+
req.startTime = Date.now();
|
|
145
|
+
next();
|
|
113
146
|
});
|
|
114
147
|
|
|
115
148
|
// Route-specific middleware
|
|
116
149
|
app.get("/protected", authenticate, handler);
|
|
117
150
|
|
|
118
|
-
// Error handler (register last
|
|
119
|
-
app.use(
|
|
120
|
-
|
|
121
|
-
|
|
151
|
+
// Error handler (register last with 4 params)
|
|
152
|
+
app.use((error, req, res, next) => {
|
|
153
|
+
console.error(error);
|
|
154
|
+
res.error(500, error.message);
|
|
122
155
|
});
|
|
123
156
|
```
|
|
124
157
|
|
|
@@ -126,18 +159,18 @@ app.use("/(.*)", (error, req, res, next) => {
|
|
|
126
159
|
|
|
127
160
|
```javascript
|
|
128
161
|
const app = woodland({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
162
|
+
origins: process.env.ALLOWED_ORIGINS?.split(",") || [],
|
|
163
|
+
autoIndex: process.env.AUTO_INDEX === "true",
|
|
164
|
+
etags: true,
|
|
165
|
+
cacheSize: 1000,
|
|
166
|
+
cacheTTL: 10000,
|
|
167
|
+
charset: "utf-8",
|
|
168
|
+
logging: {
|
|
169
|
+
enabled: true,
|
|
170
|
+
level: process.env.LOG_LEVEL || "info",
|
|
171
|
+
},
|
|
172
|
+
time: true,
|
|
173
|
+
silent: process.env.NODE_ENV === "production",
|
|
141
174
|
});
|
|
142
175
|
```
|
|
143
176
|
|
|
@@ -174,6 +207,36 @@ app.on("error", (req, res, err) => console.error(`Error ${res.statusCode}:`, err
|
|
|
174
207
|
app.on("stream", (req, res) => console.log(`Streaming file`));
|
|
175
208
|
```
|
|
176
209
|
|
|
210
|
+
## Graceful Shutdown
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
import { createServer } from "node:http";
|
|
214
|
+
import { woodland } from "woodland";
|
|
215
|
+
|
|
216
|
+
const app = woodland();
|
|
217
|
+
const server = createServer(app.route);
|
|
218
|
+
|
|
219
|
+
const gracefulShutdown = (signal) => {
|
|
220
|
+
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
221
|
+
|
|
222
|
+
server.close(() => {
|
|
223
|
+
console.log("Server closed");
|
|
224
|
+
process.exit(0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Force shutdown after 30s
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
console.error("Forced shutdown");
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}, 30000);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
235
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
236
|
+
|
|
237
|
+
server.listen(3000);
|
|
238
|
+
```
|
|
239
|
+
|
|
177
240
|
## CLI
|
|
178
241
|
|
|
179
242
|
```bash
|
|
@@ -190,7 +253,7 @@ npx woodland --ip=0.0.0.0
|
|
|
190
253
|
## Testing
|
|
191
254
|
|
|
192
255
|
```bash
|
|
193
|
-
npm test # Run tests (100% line coverage)
|
|
256
|
+
npm test # Run tests (334 tests, 100% line, 99.37% function, 95.90% branch coverage)
|
|
194
257
|
npm run coverage # Generate coverage report
|
|
195
258
|
npm run benchmark # Performance benchmarks
|
|
196
259
|
npm run lint # Check linting
|
|
@@ -198,14 +261,14 @@ npm run lint # Check linting
|
|
|
198
261
|
|
|
199
262
|
## Documentation
|
|
200
263
|
|
|
201
|
-
- [API Reference](
|
|
202
|
-
- [Technical Documentation](
|
|
203
|
-
- [Code Style Guide](
|
|
204
|
-
- [Benchmarks](
|
|
264
|
+
- [API Reference](docs/API.md) - Complete method documentation
|
|
265
|
+
- [Technical Documentation](docs/TECHNICAL_DOCUMENTATION.md) - Architecture, OWASP security, internals
|
|
266
|
+
- [Code Style Guide](docs/CODE_STYLE_GUIDE.md) - Conventions and best practices
|
|
267
|
+
- [Benchmarks](docs/BENCHMARKS.md) - Performance testing results
|
|
205
268
|
|
|
206
269
|
## Performance
|
|
207
270
|
|
|
208
|
-
Woodland delivers **enterprise-grade security without sacrificing performance**. Security features add minimal overhead
|
|
271
|
+
Woodland delivers **enterprise-grade security without sacrificing performance**. Security features add minimal overhead.
|
|
209
272
|
|
|
210
273
|
| Framework | Security Approach | Mean Response Time |
|
|
211
274
|
|-----------|------------------|-------------------|
|
|
@@ -222,6 +285,8 @@ Woodland implements multiple layers of protection:
|
|
|
222
285
|
3. **Input Validation** - IP addresses validated, URLs parsed securely
|
|
223
286
|
4. **Output Encoding** - HTML escaping automatic
|
|
224
287
|
5. **Secure Error Handling** - No internal paths exposed
|
|
288
|
+
6. **Header Injection Prevention** - Type validation for headers
|
|
289
|
+
7. **Prototype Pollution Protection** - Safe ETag generation
|
|
225
290
|
|
|
226
291
|
**Production Setup:**
|
|
227
292
|
|
|
@@ -230,16 +295,37 @@ import helmet from "helmet";
|
|
|
230
295
|
import rateLimit from "express-rate-limit";
|
|
231
296
|
|
|
232
297
|
app.always(helmet());
|
|
233
|
-
app.always(
|
|
298
|
+
app.always(
|
|
299
|
+
rateLimit({
|
|
300
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
301
|
+
max: 100, // Limit each IP to 100 requests
|
|
302
|
+
standardHeaders: true,
|
|
303
|
+
legacyHeaders: false,
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
234
306
|
```
|
|
235
307
|
|
|
308
|
+
**Security Warning:**
|
|
309
|
+
> ⚠️ **Production Deployment**: Always use a reverse proxy (nginx, Cloudflare) in production for SSL/TLS termination, DDoS protection, and additional security layers.
|
|
310
|
+
|
|
236
311
|
## License
|
|
237
312
|
|
|
238
313
|
Copyright (c) 2026 Jason Mulligan
|
|
239
314
|
|
|
240
315
|
Licensed under the **BSD-3-Clause** license.
|
|
241
316
|
|
|
317
|
+
## Contributing
|
|
318
|
+
|
|
319
|
+
Contributions are welcome!
|
|
320
|
+
|
|
321
|
+
1. Fork the repository
|
|
322
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
323
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
324
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
325
|
+
5. Open a Pull Request
|
|
326
|
+
|
|
242
327
|
## Support
|
|
243
328
|
|
|
244
329
|
- **Issues**: [GitHub Issues](https://github.com/avoidwork/woodland/issues)
|
|
245
330
|
- **Discussions**: [GitHub Discussions](https://github.com/avoidwork/woodland/discussions)
|
|
331
|
+
- **Email**: jason@mulligan.me
|
package/dist/cli.cjs
CHANGED
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
|
|
6
6
|
* @license BSD-3-Clause
|
|
7
|
-
* @version 22.0.
|
|
7
|
+
* @version 22.0.5
|
|
8
8
|
*/
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
var node_http = require('node:http');
|
|
12
|
-
var node_url = require('node:url');
|
|
13
|
-
var node_path = require('node:path');
|
|
14
|
-
var tinyCoerce = require('tiny-coerce');
|
|
15
12
|
var woodland = require('woodland');
|
|
16
13
|
var node_module = require('node:module');
|
|
14
|
+
var node_path = require('node:path');
|
|
15
|
+
var node_url = require('node:url');
|
|
17
16
|
var mimeDb = require('mime-db');
|
|
17
|
+
var tinyCoerce = require('tiny-coerce');
|
|
18
18
|
|
|
19
19
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
20
20
|
const __dirname$1 = node_url.fileURLToPath(new node_url.URL(".", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
@@ -37,20 +37,23 @@ const INT_10 = 10;
|
|
|
37
37
|
const INT_255 = 255;
|
|
38
38
|
const INT_8000 = 8000;
|
|
39
39
|
const INT_65535 = 65535;
|
|
40
|
+
const INT_NEG_1 = -1;
|
|
40
41
|
const COLON = ":";
|
|
41
42
|
const DOUBLE_COLON = "::";
|
|
42
43
|
const EMPTY = "";
|
|
43
44
|
const EQUAL = "=";
|
|
44
45
|
const HYPHEN = "-";
|
|
45
|
-
const WOODLAND = "woodland";
|
|
46
46
|
const STRING = "string";
|
|
47
47
|
`nodejs/${process.version}, ${process.platform}/${process.arch}`;
|
|
48
48
|
const LOCALHOST = "127.0.0.1";
|
|
49
49
|
const EXTENSIONS = "extensions";
|
|
50
50
|
const INFO = "info";
|
|
51
|
+
const MSG_INVALID_IP = "Invalid IP: must be a valid IPv4 or IPv6 address.";
|
|
52
|
+
const MSG_INVALID_PORT = "Invalid port: must be an integer between 0 and 65535.";
|
|
51
53
|
const NO_CACHE = "no-cache";
|
|
52
54
|
const EN_US = "en-US";
|
|
53
55
|
const SHORT = "short";
|
|
56
|
+
const EVT_LISTENING = "listening";
|
|
54
57
|
|
|
55
58
|
Object.freeze(
|
|
56
59
|
Array.from({ length: 12 }, (_, idx) => {
|
|
@@ -60,56 +63,55 @@ Object.freeze(
|
|
|
60
63
|
return Object.freeze(d.toLocaleString(EN_US, { month: SHORT }));
|
|
61
64
|
}),
|
|
62
65
|
);
|
|
66
|
+
const IPV4_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
67
|
+
const IPV6_CHAR_PATTERN = /^[0-9a-fA-F:.]+$/;
|
|
68
|
+
const IPV4_MAPPED_PATTERN = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i;
|
|
69
|
+
const HEX_GROUP_PATTERN = /^[0-9a-fA-F]{1,4}$/;
|
|
63
70
|
|
|
64
|
-
const valid = Object.entries(mimeDb).filter((i) => EXTENSIONS in i[
|
|
71
|
+
const valid = Object.entries(mimeDb).filter((i) => EXTENSIONS in i[INT_1]);
|
|
65
72
|
valid.reduce((a, v) => {
|
|
66
|
-
const result = Object.assign({ type: v[
|
|
73
|
+
const result = Object.assign({ type: v[INT_0] }, v[INT_1]);
|
|
67
74
|
const extCount = result.extensions.length;
|
|
68
|
-
for (let i =
|
|
75
|
+
for (let i = INT_0; i < extCount; i++) {
|
|
69
76
|
a[`.${result.extensions[i]}`] = result;
|
|
70
77
|
}
|
|
71
78
|
return a;
|
|
72
79
|
}, {});
|
|
73
80
|
|
|
74
|
-
const IPV4_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
|
|
75
|
-
IPV6_CHAR_PATTERN = /^[0-9a-fA-F:.]+$/,
|
|
76
|
-
IPV4_MAPPED_PATTERN = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i,
|
|
77
|
-
HEX_GROUP_PATTERN = /^[0-9a-fA-F]{1,4}$/;
|
|
78
|
-
|
|
79
81
|
/**
|
|
80
|
-
* Validates
|
|
81
|
-
* @param {string} ip -
|
|
82
|
-
* @returns {boolean} True if
|
|
82
|
+
* Validates IPv4 address format
|
|
83
|
+
* @param {string} ip - IPv4 address to validate
|
|
84
|
+
* @returns {boolean} True if valid IPv4
|
|
83
85
|
*/
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
+
function isValidIPv4(ip) {
|
|
87
|
+
const match = IPV4_PATTERN.exec(ip);
|
|
88
|
+
if (!match) {
|
|
86
89
|
return false;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
if (!match) {
|
|
92
|
+
for (let i = INT_1; i < INT_5; i++) {
|
|
93
|
+
const num = parseInt(match[i], INT_10);
|
|
94
|
+
if (num > INT_255) {
|
|
93
95
|
return false;
|
|
94
96
|
}
|
|
95
|
-
|
|
96
|
-
for (let i = 1; i < INT_5; i++) {
|
|
97
|
-
const num = parseInt(match[i], INT_10);
|
|
98
|
-
if (num > INT_255) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return true;
|
|
104
97
|
}
|
|
105
98
|
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validates IPv6 address format
|
|
104
|
+
* @param {string} ip - IPv6 address to validate
|
|
105
|
+
* @returns {boolean} True if valid IPv6
|
|
106
|
+
*/
|
|
107
|
+
function isValidIPv6(ip) {
|
|
106
108
|
if (!IPV6_CHAR_PATTERN.test(ip)) {
|
|
107
109
|
return false;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
const ipv4MappedMatch = IPV4_MAPPED_PATTERN.exec(ip);
|
|
111
113
|
if (ipv4MappedMatch) {
|
|
112
|
-
return
|
|
114
|
+
return isValidIPv4(ipv4MappedMatch[INT_1]);
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
if (ip === DOUBLE_COLON) {
|
|
@@ -117,75 +119,95 @@ function isValidIP(ip) {
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
const doubleColonIndex = ip.indexOf(DOUBLE_COLON);
|
|
120
|
-
const isCompressed = doubleColonIndex !==
|
|
122
|
+
const isCompressed = doubleColonIndex !== INT_NEG_1;
|
|
121
123
|
|
|
122
124
|
if (isCompressed) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
125
|
+
return validateCompressedIPv6(ip, doubleColonIndex);
|
|
126
|
+
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
(doubleColonIndex + INT_2 < ip.length && ip.charAt(doubleColonIndex + INT_2) === COLON)
|
|
130
|
-
) {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
128
|
+
return validateUncompressedIPv6(ip);
|
|
129
|
+
}
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Validates compressed IPv6 address (with ::)
|
|
133
|
+
* @param {string} ip - IPv6 address
|
|
134
|
+
* @param {number} doubleColonIndex - Position of ::
|
|
135
|
+
* @returns {boolean} True if valid
|
|
136
|
+
*/
|
|
137
|
+
function validateCompressedIPv6(ip, doubleColonIndex) {
|
|
138
|
+
if (ip.indexOf(DOUBLE_COLON, doubleColonIndex + INT_2) !== INT_NEG_1) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
if (
|
|
143
|
+
(doubleColonIndex > INT_0 && ip.charAt(doubleColonIndex - INT_1) === COLON) ||
|
|
144
|
+
(doubleColonIndex + INT_2 < ip.length && ip.charAt(doubleColonIndex + INT_2) === COLON)
|
|
145
|
+
) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
rightGroups = afterDoubleColon.split(COLON);
|
|
147
|
-
} else {
|
|
148
|
-
rightGroups = [];
|
|
149
|
-
}
|
|
149
|
+
const beforeDoubleColon = ip.substring(INT_0, doubleColonIndex);
|
|
150
|
+
const afterDoubleColon = ip.substring(doubleColonIndex + INT_2);
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const totalGroups = nonEmptyLeft.length + nonEmptyRight.length;
|
|
152
|
+
const leftGroups = beforeDoubleColon ? beforeDoubleColon.split(COLON) : [];
|
|
153
|
+
const rightGroups = afterDoubleColon ? afterDoubleColon.split(COLON) : [];
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
155
|
+
const totalGroups =
|
|
156
|
+
leftGroups.filter((g) => g !== EMPTY).length + rightGroups.filter((g) => g !== EMPTY).length;
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
158
|
+
if (totalGroups >= INT_8) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
165
161
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (!HEX_GROUP_PATTERN.test(nonEmptyRight[i])) {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
162
|
+
return validateHexGroups(leftGroups) && validateHexGroups(rightGroups);
|
|
163
|
+
}
|
|
172
164
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Validates uncompressed IPv6 address
|
|
167
|
+
* @param {string} ip - IPv6 address
|
|
168
|
+
* @returns {boolean} True if valid
|
|
169
|
+
*/
|
|
170
|
+
function validateUncompressedIPv6(ip) {
|
|
171
|
+
const groups = ip.split(COLON);
|
|
172
|
+
if (groups.length !== INT_8) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return validateHexGroups(groups);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validates hex groups in IPv6 address
|
|
181
|
+
* @param {Array} groups - Array of hex group strings
|
|
182
|
+
* @returns {boolean} True if all groups are valid
|
|
183
|
+
*/
|
|
184
|
+
function validateHexGroups(groups) {
|
|
185
|
+
const groupCount = groups.length;
|
|
186
|
+
|
|
187
|
+
for (let i = INT_0; i < groupCount; i++) {
|
|
188
|
+
/* node:coverage ignore next 3 */
|
|
189
|
+
if (!groups[i] || !HEX_GROUP_PATTERN.test(groups[i])) {
|
|
177
190
|
return false;
|
|
178
191
|
}
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
179
195
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Validates if an IP address is properly formatted
|
|
198
|
+
* @param {string} ip - IP address to validate
|
|
199
|
+
* @returns {boolean} True if IP is valid format
|
|
200
|
+
*/
|
|
201
|
+
function isValidIP(ip) {
|
|
202
|
+
if (!ip || typeof ip !== STRING) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
186
205
|
|
|
187
|
-
|
|
206
|
+
if (ip.indexOf(COLON) === INT_NEG_1) {
|
|
207
|
+
return isValidIPv4(ip);
|
|
188
208
|
}
|
|
209
|
+
|
|
210
|
+
return isValidIPv6(ip);
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
/**
|
|
@@ -195,10 +217,10 @@ function isValidIP(ip) {
|
|
|
195
217
|
*/
|
|
196
218
|
function parseArgs(args) {
|
|
197
219
|
return args
|
|
198
|
-
.filter((i) => i.charAt(
|
|
220
|
+
.filter((i) => i.charAt(INT_0) === HYPHEN && i.charAt(INT_1) === HYPHEN)
|
|
199
221
|
.reduce((a, v) => {
|
|
200
|
-
const x = v.split(`${HYPHEN}${HYPHEN}`)[
|
|
201
|
-
a[x[
|
|
222
|
+
const x = v.split(`${HYPHEN}${HYPHEN}`)[INT_1].split(EQUAL);
|
|
223
|
+
a[x[INT_0]] = tinyCoerce.coerce(x[INT_1]);
|
|
202
224
|
return a;
|
|
203
225
|
}, {});
|
|
204
226
|
}
|
|
@@ -209,13 +231,12 @@ function parseArgs(args) {
|
|
|
209
231
|
* @returns {Object} Validation result with valid flag and error message
|
|
210
232
|
*/
|
|
211
233
|
function validatePort(port) {
|
|
212
|
-
// Reject empty strings and whitespace-only values
|
|
213
234
|
if (port === EMPTY || (typeof port === STRING && port.trim() === EMPTY)) {
|
|
214
|
-
return { valid: false, error:
|
|
235
|
+
return { valid: false, error: MSG_INVALID_PORT };
|
|
215
236
|
}
|
|
216
237
|
const validPort = Number(port);
|
|
217
238
|
if (!Number.isInteger(validPort) || validPort < INT_0 || validPort > INT_65535) {
|
|
218
|
-
return { valid: false, error:
|
|
239
|
+
return { valid: false, error: MSG_INVALID_PORT };
|
|
219
240
|
}
|
|
220
241
|
return { valid: true, port: validPort };
|
|
221
242
|
}
|
|
@@ -228,7 +249,7 @@ function validatePort(port) {
|
|
|
228
249
|
function validateIP(ip) {
|
|
229
250
|
const validIP = isValidIP(ip);
|
|
230
251
|
if (!validIP) {
|
|
231
|
-
return { valid: false, error:
|
|
252
|
+
return { valid: false, error: MSG_INVALID_IP };
|
|
232
253
|
}
|
|
233
254
|
return { valid: true, ip };
|
|
234
255
|
}
|
|
@@ -270,7 +291,7 @@ function main(args = process.argv) {
|
|
|
270
291
|
const server = node_http.createServer(app.route);
|
|
271
292
|
server.listen(portValidation.port, ip);
|
|
272
293
|
/* node:coverage ignore next 6 */
|
|
273
|
-
server.on(
|
|
294
|
+
server.on(EVT_LISTENING, () => {
|
|
274
295
|
const actualPort = server.address().port;
|
|
275
296
|
app.logger.log(
|
|
276
297
|
`id=woodland, hostname=${process.env.HOSTNAME ?? "localhost"}, ip=${ip}, port=${actualPort}`,
|
|
@@ -281,17 +302,8 @@ function main(args = process.argv) {
|
|
|
281
302
|
return server;
|
|
282
303
|
}
|
|
283
304
|
|
|
284
|
-
// CLI entry point -
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (process.argv[1]) {
|
|
288
|
-
const scriptPath = node_path.resolve(process.argv[1]);
|
|
289
|
-
if (scriptPath === __filename$1 || node_path.basename(scriptPath) === WOODLAND) {
|
|
290
|
-
main();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
305
|
+
// CLI entry point - always run main
|
|
306
|
+
/* node:coverage ignore next */
|
|
307
|
+
main();
|
|
293
308
|
|
|
294
309
|
exports.main = main;
|
|
295
|
-
exports.parseArgs = parseArgs;
|
|
296
|
-
exports.validateIP = validateIP;
|
|
297
|
-
exports.validatePort = validatePort;
|