Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions lib/cors.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const DEFAULT_CORS_HEADERS = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400' // 24 hours
};
}

/**
* Applies CORS headers to a response
Expand All @@ -24,14 +24,14 @@ const DEFAULT_CORS_HEADERS = {
* @param {Object} [customHeaders] - Custom CORS headers to merge with defaults
* @returns {void}
*/
function applyCorsHeaders(response, enabled = true, customHeaders = {}) {
if (!enabled) return;
const headers = { ...DEFAULT_CORS_HEADERS, ...customHeaders };
function applyCorsHeaders (response, enabled = true, customHeaders = {}) {
if (!enabled) return

const headers = { ...DEFAULT_CORS_HEADERS, ...customHeaders }

Object.entries(headers).forEach(([key, value]) => {
response.setHeader(key, value);
});
response.setHeader(key, value)
})
}

/**
Expand All @@ -41,24 +41,24 @@ function applyCorsHeaders(response, enabled = true, customHeaders = {}) {
* @param {boolean} [enabled=true] - Whether CORS is enabled
* @returns {boolean} - True if this was a preflight request that was handled
*/
function handlePreflight(request, response, enabled = true) {
function handlePreflight (request, response, enabled = true) {
if (!enabled || request.method !== 'OPTIONS') {
return false;
return false
}

applyCorsHeaders(response, true, {
'Access-Control-Allow-Methods': request.headers['access-control-request-method'] ||
'Access-Control-Allow-Methods': request.headers['access-control-request-method'] ||
DEFAULT_CORS_HEADERS['Access-Control-Allow-Methods'],
'Access-Control-Allow-Headers': request.headers['access-control-request-headers'] ||
'Access-Control-Allow-Headers': request.headers['access-control-request-headers'] ||
DEFAULT_CORS_HEADERS['Access-Control-Allow-Headers']
});
})

response.status(204).send('');
return true;
response.status(204).send('')
return true
}

