Skip to content

Commit 5c728f1

Browse files
committed
Major updates
1 parent 6a5cf7a commit 5c728f1

File tree

7 files changed

+1017
-449
lines changed

7 files changed

+1017
-449
lines changed

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,39 @@
11
# Changelog
22

33
All notable changes to this project will be documented in this file.
4+
5+
## [0.9.6] - 2025-08-01
6+
### Updated
7+
- Updated License to LGPL-2.1-only.
8+
9+
### Added Core Enhancements
10+
- Enhanced server/index.js with comprehensive JSDoc type definitions
11+
- Added improved routing with deterministic parameter handling
12+
- Integrated CORS support with automatic preflight OPTIONS handling
13+
- Added static file serving with security protections
14+
- Enhanced response.js with chainable API and improved error handling
15+
- Added robust file serving capabilities with directory traversal protection
16+
- Implemented connection timeout and error handling for sockets
17+
- Added detailed type definitions for better IDE support
18+
19+
### Fixed Core Issues
20+
- Fixed route parameter extraction with safe URL decoding
21+
- Improved error handling in response methods
22+
- Added proper MIME type detection for static files
23+
- Enhanced socket error handling and cleanup
24+
- Fixed CORS preflight response handling
25+
26+
### Added Utilities
27+
- Created lib/cors.js utility for centralized CORS handling
28+
- Enhanced lib/httpParser.js with safe fallbacks
29+
- Added comprehensive utils for MIME type detection
30+
- Added robust path normalization and security checks
31+
32+
### Documentation
33+
- Added comprehensive JSDoc documentation for all public APIs
34+
- Created detailed type definitions for TypeScript compatibility
35+
- Added inline documentation for complex functions
36+
437
## [0.9.5] - 2025-03-06
538
### Added
639
- Added 'OPTIONS' method to handle preflight requests for CORS.

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Start by forking the repository on GitHub. This will create a personal copy of t
1313
Clone your forked repository to your local machine:
1414

1515
```bash
16-
git clone https://github.com/YOUR_USERNAME/hasty-server.git
16+
git clone https://github.com/IntegerAlex/hasty-server.git
1717
cd hasty-server
1818
```
1919

lib/cors.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @typedef {Object} CorsHeaders
3+
* @property {string} 'Access-Control-Allow-Origin' - The origin(s) allowed to access the resource
4+
* @property {string} 'Access-Control-Allow-Methods' - The HTTP methods allowed when accessing the resource
5+
* @property {string} 'Access-Control-Allow-Headers' - The headers that can be used during the actual request
6+
* @property {string} 'Access-Control-Max-Age' - How long the results of a preflight request can be cached
7+
*/
8+
9+
/**
10+
* Default CORS headers
11+
* @type {CorsHeaders}
12+
*/
13+
const DEFAULT_CORS_HEADERS = {
14+
'Access-Control-Allow-Origin': '*',
15+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
16+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
17+
'Access-Control-Max-Age': '86400' // 24 hours
18+
};
19+
20+
/**
21+
* Applies CORS headers to a response
22+
* @param {Object} response - The response object to modify
23+
* @param {boolean} [enabled=true] - Whether CORS is enabled
24+
* @param {Object} [customHeaders] - Custom CORS headers to merge with defaults
25+
* @returns {void}
26+
*/
27+
function applyCorsHeaders(response, enabled = true, customHeaders = {}) {
28+
if (!enabled) return;
29+
30+
const headers = { ...DEFAULT_CORS_HEADERS, ...customHeaders };
31+
32+
Object.entries(headers).forEach(([key, value]) => {
33+
response.setHeader(key, value);
34+
});
35+
}
36+
37+
/**
38+
* Handles preflight OPTIONS requests
39+
* @param {Object} request - The request object
40+
* @param {Object} response - The response object
41+
* @param {boolean} [enabled=true] - Whether CORS is enabled
42+
* @returns {boolean} - True if this was a preflight request that was handled
43+
*/
44+
function handlePreflight(request, response, enabled = true) {
45+
if (!enabled || request.method !== 'OPTIONS') {
46+
return false;
47+
}
48+
49+
applyCorsHeaders(response, true, {
50+
'Access-Control-Allow-Methods': request.headers['access-control-request-method'] ||
51+
DEFAULT_CORS_HEADERS['Access-Control-Allow-Methods'],
52+
'Access-Control-Allow-Headers': request.headers['access-control-request-headers'] ||
53+
DEFAULT_CORS_HEADERS['Access-Control-Allow-Headers']
54+
});
55+
56+
response.status(204).send('');
57+
return true;
58+
}
59+
60+
module.exports = {
61+
applyCorsHeaders,
62+
handlePreflight,
63+
DEFAULT_CORS_HEADERS
64+
};

