|
1 | 1 | const { findFirstBrac, HTTPbody, JSONbodyParser, queryParser } = require('./utils.js') |
2 | 2 |
|
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 | + */ |
4 | 26 | async function httpParser(request, connection = {}) { |
5 | 27 | 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() : ''; |
8 | 41 |
|
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 | + } |
11 | 46 |
|
12 | 47 | // 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'); |
16 | 52 | } |
17 | 53 |
|
18 | 54 | const headersPart = headerBodySplit[0] // First part is the headers |
19 | 55 | const bodyPart = headerBodySplit[1] || '' // Second part is the body, default to empty string if no body |
20 | 56 |
|
21 | 57 | // 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()); |
23 | 60 |
|
24 | 61 | // 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'); |
28 | 65 | } |
29 | 66 |
|
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; |
33 | 86 |
|
34 | | - // Add headers parsing |
35 | | - req.headers = {} |
| 87 | + // Parse headers with validation |
36 | 88 | 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; |
45 | 107 | } |
46 | 108 | } |
47 | 109 |
|
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); |
56 | 121 | } 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 = {}; |
66 | 123 | } |
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; |
75 | 164 | } |
| 165 | + } catch (error) { |
| 166 | + console.warn('Error processing request body:', error); |
| 167 | + req.body = {}; |
76 | 168 | } |
77 | 169 |
|
78 | | - return req // Return the fully parsed request object |
| 170 | + return req; |
79 | 171 | } 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 | + }; |
82 | 183 | } |
83 | 184 | } |
84 | 185 |
|
|
0 commit comments