module.exports = {
applyCorsHeaders,
handlePreflight,
DEFAULT_CORS_HEADERS
};
}
130 changes: 65 additions & 65 deletions lib/httpParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const { findFirstBrac, HTTPbody, JSONbodyParser, queryParser } = require('./util
* @returns {Promise<ParsedRequest>} Parsed HTTP request object
* @throws {Error} If the request is malformed
*/
async function httpParser(request, connection = {}) {
async function httpParser (request, connection = {}) {
try {
/** @type {ParsedRequest} */
const req = {
Expand All @@ -34,142 +34,142 @@ async function httpParser(request, connection = {}) {
query: {},
ip: '127.0.0.1',
body: undefined
};
}

// Convert buffer to string if necessary and handle empty requests
const requestString = (request && request.toString) ? request.toString() : '';
const requestString = (request && request.toString) ? request.toString() : ''

// Set client IP address with fallback
if (connection && connection.remoteAddress) {
req.ip = connection.remoteAddress;
req.ip = connection.remoteAddress
}

// Step 1: Split the request into headers and body by finding "\r\n\r\n"
// Split into headers and body parts
const headerBodySplit = requestString.split('\r\n\r\n');
const headerBodySplit = requestString.split('\r\n\r\n')
if (headerBodySplit.length === 0 || !headerBodySplit[0]) {
throw new Error('Invalid HTTP request: Missing headers');
throw new Error('Invalid HTTP request: Missing headers')
}

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

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

// Parse the request line (first line of the headers)
const requestLine = (headers[0] || '').split(/\s+/);
const requestLine = (headers[0] || '').split(/\s+/)
if (requestLine.length < 3 || !requestLine[0] || !requestLine[1] || !requestLine[2]) {
throw new Error('Invalid request line format');
throw new Error('Invalid request line format')
}

// Parse method, path, and version with validation
const method = requestLine[0].toUpperCase();
const path = requestLine[1] || '/';
const version = requestLine[2];
const method = requestLine[0].toUpperCase()
const path = requestLine[1] || '/'
const version = requestLine[2]

// Validate HTTP method
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
if (!validMethods.includes(method)) {
throw new Error(`Unsupported HTTP method: ${method}`);
throw new Error(`Unsupported HTTP method: ${method}`)
}

// Validate HTTP version
if (!/^HTTP\/\d+\.\d+$/.test(version)) {
throw new Error(`Invalid HTTP version: ${version}`);
throw new Error(`Invalid HTTP version: ${version}`)
}
req.method = method;
req.path = path;
req.version = version;

req.method = method
req.path = path
req.version = version

// Parse headers with validation
for (let i = 1; i < headers.length; i++) {
const line = (headers[i] || '').trim();
if (!line) continue;
const colonIndex = line.indexOf(':');
if (colonIndex <= 0) continue; // Skip malformed headers
const key = line.slice(0, colonIndex).trim().toLowerCase();
const value = line.slice(colonIndex + 1).trim();
const line = (headers[i] || '').trim()
if (!line) continue

const colonIndex = line.indexOf(':')
if (colonIndex <= 0) continue // Skip malformed headers

const key = line.slice(0, colonIndex).trim().toLowerCase()
const value = line.slice(colonIndex + 1).trim()

// Handle duplicate headers by appending with comma (per HTTP spec)
if (req.headers[key]) {
if (Array.isArray(req.headers[key])) {
req.headers[key].push(value);
req.headers[key].push(value)
} else {
req.headers[key] = [req.headers[key], value];
req.headers[key] = [req.headers[key], value]
}
} else {
req.headers[key] = value;
req.headers[key] = value
}
}

// Parse query string and clean path
try {
// Handle potential URI encoding issues
const cleanPath = decodeURIComponent(req.path || '/').split('?')[0] || '/';
req.path = cleanPath;
const cleanPath = decodeURIComponent(req.path || '/').split('?')[0] || '/'
req.path = cleanPath

// Parse query parameters safely
const queryStart = req.path.indexOf('?');
const queryStart = req.path.indexOf('?')
if (queryStart !== -1) {
req.query = queryParser(req.path.slice(queryStart + 1));
req.path = req.path.slice(0, queryStart);
req.query = queryParser(req.path.slice(queryStart + 1))
req.path = req.path.slice(0, queryStart)
} else {
req.query = {};
req.query = {}
}
} catch (error) {
console.warn('Error parsing query string:', error);
req.query = {};
req.path = '/';
console.warn('Error parsing query string:', error)
req.query = {}
req.path = '/'
}

// Parse request body based on method and content type
try {
if (['POST', 'PUT', 'PATCH'].includes(req.method) && bodyPart) {
const contentType = (req.headers['content-type'] || '').toLowerCase();
const contentType = (req.headers['content-type'] || '').toLowerCase()

if (contentType.includes('application/json')) {
try {
const bodyData = await HTTPbody(bodyPart, 0);
req.body = JSONbodyParser(bodyData) || {};
const bodyData = await HTTPbody(bodyPart, 0)
req.body = JSONbodyParser(bodyData) || {}
} catch (error) {
console.warn('Error parsing JSON body:', error);
req.body = {};
console.warn('Error parsing JSON body:', error)
req.body = {}
}
} else if (contentType.includes('application/x-www-form-urlencoded')) {
try {
req.body = queryParser(bodyPart);
req.body = queryParser(bodyPart)
} catch (error) {
console.warn('Error parsing form data:', error);
req.body = {};
console.warn('Error parsing form data:', error)
req.body = {}
}
} else {
// For other content types, keep as raw string
req.body = bodyPart;
req.body = bodyPart
}
} else if (req.method === 'OPTIONS') {
// Handle OPTIONS preflight request
req.body = {};
req.body = {}
req.cors = {
origin: req.headers['origin'],
origin: req.headers.origin,
method: req.headers['access-control-request-method'] || '*',
headers: req.headers['access-control-request-headers'] || ''
};
}
} else {
req.body = undefined;
req.body = undefined
}
} catch (error) {
console.warn('Error processing request body:', error);
req.body = {};
console.warn('Error processing request body:', error)
req.body = {}
}

return req;
return req
} catch (error) {
console.error('Error parsing HTTP request:', error);
console.error('Error parsing HTTP request:', error)
// Create a minimal valid request object even on error
return {
method: 'GET',
Expand All @@ -179,7 +179,7 @@ async function httpParser(request, connection = {}) {
query: {},
ip: connection.remoteAddress || '127.0.0.1',
body: undefined
};
}
}
}

Expand Down
Loading
Loading