lib/httpParser.js

Lines changed: 154 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,185 @@
11
const { findFirstBrac, HTTPbody, JSONbodyParser, queryParser } = require('./utils.js')
22

3-
// Async function to parse the HTTP request
3+
/**
4+
* @typedef {Object} ParsedRequest
5+
* @property {string} method - HTTP method (e.g., 'GET', 'POST')
6+
* @property {string} path - Request path without query string
7+
* @property {string} version - HTTP version (e.g., 'HTTP/1.1')
8+
* @property {Object.<string, string>} headers - Lowercase header keys with their values
9+
* @property {Object.<string, string|string[]>} query - Parsed query parameters
10+
* @property {Object|string|Buffer} [body] - Parsed request body
11+
* @property {string} ip - Client IP address
12+
* @property {Object} [cors] - CORS related information (for OPTIONS requests)
13+
* @property {string} [cors.origin] - Origin header from CORS preflight
14+
* @property {string} [cors.method] - Requested method from CORS preflight
15+
* @property {string} [cors.headers] - Requested headers from CORS preflight
16+
*/
17+
18+
/**
19+
* Parses an HTTP request from raw data
20+
* @param {string|Buffer} request - Raw HTTP request data
21+
* @param {Object} [connection={}] - Connection information (e.g., remoteAddress)
22+
* @param {string} [connection.remoteAddress] - Client IP address
23+
* @returns {Promise<ParsedRequest>} Parsed HTTP request object
24+
* @throws {Error} If the request is malformed
25+
*/
426
async function httpParser(request, connection = {}) {
527
try {
6-
const req = {} // Create a new object to store the parsed request
7-
const requestString = request.toString() // Convert buffer to string, if necessary
28+
/** @type {ParsedRequest} */
29+
const req = {
30+
method: 'GET',
31+
path: '/',
32+
version: 'HTTP/1.1',
33+
headers: {},
34+
query: {},
35+
ip: '127.0.0.1',
36+
body: undefined
37+
};
38+
39+
// Convert buffer to string if necessary and handle empty requests
40+
const requestString = (request && request.toString) ? request.toString() : '';
841

9-
// Set client IP address (similar to Express)
10-
req.ip = connection.remoteAddress || '127.0.0.1'
42+
// Set client IP address with fallback
43+
if (connection && connection.remoteAddress) {
44+
req.ip = connection.remoteAddress;
45+
}
1146

1247
// Step 1: Split the request into headers and body by finding "\r\n\r\n"
13-
const headerBodySplit = requestString.split('\r\n\r\n') // Headers and body are separated by double newline
14-
if (headerBodySplit.length < 1) {
15-
throw new Error('Invalid HTTP request format')
48+
// Split into headers and body parts
49+
const headerBodySplit = requestString.split('\r\n\r\n');
50+
if (headerBodySplit.length === 0 || !headerBodySplit[0]) {
51+
throw new Error('Invalid HTTP request: Missing headers');
1652
}
1753

1854
const headersPart = headerBodySplit[0] // First part is the headers
1955
const bodyPart = headerBodySplit[1] || '' // Second part is the body, default to empty string if no body
2056

2157
// Step 2: Extract the headers (the first line is the request line, e.g., "POST /path HTTP/1.1")
22-
const headers = headersPart.split(/\r?\n/).filter(line => line.trim()) // Handle both \r\n and \n
58+
// Split headers into lines, handling both CRLF and LF line endings
59+
const headers = headersPart.split(/\r?\n/).filter(line => line.trim());
2360

2461
// Parse the request line (first line of the headers)
25-
const requestLine = headers[0].split(' ') // ["POST", "/path", "HTTP/1.1"]
26-
if (requestLine.length !== 3) {
27-
throw new Error('Invalid request line format')
62+
const requestLine = (headers[0] || '').split(/\s+/);
63+
if (requestLine.length < 3 || !requestLine[0] || !requestLine[1] || !requestLine[2]) {
64+
throw new Error('Invalid request line format');
2865
}
2966

30-
req.method = requestLine[0].toUpperCase() // e.g., "POST"
31-
req.path = requestLine[1] // e.g., "/path"
32-
req.version = requestLine[2] // e.g., "HTTP/1.1"
67+
// Parse method, path, and version with validation
68+
const method = requestLine[0].toUpperCase();
69+
const path = requestLine[1] || '/';
70+
const version = requestLine[2];
71+
72+
// Validate HTTP method
73+
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
74+
if (!validMethods.includes(method)) {
75+
throw new Error(`Unsupported HTTP method: ${method}`);
76+
}
77+
78+
// Validate HTTP version
79+
if (!/^HTTP\/\d+\.\d+$/.test(version)) {
80+
throw new Error(`Invalid HTTP version: ${version}`);
81+
}
82+
83+
req.method = method;
84+
req.path = path;
85+
req.version = version;
3386

34-
// Add headers parsing
35-
req.headers = {}
87+
// Parse headers with validation
3688
for (let i = 1; i < headers.length; i++) {
37-
const line = headers[i].trim()
38-
if (line) {
39-
const colonIndex = line.indexOf(':')
40-
if (colonIndex === -1) continue // Skip malformed headers
41-
42-
const key = line.slice(0, colonIndex).trim().toLowerCase()
43-
const value = line.slice(colonIndex + 1).trim()
44-
req.headers[key] = value
89+
const line = (headers[i] || '').trim();
90+
if (!line) continue;
91+
92+
const colonIndex = line.indexOf(':');
93+
if (colonIndex <= 0) continue; // Skip malformed headers
94+
95+
const key = line.slice(0, colonIndex).trim().toLowerCase();
96+
const value = line.slice(colonIndex + 1).trim();
97+
98+
// Handle duplicate headers by appending with comma (per HTTP spec)
99+
if (req.headers[key]) {
100+
if (Array.isArray(req.headers[key])) {
101+
req.headers[key].push(value);
102+
} else {
103+
req.headers[key] = [req.headers[key], value];
104+
}
105+
} else {
106+
req.headers[key] = value;
45107
}
46108
}
47109

48-
// Step 3: Handle GET requests (expect a query string)
49-
req.query = queryParser(req.path) // Parse query string for GET requests
50-
req.path = req.path.split('?')[0] // Remove query string from path
51-
52-
// Step 4: Handle POST and OPTIONS requests
53-
if (req.method === 'POST') {
54-
if (!bodyPart) {
55-
req.body = {}
110+
// Parse query string and clean path
111+
try {
112+
// Handle potential URI encoding issues
113+
const cleanPath = decodeURIComponent(req.path || '/').split('?')[0] || '/';
114+
req.path = cleanPath;
115+
116+
// Parse query parameters safely
117+
const queryStart = req.path.indexOf('?');
118+
if (queryStart !== -1) {
119+
req.query = queryParser(req.path.slice(queryStart + 1));
120+
req.path = req.path.slice(0, queryStart);
56121
} else {
57-
try {
58-
// Await the body parsing (this is an async operation)
59-
const bodyData = await HTTPbody(bodyPart, 0)
60-
// Step 5: Parse the body into JSON format
61-
req.body = JSONbodyParser(bodyData) // Convert the parsed body into JSON
62-
} catch (error) {
63-
console.error('Error parsing request body:', error)
64-
req.body = {} // Set empty object as fallback
65-
}
122+
req.query = {};
66123
}
67-
} else if (req.method === 'OPTIONS') {
68-
// Handle OPTIONS preflight request
69-
req.body = {}
70-
// Store CORS-specific headers for easy access
71-
req.cors = {
72-
origin: req.headers['origin'],
73-
method: req.headers['access-control-request-method'],
74-
headers: req.headers['access-control-request-headers']
124+
} catch (error) {
125+
console.warn('Error parsing query string:', error);
126+
req.query = {};
127+
req.path = '/';
128+
}
129+
130+
// Parse request body based on method and content type
131+
try {
132+
if (['POST', 'PUT', 'PATCH'].includes(req.method) && bodyPart) {
133+
const contentType = (req.headers['content-type'] || '').toLowerCase();
134+
135+
if (contentType.includes('application/json')) {
136+
try {
137+
const bodyData = await HTTPbody(bodyPart, 0);
138+
req.body = JSONbodyParser(bodyData) || {};
139+
} catch (error) {
140+
console.warn('Error parsing JSON body:', error);
141+
req.body = {};
142+
}
143+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
144+
try {
145+
req.body = queryParser(bodyPart);
146+
} catch (error) {
147+
console.warn('Error parsing form data:', error);
148+
req.body = {};
149+
}
150+
} else {
151+
// For other content types, keep as raw string
152+
req.body = bodyPart;
153+
}
154+
} else if (req.method === 'OPTIONS') {
155+
// Handle OPTIONS preflight request
156+
req.body = {};
157+
req.cors = {
158+
origin: req.headers['origin'],
159+
method: req.headers['access-control-request-method'] || '*',
160+
headers: req.headers['access-control-request-headers'] || ''
161+
};
162+
} else {
163+
req.body = undefined;
75164
}
165+
} catch (error) {
166+
console.warn('Error processing request body:', error);
167+
req.body = {};
76168
}
77169

78-
return req // Return the fully parsed request object
170+
return req;
79171
} catch (error) {
80-
console.error('Error parsing HTTP request:', error)
81-
throw error // Re-throw to let caller handle the error
172+
console.error('Error parsing HTTP request:', error);
173+
// Create a minimal valid request object even on error
174+
return {
175+
method: 'GET',
176+
path: '/',
177+
version: 'HTTP/1.1',
178+
headers: {},
179+
query: {},
180+
ip: connection.remoteAddress || '127.0.0.1',
181+
body: undefined
182+
};
82183
}
83184
}
84185

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hasty-server",
3-
"version": "0.9.5",
3+
"version": "0.9.6",
44
"main": "./server/index.js",
55
"directories": {
66
"lib": "lib",
@@ -27,12 +27,13 @@
2727
"package.json",
2828
"CHANGELOG.md"
2929
],
30-
"author": "Akshat Kotpalliwar",
30+
"author": "Akshat Kotpalliwar (alias IntegerAlex on GitHub)",
3131
"repository": {
3232
"type": "git",
3333
"url": "https://github.com/IntegerAlex/hasty-server.git"
3434
},
3535
"homepage": "https://hasty-server.vercel.app",
36-
"license": "GPL-3.0",
37-
"description": "A Blazing fast simple http server for node.js"
36+
"license": "LGPL-2.1-only",
37+
"description": "A Blazing fast simple http server for node.js",
38+
"dependencies": {}
3839
}

0 commit comments

Comments
 (0)