diff --git a/doc/babel.config.js b/doc/babel.config.js index e00595d..14e5baf 100644 --- a/doc/babel.config.js +++ b/doc/babel.config.js @@ -1,3 +1,3 @@ module.exports = { - presets: [require.resolve('@docusaurus/core/lib/babel/preset')], -}; + presets: [require.resolve('@docusaurus/core/lib/babel/preset')] +} diff --git a/doc/docusaurus.config.js b/doc/docusaurus.config.js index f46f7d9..48336c0 100644 --- a/doc/docusaurus.config.js +++ b/doc/docusaurus.config.js @@ -1,5 +1,5 @@ // @ts-check -import { themes as prismThemes } from 'prism-react-renderer'; +import { themes as prismThemes } from 'prism-react-renderer' /** @type {import('@docusaurus/types').Config} */ const config = { @@ -18,7 +18,7 @@ const config = { i18n: { defaultLocale: 'en', - locales: ['en'], + locales: ['en'] }, presets: [ @@ -28,24 +28,24 @@ const config = { ({ docs: { sidebarPath: require.resolve('./sidebars.js'), - editUrl: 'https://github.com/IntegerAlex/hasty-server/tree/master/doc', + editUrl: 'https://github.com/IntegerAlex/hasty-server/tree/master/doc' }, blog: { showReadingTime: true, feedOptions: { type: ['rss', 'atom'], - xslt: true, + xslt: true }, editUrl: 'https://github.com/IntegerAlex/hasty-server.git', onInlineTags: 'warn', onInlineAuthors: 'warn', - onUntruncatedBlogPosts: 'warn', + onUntruncatedBlogPosts: 'warn' }, theme: { - customCss: require.resolve('./src/css/custom.css'), - }, - }), - ], + customCss: require.resolve('./src/css/custom.css') + } + }) + ] ], themeConfig: @@ -54,34 +54,34 @@ const config = { colorMode: { defaultMode: 'dark', disableSwitch: false, - respectPrefersColorScheme: true, + respectPrefersColorScheme: true }, announcementBar: { id: 'welcome', content: 'πŸš€ Welcome to Hasty Server Docs! Explore and build amazing web servers.', backgroundColor: '#4f46e5', textColor: '#ffffff', - isCloseable: true, + isCloseable: true }, navbar: { title: 'Hasty Server', logo: { alt: 'Hasty Server Logo', - src: 'img/logo.svg', + src: 'img/logo.svg' }, items: [ { type: 'docSidebar', sidebarId: 'docsSidebar', position: 'left', - label: 'Docs', + label: 'Docs' }, { href: 'https://github.com/IntegerAlex/hasty-server', label: 'GitHub', - position: 'right', - }, - ], + position: 'right' + } + ] }, footer: { style: 'dark', @@ -91,56 +91,56 @@ const config = { items: [ { label: 'Docs', - to: '/docs/intro', - }, - ], + to: '/docs/intro' + } + ] }, { title: 'Community', items: [ { label: 'GitHub', - href: 'https://github.com/IntegerAlex/hasty-server.git', - }, - ], + href: 'https://github.com/IntegerAlex/hasty-server.git' + } + ] }, { title: 'More', items: [ { label: 'GitHub', - href: 'https://github.com/IntegerAlex/hasty-server', - }, - ], - }, + href: 'https://github.com/IntegerAlex/hasty-server' + } + ] + } ], - copyright: `Copyright Β© ${new Date().getFullYear()} Hasty Server.`, + copyright: `Copyright Β© ${new Date().getFullYear()} Hasty Server.` }, prism: { theme: { ...prismThemes.github, plain: { ...prismThemes.github.plain, - backgroundColor: '#f8fafc', - }, + backgroundColor: '#f8fafc' + } }, darkTheme: { ...prismThemes.dracula, plain: { ...prismThemes.dracula.plain, - backgroundColor: '#1e293b', - }, + backgroundColor: '#1e293b' + } }, - additionalLanguages: ['http', 'bash', 'json'], + additionalLanguages: ['http', 'bash', 'json'] }, docs: { sidebar: { hideable: true, - autoCollapseCategories: true, - }, + autoCollapseCategories: true + } }, - image: 'img/social-card.png', - }), -}; + image: 'img/social-card.png' + }) +} -export default config; +export default config diff --git a/doc/sidebars.js b/doc/sidebars.js index 3710118..711aa66 100644 --- a/doc/sidebars.js +++ b/doc/sidebars.js @@ -1,6 +1,6 @@ /** * Sidebar configuration for Hasty Server documentation - * + * * This file defines the navigation structure for the documentation site. * The sidebar is organized into logical sections for easy navigation. */ @@ -18,10 +18,10 @@ const sidebars = { collapsible: true, collapsed: false, items: [ - 'getting-started/quick-start', - ], + 'getting-started/quick-start' + ] }, - + // Guides section { type: 'category', @@ -33,13 +33,13 @@ const sidebars = { 'guides/static-files', 'guides/error-handling', - 'guides/limitations', - ], + 'guides/limitations' + ] }, - + // Additional standalone pages - 'index', - ], -}; + 'index' + ] +} -export default sidebars; +export default sidebars diff --git a/doc/src/components/HomepageFeatures/index.js b/doc/src/components/HomepageFeatures/index.js index f7440b3..447f280 100644 --- a/doc/src/components/HomepageFeatures/index.js +++ b/doc/src/components/HomepageFeatures/index.js @@ -1,64 +1,64 @@ -import clsx from 'clsx'; -import Heading from '@theme/Heading'; -import styles from './styles.module.css'; +import clsx from 'clsx' +import Heading from '@theme/Heading' +import styles from './styles.module.css' const FeatureList = [ { title: 'High Performance', description: 'Hasty Server is designed for speed. It’s built using low-level networking APIs, ensuring minimal overhead and high concurrency. This makes it capable of handling thousands of requests per second with ease, perfect for high-traffic applications.', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default }, { title: 'Developer Friendly', description: 'It offers a simple and intuitive API, inspired by Express.js. Whether you’re a beginner or an experienced developer, the clear structure and easy-to-use syntax allow you to build applications faster and with fewer bugs.', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default }, { title: 'Built-in Security', description: 'Security is a top priority. Hasty Server includes built-in protections against common vulnerabilities such as XSS (Cross-Site Scripting), CSRF (Cross-Site Request Forgery), and SQL injection, helping you create secure applications without extra effort.', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default }, { title: 'Extensible Architecture', description: 'With its middleware-based architecture, Hasty Server allows you to add custom plugins and middlewares. You can extend its functionality by adding your own handlers or integrating third-party libraries with ease.', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default }, { title: 'Lightweight & Minimal', description: 'Despite its powerful features, Hasty Server remains lightweight and efficient. It minimizes resource consumption, making it ideal for resource-constrained environments such as microservices or serverless applications.', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default }, { title: 'Flexible Routing', description: 'Hasty Server provides a flexible routing system, allowing you to define routes for different HTTP methods (GET, POST, PUT, DELETE, etc.). You can also create dynamic routes with parameters for even more customization.', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, - }, -]; + Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default + } +] -function Feature({Svg, title, description}) { +function Feature ({ Svg, title, description }) { return (
-
- +
+
-
- {title} +
+ {title}

{description}

- ); + ) } -export default function HomepageFeatures() { +export default function HomepageFeatures () { return (
-
-
+
+
{FeatureList.map((props, idx) => ( ))}
- ); + ) } diff --git a/src/lib/cors.js b/src/lib/cors.js index 363cc1a..92f9c83 100644 --- a/src/lib/cors.js +++ b/src/lib/cors.js @@ -16,7 +16,7 @@ const DEFAULT_CORS_HEADERS = { 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', // 24 hours 'Access-Control-Allow-Credentials': 'true' -}; +} /** * Applies CORS headers to a response @@ -25,17 +25,17 @@ 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; +function applyCorsHeaders (response, enabled = true, customHeaders = {}) { + if (!enabled) return - const headers = { ...DEFAULT_CORS_HEADERS, ...customHeaders }; + const headers = { ...DEFAULT_CORS_HEADERS, ...customHeaders } Object.entries(headers).forEach(([key, value]) => { // Only set if explicitly provided in customHeaders or if not already set if (customHeaders[key] || !response.headers[key]) { - response.setHeader(key, value); + response.setHeader(key, value) } - }); + }) } /** @@ -45,9 +45,9 @@ 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, { @@ -55,14 +55,14 @@ function handlePreflight(request, response, enabled = true) { DEFAULT_CORS_HEADERS['Access-Control-Allow-Methods'], '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 -}; +} diff --git a/src/lib/httpParser.js b/src/lib/httpParser.js index 4348221..f213335 100644 --- a/src/lib/httpParser.js +++ b/src/lib/httpParser.js @@ -23,7 +23,7 @@ const { findFirstBrac, HTTPbody, JSONbodyParser, queryParser } = require('./util * @returns {Promise} 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 = { @@ -34,21 +34,21 @@ 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 @@ -56,120 +56,120 @@ async function httpParser(request, connection = {}) { // 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 line = (headers[i] || '').trim() + if (!line) continue - const colonIndex = line.indexOf(':'); - if (colonIndex <= 0) continue; // Skip malformed headers + 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 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 { - const originalPath = req.path || '/'; + const originalPath = req.path || '/' // Parse query parameters safely - const queryStart = originalPath.indexOf('?'); + const queryStart = originalPath.indexOf('?') if (queryStart !== -1) { - req.query = queryParser(originalPath); + req.query = queryParser(originalPath) // Clean path after extracting query - req.path = decodeURIComponent(originalPath.slice(0, queryStart)) || '/'; + req.path = decodeURIComponent(originalPath.slice(0, queryStart)) || '/' } else { - req.query = {}; - req.path = decodeURIComponent(originalPath) || '/'; + req.query = {} + req.path = decodeURIComponent(originalPath) || '/' } } 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 { - req.body = JSONbodyParser(bodyPart); + req.body = JSONbodyParser(bodyPart) } 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); - throw error; + console.error('Error parsing HTTP request:', error) + throw error } } diff --git a/src/lib/parser.js b/src/lib/parser.js index 93feb2c..82b6a64 100644 --- a/src/lib/parser.js +++ b/src/lib/parser.js @@ -18,15 +18,15 @@ const net = require('net') * * @param {string} req - The string to search within. * @param {string} target - The character to find in the string. - * @returns {number} The index of the first occurrence of the target character, + * @returns {number} The index of the first occurrence of the target character, * or -1 if the target character is not found. - * - * @example - * + * + * @example + * * const myString = "Hello, world!"; * const targetChar = "o"; * const index = findFirstBrac(myString, targetChar); - * + * * if (index !== -1) { * console.log(`The first occurrence of '${targetChar}' is at index ${index}.`); * } else { @@ -48,7 +48,7 @@ function findFirstBrac (req, target) { * * @param {string} request - The HTTP request string to parse. * @returns {Promise} A promise that resolves to an object containing the HTTP method, path, version, and body (if applicable). - * + * * @example * // Example HTTP request string const httpRequest = `POST /api/data HTTP/1.1 @@ -67,7 +67,7 @@ httpParser(httpRequest).then(parsedRequest => { }).catch(error => { console.error('Error parsing HTTP request:', error); }); -* +* */ async function httpParser (request) { const req = new Object() @@ -98,13 +98,13 @@ httpParser(request).then((data) => { }) -/** +/** * Stores a key-value pair from a request string into a JSON object. * * @param {Array} req - The request string split into an array of characters. * @param {Object} httpJSON - The JSON object to store the key-value pair. * @returns {Array} The modified request array after extracting the key-value pair. - * + * * @example * // Example request string const requestString = "key1:value1,key2:value2"; @@ -151,7 +151,7 @@ function storePair (req, httpJSON) { * * @param {string} body - The JSON body string to parse. * @returns {Object} The parsed JSON object. - * + * * @example * const jsonString = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; const parsedObject = JSONbodyParser(jsonString); @@ -190,7 +190,7 @@ function JSONbodyParser (body) { * @param {Array} req - The request string split into an array of lines. * @param {number} pos - The position to start extracting the body. * @returns {Promise} A promise that resolves to the extracted body string. - * + * * @example * const httpRequest = `POST /api/data HTTP/1.1 Host: example.com @@ -233,7 +233,7 @@ function HTTPbody (req, pos) { * Parses a query string from a URL and extracts its components. * * @param {string} request - The URL containing the query string. - * + * * @example const url = 'https://example.com?name=JohnDoe&age=25&city=NewYork'; const parsedQuery = queryParser(url); @@ -253,7 +253,7 @@ function queryParser (request) { * * @param {string} query - The query string to parse. * @returns {Object} The parsed query string as a JSON object. - * + * * @example * // Example usage const queryString = "key1=value1&key2=value2"; @@ -286,8 +286,8 @@ function storeQuery (query) { * * @param {Array} req - The query string split into an array of characters. * @returns {Object} The JSON object containing the key-value pair. - * - * @example + * + * @example * const queryString = "key1=value1&key2=value2"; const queryArray = queryString.split(''); let queryJSON = {}; diff --git a/src/lib/utils.js b/src/lib/utils.js index 27634e3..9e25798 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,14 +1,13 @@ - /** * Finds the index of the first occurrence of the target character in the given string. * @param {string} req - The string to search through. * @param {string} target - The character to find in the string. * @returns {number} The index of the target character or -1 if not found. - * + * * @example * const index = findFirstBrac('Hello, World!', 'o'); */ -function findFirstBrac(req, target) { +function findFirstBrac (req, target) { for (let i = 0; i < req.length; i++) { if (req[i] === target) { return i @@ -22,11 +21,11 @@ function findFirstBrac(req, target) { * @param {string} req - The HTTP request as a string. * @param {number} pos - The position in the string to start parsing. * @returns {Promise} A promise that resolves to the cleaned-up body. - * + * * @example * const body = await HTTPbody(req, pos); */ -function HTTPbody(req, pos) { +function HTTPbody (req, pos) { let flag = 0 let body = '' return new Promise((resolve, reject) => { @@ -53,11 +52,11 @@ function HTTPbody(req, pos) { * Cleans up the body content by trimming spaces and standardizing spacing around colons and commas. * @param {string} body - The body content to clean up. * @returns {string} body - The cleaned-up body. - * + * * @example * const cleanedBody = cleanUpBody(body); */ -function cleanUpBody(body) { +function cleanUpBody (body) { // Trim leading and trailing spaces body = body.trim() @@ -74,119 +73,118 @@ function cleanUpBody(body) { * Parses a JSON-like HTTP body into an object. * @param {string} body - The HTTP body content as a string. * @returns {Object} The parsed JSON object. - * + * * @example * const parsedBody = JSONbodyParser(body); */ -function JSONbodyParser(body) { - const req = body.split(''); +function JSONbodyParser (body) { + const req = body.split('') // Helper to skip whitespace - function skipWhitespace() { + function skipWhitespace () { while (req.length > 0 && /\s/.test(req[0])) { - req.shift(); + req.shift() } } // Recursive parser - function parse() { - skipWhitespace(); - if (req.length === 0) return undefined; + function parse () { + skipWhitespace() + if (req.length === 0) return undefined - const char = req[0]; + const char = req[0] if (char === '{') { - req.shift(); // consume '{' - const obj = {}; + req.shift() // consume '{' + const obj = {} while (req.length > 0) { - skipWhitespace(); + skipWhitespace() if (req[0] === '}') { - req.shift(); // consume '}' - return obj; + req.shift() // consume '}' + return obj } // Parse key - let key = ''; + let key = '' if (req[0] === '"') { - req.shift(); // consume '"' + req.shift() // consume '"' while (req.length > 0 && req[0] !== '"') { - key += req.shift(); + key += req.shift() } - req.shift(); // consume closing '"' + req.shift() // consume closing '"' } else { // Allow unquoted keys (non-standard but supported by original parser logic) while (req.length > 0 && req[0] !== ':' && req[0] !== ' ') { - key += req.shift(); + key += req.shift() } } - skipWhitespace(); - if (req[0] === ':') req.shift(); // consume ':' + skipWhitespace() + if (req[0] === ':') req.shift() // consume ':' - const value = parse(); - obj[key] = value; + const value = parse() + obj[key] = value - skipWhitespace(); - if (req[0] === ',') req.shift(); // consume ',' + skipWhitespace() + if (req[0] === ',') req.shift() // consume ',' } } else if (char === '[') { - req.shift(); // consume '[' - const arr = []; + req.shift() // consume '[' + const arr = [] while (req.length > 0) { - skipWhitespace(); + skipWhitespace() if (req[0] === ']') { - req.shift(); // consume ']' - return arr; + req.shift() // consume ']' + return arr } - const value = parse(); - arr.push(value); + const value = parse() + arr.push(value) - skipWhitespace(); - if (req[0] === ',') req.shift(); // consume ',' + skipWhitespace() + if (req[0] === ',') req.shift() // consume ',' } } else if (char === '"') { - req.shift(); // consume '"' - let str = ''; + req.shift() // consume '"' + let str = '' while (req.length > 0 && req[0] !== '"') { - str += req.shift(); + str += req.shift() } - req.shift(); // consume closing '"' - return str; + req.shift() // consume closing '"' + return str } else { // Number, boolean, null - let val = ''; + let val = '' while (req.length > 0 && req[0] !== ',' && req[0] !== '}' && req[0] !== ']') { - val += req.shift(); + val += req.shift() } - val = val.trim(); - if (val === 'true') return true; - if (val === 'false') return false; - if (val === 'null') return null; - const num = Number(val); - return isNaN(num) ? val : num; + val = val.trim() + if (val === 'true') return true + if (val === 'false') return false + if (val === 'null') return null + const num = Number(val) + return isNaN(num) ? val : num } } try { - return parse() || {}; + return parse() || {} } catch (e) { - console.error('JSON Parse Error:', e); - return {}; + console.error('JSON Parse Error:', e) + return {} } } - /** * Stores key-value pairs in the provided JSON object. * @param {Array} req - The remaining request characters. * @param {Object} httpJSON - The JSON object to store the parsed data. * @returns {Array} The remaining unprocessed request characters. - * - * @example + * + * @example * storePair(req, httpJSON); */ -function storePair(req, httpJSON) { +function storePair (req, httpJSON) { let key = '' let value = '' @@ -224,17 +222,16 @@ function storePair(req, httpJSON) { return req } - /** * Parses primitive values from the request array. * @param {Array} req - The remaining request characters. * @returns {string|number} The parsed value, either as a string or number. - * - * @example + * + * @example * const parsedValue = parseValue(req); */ // Helper function to parse primitive values (strings, numbers, etc.) -function parseValue(req) { +function parseValue (req) { let value = '' let isString = false @@ -274,11 +271,11 @@ function parseValue(req) { * Parses a query string from a request URL into a JSON object. * @param {string} request - The request URL as a string. * @returns {Object} The parsed query parameters as a JSON object. - * - * @example + * + * @example * const queryParams = queryParser(request); */ -function queryParser(request) { +function queryParser (request) { const httpQueryJSON = new Object() const queryStart = request.indexOf('?') @@ -300,16 +297,15 @@ function queryParser(request) { const mimeDb = require('./mimeDb') // Adjust the path as needed - /** * Looks up the MIME type based on the file extension. * @param {string} extension - The file extension to look up. * @returns {string} The MIME type or 'application/octet-stream' if not found. - * + * * @example * const mimeType = lookupMimeType('application/json'); */ -function lookupMimeType(extension) { +function lookupMimeType (extension) { const mimeType = Object.keys(mimeDb).find(type => mimeDb[type].extensions.includes(extension) ) diff --git a/src/server/index.js b/src/server/index.js index e455225..a91bd4b 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,9 +1,9 @@ -const { httpParser } = require('../lib/httpParser.js'); -const net = require('net'); -const path = require('path'); -const { URL } = require('url'); -const Response = require('./response.js'); -const { handlePreflight } = require('../lib/cors'); +const { httpParser } = require('../lib/httpParser.js') +const net = require('net') +const path = require('path') +const { URL } = require('url') +const Response = require('./response.js') +const { handlePreflight } = require('../lib/cors') /** * @typedef {Object} Route @@ -19,27 +19,27 @@ const { handlePreflight } = require('../lib/cors'); * @param {Object} context - Server context object * @returns {net.Server} - Node.js Server instance */ -function createServer(callback, context) { +function createServer (callback, context) { return net.createServer((socket) => { // Set socket timeout to prevent hanging connections - socket.setTimeout(30000); // 30 seconds + socket.setTimeout(30000) // 30 seconds // Handle connection errors socket.on('error', (error) => { - console.error('Socket error:', error); + console.error('Socket error:', error) if (!socket.destroyed) { - socket.destroy(); + socket.destroy() } - }); + }) // Handle timeout socket.on('timeout', () => { - console.warn('Socket timeout'); - socket.end('HTTP/1.1 408 Request Timeout\r\n\r\n'); - }); + console.warn('Socket timeout') + socket.end('HTTP/1.1 408 Request Timeout\r\n\r\n') + }) - callback(socket, context); - }); + callback(socket, context) + }) } /** @@ -52,56 +52,56 @@ function createServer(callback, context) { * @param {net.Socket} socket - The socket connection * @param {Object} context - Server context with routes and configuration */ -function handleConnection(socket, context) { - let buffer = Buffer.alloc(0); +function handleConnection (socket, context) { + let buffer = Buffer.alloc(0) socket.on('data', (chunk) => { - buffer = Buffer.concat([buffer, chunk]); + buffer = Buffer.concat([buffer, chunk]) // Process as many requests as possible from the buffer while (buffer.length > 0) { // 1. Check for header end - const headerEndIndex = buffer.indexOf('\r\n\r\n'); + const headerEndIndex = buffer.indexOf('\r\n\r\n') if (headerEndIndex === -1) { // Headers not fully received yet - break; + break } // 2. Parse headers to find Content-Length - const headerPart = buffer.slice(0, headerEndIndex).toString(); - const contentLengthMatch = headerPart.match(/Content-Length:\s*(\d+)/i); - const contentLength = contentLengthMatch ? parseInt(contentLengthMatch[1], 10) : 0; + const headerPart = buffer.slice(0, headerEndIndex).toString() + const contentLengthMatch = headerPart.match(/Content-Length:\s*(\d+)/i) + const contentLength = contentLengthMatch ? parseInt(contentLengthMatch[1], 10) : 0 // 3. Check if we have the full body - const totalRequestLength = headerEndIndex + 4 + contentLength; + const totalRequestLength = headerEndIndex + 4 + contentLength if (buffer.length < totalRequestLength) { // Body not fully received yet - break; + break } // 4. Extract the full request - const requestData = buffer.slice(0, totalRequestLength); + const requestData = buffer.slice(0, totalRequestLength) // 5. Remove processed data from buffer - buffer = buffer.slice(totalRequestLength); + buffer = buffer.slice(totalRequestLength) // 6. Process the request processRequest(socket, requestData, context) .catch(error => { - console.error('Error processing request:', error); + console.error('Error processing request:', error) // Only send error if socket is still open and writable if (socket.writable) { - const res = new Response(socket, context.enableCors); - res.status(500).send('Internal Server Error'); + const res = new Response(socket, context.enableCors) + res.status(500).send('Internal Server Error') } - }); + }) } - }); + }) socket.on('error', (error) => { // console.error('Connection error:', error); - }); + }) } /** @@ -111,67 +111,66 @@ function handleConnection(socket, context) { * @param {Object} context - Server context * @returns {Promise} */ -async function processRequest(socket, requestData, context) { +async function processRequest (socket, requestData, context) { // Parse the HTTP request first to check headers - let req; + let req try { - req = await httpParser(requestData.toString()); + req = await httpParser(requestData.toString()) } catch (error) { - console.error('Request parsing error:', error); + console.error('Request parsing error:', error) if (socket.writable) { - const res = new Response(socket, context.enableCors); - res.status(400).send('Bad Request'); + const res = new Response(socket, context.enableCors) + res.status(400).send('Bad Request') } - return; + return } // Determine if we should keep the connection alive // Default to true for HTTP/1.1, false for HTTP/1.0 unless Keep-Alive header is present - const connectionHeader = (req.headers['connection'] || '').toLowerCase(); - const isHttp11 = req.version === 'HTTP/1.1'; - let shouldKeepAlive = isHttp11; + const connectionHeader = (req.headers.connection || '').toLowerCase() + const isHttp11 = req.version === 'HTTP/1.1' + let shouldKeepAlive = isHttp11 if (connectionHeader === 'close') { - shouldKeepAlive = false; + shouldKeepAlive = false } else if (connectionHeader === 'keep-alive') { - shouldKeepAlive = true; + shouldKeepAlive = true } - const res = new Response(socket, context.enableCors, shouldKeepAlive, req.method); + const res = new Response(socket, context.enableCors, shouldKeepAlive, req.method) try { // Handle CORS preflight requests if (req.method === 'OPTIONS' && context.enableCors) { - handlePreflight(req, res); - return; + handlePreflight(req, res) + return } // Find matching route - const route = findMatchingRoute(req.method, req.path, context.routes); + const route = findMatchingRoute(req.method, req.path, context.routes) if (!route) { - res.status(404).send('Not Found'); - return; + res.status(404).send('Not Found') + return } // Extract URL parameters - req.params = extractParams(route.path, req.path); + req.params = extractParams(route.path, req.path) // Parse query parameters if (req.path.includes('?')) { - const url = new URL(`http://dummy${req.path}`); - req.query = Object.fromEntries(url.searchParams.entries()); + const url = new URL(`http://dummy${req.path}`) + req.query = Object.fromEntries(url.searchParams.entries()) } else { - req.query = {}; + req.query = {} } // Execute route handler - await route.callback(req, res); - + await route.callback(req, res) } catch (error) { - console.error('Request processing error:', error); + console.error('Request processing error:', error) if (!res.headersSent) { - res.status(500).send('Internal Server Error'); + res.status(500).send('Internal Server Error') } } } @@ -183,18 +182,18 @@ async function processRequest(socket, requestData, context) { * @param {Array} routes - Available routes * @returns {Route|undefined} - Matched route or undefined */ -function findMatchingRoute(method, path, routes) { +function findMatchingRoute (method, path, routes) { // First try exact match const exactMatch = routes.find( route => route.method === method && route.path === path - ); + ) - if (exactMatch) return exactMatch; + if (exactMatch) return exactMatch // Then try parameterized routes return routes.find(route => route.method === method && matchRouteWithParams(route.path, path) - ); + ) } /** @@ -203,15 +202,15 @@ function findMatchingRoute(method, path, routes) { * @param {string} urlPath - Actual URL path * @returns {boolean} - True if the route matches */ -function matchRouteWithParams(routePath, urlPath) { - const routeParts = routePath.split('/'); - const pathParts = urlPath.split('?')[0].split('/'); +function matchRouteWithParams (routePath, urlPath) { + const routeParts = routePath.split('/') + const pathParts = urlPath.split('?')[0].split('/') - if (routeParts.length !== pathParts.length) return false; + if (routeParts.length !== pathParts.length) return false return routeParts.every((part, i) => part.startsWith(':') || part === pathParts[i] - ); + ) } /** @@ -220,18 +219,18 @@ function matchRouteWithParams(routePath, urlPath) { * @param {string} urlPath - Actual URL path (e.g., '/users/123') * @returns {Object} - Extracted parameters */ -function extractParams(routePath, urlPath) { - const params = {}; - const routeParts = routePath.split('/'); - const pathParts = urlPath.split('?')[0].split('/'); +function extractParams (routePath, urlPath) { + const params = {} + const routeParts = routePath.split('/') + const pathParts = urlPath.split('?')[0].split('/') routeParts.forEach((part, i) => { if (part.startsWith(':')) { - params[part.slice(1)] = decodeURIComponent(pathParts[i]); + params[part.slice(1)] = decodeURIComponent(pathParts[i]) } - }); + }) - return params; + return params } /** @@ -243,16 +242,16 @@ class Server { * Creates a new Server instance * @constructor */ - constructor() { + constructor () { /** @private */ - this.routes = []; + this.routes = [] /** @private */ - this.server = null; + this.server = null // Bind methods - this.listen = this.listen.bind(this); - this.close = this.close.bind(this); + this.listen = this.listen.bind(this) + this.close = this.close.bind(this) } /** @@ -261,47 +260,46 @@ class Server { * @param {Function} [callback] - Callback function when server starts * @returns {Promise} */ - listen(port, callback) { + listen (port, callback) { return new Promise((resolve, reject) => { try { this.server = createServer(handleConnection, { routes: this.routes, enableCors: this.enableCors || false - }); + }) this.server.on('error', (error) => { - console.error('Server error:', error); - reject(error); - }); + console.error('Server error:', error) + reject(error) + }) this.server.listen(port, '0.0.0.0', () => { - console.log(`Server listening on port ${port}`); - if (callback) callback(); - resolve(); - }); - + console.log(`Server listening on port ${port}`) + if (callback) callback() + resolve() + }) } catch (error) { - console.error('Failed to start server:', error); - reject(error); + console.error('Failed to start server:', error) + reject(error) } - }); + }) } /** * Stops the server * @returns {Promise} */ - close() { + close () { return new Promise((resolve) => { if (this.server) { this.server.close(() => { - console.log('Server stopped'); - resolve(); - }); + console.log('Server stopped') + resolve() + }) } else { - resolve(); + resolve() } - }); + }) } } @@ -314,14 +312,14 @@ class Hasty extends Server { * Creates a new Hasty server instance * @constructor */ - constructor() { - super(); + constructor () { + super() /** @private */ - this.enableCors = false; + this.enableCors = false // Bind methods - this.cors = this.cors.bind(this); + this.cors = this.cors.bind(this) } /** @@ -329,9 +327,9 @@ class Hasty extends Server { * @param {boolean} [enabled=true] - Whether to enable CORS * @returns {Hasty} - The server instance for chaining */ - cors(enabled = true) { - this.enableCors = enabled; - return this; + cors (enabled = true) { + this.enableCors = enabled + return this } /** @@ -342,15 +340,15 @@ class Hasty extends Server { * @param {Function} callback - Route handler * @returns {void} */ - _registerRoute(method, path, callback) { + _registerRoute (method, path, callback) { // Normalize path - ensure it starts with a slash but don't modify parameter routes - let normalizedPath = path; + let normalizedPath = path if (!normalizedPath.startsWith('/')) { - normalizedPath = '/' + normalizedPath; + normalizedPath = '/' + normalizedPath } // Remove trailing slash unless it's the root path if (normalizedPath !== '/' && normalizedPath.endsWith('/')) { - normalizedPath = normalizedPath.slice(0, -1); + normalizedPath = normalizedPath.slice(0, -1) } this.routes.push({ @@ -358,20 +356,20 @@ class Hasty extends Server { path: normalizedPath, callback: async (req, res) => { try { - await callback(req, res); + await callback(req, res) // If headers haven't been sent and the response hasn't been ended if (!res.headersSent && !res.finished) { - res.end(); + res.end() } } catch (error) { - console.error('Route handler error:', error); + console.error('Route handler error:', error) if (!res.headersSent) { - res.status(500).send('Internal Server Error'); + res.status(500).send('Internal Server Error') } } } - }); + }) } // HTTP method shortcuts @@ -382,8 +380,8 @@ class Hasty extends Server { * @param {Function} callback - Route handler * @returns {void} */ - get(path, callback) { - this._registerRoute('GET', path, callback); + get (path, callback) { + this._registerRoute('GET', path, callback) } /** @@ -392,8 +390,8 @@ class Hasty extends Server { * @param {Function} callback - Route handler * @returns {void} */ - post(path, callback) { - this._registerRoute('POST', path, callback); + post (path, callback) { + this._registerRoute('POST', path, callback) } /** @@ -402,8 +400,8 @@ class Hasty extends Server { * @param {Function} callback - Route handler * @returns {void} */ - put(path, callback) { - this._registerRoute('PUT', path, callback); + put (path, callback) { + this._registerRoute('PUT', path, callback) } /** @@ -412,8 +410,8 @@ class Hasty extends Server { * @param {Function} callback - Route handler * @returns {void} */ - delete(path, callback) { - this._registerRoute('DELETE', path, callback); + delete (path, callback) { + this._registerRoute('DELETE', path, callback) } /** @@ -422,8 +420,8 @@ class Hasty extends Server { * @param {Function} callback - Route handler * @returns {void} */ - patch(path, callback) { - this._registerRoute('PATCH', path, callback); + patch (path, callback) { + this._registerRoute('PATCH', path, callback) } /** @@ -432,8 +430,8 @@ class Hasty extends Server { * @param {Function} callback - Route handler * @returns {void} */ - head(path, callback) { - this._registerRoute('HEAD', path, callback); + head (path, callback) { + this._registerRoute('HEAD', path, callback) } /** @@ -442,18 +440,18 @@ class Hasty extends Server { * @param {Function} [callback] - Optional route handler * @returns {void} */ - options(path, callback) { + options (path, callback) { if (callback) { - this._registerRoute('OPTIONS', path, callback); + this._registerRoute('OPTIONS', path, callback) } else { // Auto-handle OPTIONS for CORS this._registerRoute('OPTIONS', path, (req, res) => { if (this.enableCors) { - handlePreflight(req, res); + handlePreflight(req, res) } else { - res.status(200).end(); + res.status(200).end() } - }); + }) } } @@ -464,25 +462,25 @@ class Hasty extends Server { * @param {string} [options.prefix='/'] - URL prefix for the static files * @returns {void} */ - static(root, options = {}) { - const { prefix = '/' } = options; - const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`; + static (root, options = {}) { + const { prefix = '/' } = options + const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/` this.get(`${normalizedPrefix}*`, (req, res) => { // Remove the prefix and any leading slashes to get the relative path - const relativePath = req.path.slice(normalizedPrefix.length).replace(/^\/+/, ''); + const relativePath = req.path.slice(normalizedPrefix.length).replace(/^\/+/, '') // Prevent directory traversal if (relativePath.includes('../') || relativePath.includes('..\\')) { - return res.status(403).send('Forbidden'); + return res.status(403).send('Forbidden') } // Join with the root directory and normalize the path - const filePath = path.join(root, relativePath || 'index.html'); + const filePath = path.join(root, relativePath || 'index.html') // Send the file - res.sendFile(filePath); - }); + res.sendFile(filePath) + }) } } diff --git a/src/server/response.js b/src/server/response.js index e5de25d..ce8c2a4 100644 --- a/src/server/response.js +++ b/src/server/response.js @@ -1,7 +1,7 @@ -const fs = require('fs'); -const path = require('path'); -const { lookupMimeType } = require('../lib/utils'); -const { applyCorsHeaders } = require('../lib/cors'); +const fs = require('fs') +const path = require('path') +const { lookupMimeType } = require('../lib/utils') +const { applyCorsHeaders } = require('../lib/cors') /** * @typedef {Object} ResponseOptions @@ -16,7 +16,6 @@ const { applyCorsHeaders } = require('../lib/cors'); * @property {boolean} [end=true] - Whether to end the response after sending */ - /** @type {Object.} */ const STATUS_CODES = Object.freeze({ 100: 'Continue', @@ -81,8 +80,7 @@ const STATUS_CODES = Object.freeze({ 508: 'Loop Detected', 510: 'Not Extended', 511: 'Network Authentication Required' -}); - +}) /** * Response class for handling HTTP responses. @@ -97,22 +95,22 @@ const STATUS_CODES = Object.freeze({ */ class Response { /** @type {number} */ - statusCode = 200; + statusCode = 200 /** @type {boolean} */ - enableCors = false; + enableCors = false /** @type {Object.} */ - headers = {}; + headers = {} /** @type {boolean} */ - headersSent = false; + headersSent = false /** @type {import('net').Socket} */ - socket; + socket /** @type {boolean} */ - shouldKeepAlive = false; + shouldKeepAlive = false /** * Create a new Response instance @@ -121,20 +119,20 @@ class Response { * @param {boolean} [shouldKeepAlive=false] - Whether to keep the connection alive * @param {string} [method='GET'] - The HTTP method of the request */ - constructor(socket, enableCors = false, shouldKeepAlive = false, method = 'GET') { + constructor (socket, enableCors = false, shouldKeepAlive = false, method = 'GET') { if (!socket || typeof socket.write !== 'function') { - throw new Error('Socket is required and must be a writable stream'); + throw new Error('Socket is required and must be a writable stream') } - this.socket = socket; - this.enableCors = Boolean(enableCors); - this.shouldKeepAlive = Boolean(shouldKeepAlive); - this.method = method.toUpperCase(); + this.socket = socket + this.enableCors = Boolean(enableCors) + this.shouldKeepAlive = Boolean(shouldKeepAlive) + this.method = method.toUpperCase() this.headers = { 'Content-Type': 'text/plain', 'X-Powered-By': 'Hasty-Server', - 'Connection': this.shouldKeepAlive ? 'keep-alive' : 'close' - }; + Connection: this.shouldKeepAlive ? 'keep-alive' : 'close' + } } // ... (methods omitted for brevity) @@ -145,12 +143,12 @@ class Response { * @returns {Response} The Response instance for chaining * @throws {Error} If status code is invalid */ - status(code) { + status (code) { if (!Number.isInteger(code) || code < 100 || code > 599) { - throw new Error(`Invalid status code: ${code}`); + throw new Error(`Invalid status code: ${code}`) } - this.statusCode = code; - return this; + this.statusCode = code + return this } /** @@ -159,19 +157,19 @@ class Response { * @param {string|number|string[]} value - Header value(s) * @returns {Response} The Response instance for chaining */ - setHeader(key, value) { + setHeader (key, value) { if (this.headersSent) { - console.warn('Cannot set header after headers have been sent'); - return this; + console.warn('Cannot set header after headers have been sent') + return this } if (Array.isArray(value)) { - this.headers[key] = value.join(', '); + this.headers[key] = value.join(', ') } else { - this.headers[key] = String(value); + this.headers[key] = String(value) } - return this; + return this } /** @@ -179,16 +177,16 @@ class Response { * @private * @returns {void} */ - _applyCorsHeaders() { + _applyCorsHeaders () { if (this.enableCors) { - applyCorsHeaders(this, true); + applyCorsHeaders(this, true) } } /** * It formats the headers into a string. */ - formatHeaders() { + formatHeaders () { return Object.keys(this.headers) .map(key => `${key}: ${this.headers[key]}`) .join('\r\n') @@ -200,75 +198,75 @@ class Response { * @param {SendOptions} [options] - Additional options * @returns {void} */ - send(data, options = {}) { + send (data, options = {}) { if (this.headersSent) { - console.warn('Headers already sent'); - return; + console.warn('Headers already sent') + return } - const { contentType, end = true } = options; + const { contentType, end = true } = options // Set content type if provided or default to text/plain if (contentType) { - this.setHeader('Content-Type', contentType); + this.setHeader('Content-Type', contentType) } else if (!this.headers['Content-Type']) { - this.setHeader('Content-Type', 'text/plain; charset=utf-8'); + this.setHeader('Content-Type', 'text/plain; charset=utf-8') } // Apply CORS headers - this._applyCorsHeaders(); + this._applyCorsHeaders() // Handle different data types - let responseData = data; + let responseData = data if (data !== undefined && data !== null) { if (typeof data === 'object' && !Buffer.isBuffer(data)) { try { - responseData = JSON.stringify(data); + responseData = JSON.stringify(data) if (!contentType) { - this.setHeader('Content-Type', 'application/json; charset=utf-8'); + this.setHeader('Content-Type', 'application/json; charset=utf-8') } } catch (error) { - console.error('Error stringifying JSON:', error); - return this.status(500).send('Error generating response'); + console.error('Error stringifying JSON:', error) + return this.status(500).send('Error generating response') } } else if (Buffer.isBuffer(data)) { if (!contentType) { - this.setHeader('Content-Type', 'application/octet-stream'); + this.setHeader('Content-Type', 'application/octet-stream') } } else { - responseData = String(data); + responseData = String(data) } } else { - responseData = ''; + responseData = '' } // Set Content-Length header - const contentLength = responseData ? Buffer.byteLength(responseData) : 0; - this.setHeader('Content-Length', contentLength); + const contentLength = responseData ? Buffer.byteLength(responseData) : 0 + this.setHeader('Content-Length', contentLength) // Build response - const statusText = STATUS_CODES[this.statusCode] || 'Unknown Status'; + const statusText = STATUS_CODES[this.statusCode] || 'Unknown Status' const headers = Object.entries(this.headers) .map(([key, value]) => `${key}: ${value}`) - .join('\r\n'); + .join('\r\n') // Send response this.socket.write( `HTTP/1.1 ${this.statusCode} ${statusText}\r\n` + `${headers}\r\n\r\n` - ); + ) // Send response body if present AND not HEAD request if (contentLength > 0 && this.method !== 'HEAD') { - this.socket.write(responseData); + this.socket.write(responseData) } - this.headersSent = true; + this.headersSent = true // End the connection if requested if (end) { if (!this.shouldKeepAlive) { - this.socket.end(); + this.socket.end() } } } @@ -279,11 +277,11 @@ class Response { * @param {number} [status] - Optional status code * @returns {void} */ - json(data, status) { + json (data, status) { if (status !== undefined) { - this.status(status); + this.status(status) } - this.send(data, { contentType: 'application/json; charset=utf-8' }); + this.send(data, { contentType: 'application/json; charset=utf-8' }) } /** @@ -295,80 +293,79 @@ class Response { * @param {boolean} [options.end=true] - Whether to end the response * @returns {void} */ - download(filePath, filename, options = {}) { + download (filePath, filename, options = {}) { if (this.headersSent) { - console.warn('Headers already sent'); - return; + console.warn('Headers already sent') + return } - const { contentType, end = true } = options; + const { contentType, end = true } = options try { - const stats = fs.statSync(filePath); + const stats = fs.statSync(filePath) // Set content disposition for download - const contentDisposition = `attachment${filename ? `; filename="${filename}"` : ''}`; - this.setHeader('Content-Disposition', contentDisposition); + const contentDisposition = `attachment${filename ? `; filename="${filename}"` : ''}` + this.setHeader('Content-Disposition', contentDisposition) // Set content type if (contentType) { - this.setHeader('Content-Type', contentType); + this.setHeader('Content-Type', contentType) } else { - const mimeType = lookupMimeType(path.extname(filePath).substring(1)) || 'application/octet-stream'; - this.setHeader('Content-Type', mimeType); + const mimeType = lookupMimeType(path.extname(filePath).substring(1)) || 'application/octet-stream' + this.setHeader('Content-Type', mimeType) } // Set content length - this.setHeader('Content-Length', stats.size); + this.setHeader('Content-Length', stats.size) // Apply CORS headers - this._applyCorsHeaders(); + this._applyCorsHeaders() // Send headers - const statusText = STATUS_CODES[this.statusCode] || 'OK'; + const statusText = STATUS_CODES[this.statusCode] || 'OK' const headers = Object.entries(this.headers) .map(([key, value]) => `${key}: ${value}`) - .join('\r\n'); + .join('\r\n') this.socket.write( `HTTP/1.1 ${this.statusCode} ${statusText}\r\n` + `${headers}\r\n\r\n` - ); + ) - this.headersSent = true; + this.headersSent = true // Stream the file - const fileStream = fs.createReadStream(filePath); - fileStream.pipe(this.socket, { end }); + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(this.socket, { end }) // Handle stream errors fileStream.on('error', (error) => { - console.error('Error streaming file:', error); + console.error('Error streaming file:', error) if (!this.headersSent) { - this.status(500).send('Error streaming file'); + this.status(500).send('Error streaming file') } else { - this.socket.end(); + this.socket.end() } - }); + }) if (end) { fileStream.on('end', () => { if (!this.shouldKeepAlive) { - this.socket.end(); + this.socket.end() } - }); + }) } - } catch (error) { - console.error('Error serving file:', error); + console.error('Error serving file:', error) if (!this.headersSent) { if (error.code === 'ENOENT') { - this.status(404).send('File not found'); + this.status(404).send('File not found') } else { - this.status(500).send('Error serving file'); + this.status(500).send('Error serving file') } } else { - this.socket.end(); + this.socket.end() } } } @@ -381,76 +378,75 @@ class Response { * @param {boolean} [options.end=true] - Whether to end the response * @returns {void} */ - sendFile(filePath, options = {}) { + sendFile (filePath, options = {}) { if (this.headersSent) { - console.warn('Headers already sent'); - return; + console.warn('Headers already sent') + return } - const { contentType, end = true } = options; + const { contentType, end = true } = options try { - const stats = fs.statSync(filePath); + const stats = fs.statSync(filePath) // Set content type based on file extension if not provided if (contentType) { - this.setHeader('Content-Type', contentType); + this.setHeader('Content-Type', contentType) } else { - const mimeType = lookupMimeType(path.extname(filePath).substring(1)) || 'application/octet-stream'; - this.setHeader('Content-Type', mimeType); + const mimeType = lookupMimeType(path.extname(filePath).substring(1)) || 'application/octet-stream' + this.setHeader('Content-Type', mimeType) } // Set content length - this.setHeader('Content-Length', stats.size); + this.setHeader('Content-Length', stats.size) // Apply CORS headers - this._applyCorsHeaders(); + this._applyCorsHeaders() // Send headers - const statusText = STATUS_CODES[this.statusCode] || 'OK'; + const statusText = STATUS_CODES[this.statusCode] || 'OK' const headers = Object.entries(this.headers) .map(([key, value]) => `${key}: ${value}`) - .join('\r\n'); + .join('\r\n') this.socket.write( `HTTP/1.1 ${this.statusCode} ${statusText}\r\n` + `${headers}\r\n\r\n` - ); + ) - this.headersSent = true; + this.headersSent = true // Stream the file - const fileStream = fs.createReadStream(filePath); - fileStream.pipe(this.socket, { end }); + const fileStream = fs.createReadStream(filePath) + fileStream.pipe(this.socket, { end }) // Handle stream errors fileStream.on('error', (error) => { - console.error('Error streaming file:', error); + console.error('Error streaming file:', error) if (!this.headersSent) { - this.status(500).send('Error streaming file'); + this.status(500).send('Error streaming file') } else { - this.socket.end(); + this.socket.end() } - }); + }) if (end) { fileStream.on('end', () => { if (!this.shouldKeepAlive) { - this.socket.end(); + this.socket.end() } - }); + }) } - } catch (error) { - console.error('Error serving file:', error); + console.error('Error serving file:', error) if (!this.headersSent) { if (error.code === 'ENOENT') { - this.status(404).send('File not found'); + this.status(404).send('File not found') } else { - this.status(500).send('Error serving file'); + this.status(500).send('Error serving file') } } else { - this.socket.end(); + this.socket.end() } } } @@ -461,10 +457,10 @@ class Response { * @param {string} [message] - Optional message (defaults to status text) * @returns {void} */ - sendStatus(statusCode, message) { - this.status(statusCode); - const statusText = message || STATUS_CODES[statusCode] || 'Unknown Status'; - this.send(statusText); + sendStatus (statusCode, message) { + this.status(statusCode) + const statusText = message || STATUS_CODES[statusCode] || 'Unknown Status' + this.send(statusText) } /** @@ -472,36 +468,36 @@ class Response { * @param {string|Buffer} [data] - Optional data to send before ending * @returns {void} */ - end(data) { + end (data) { if (data !== undefined) { - this.send(data, { end: true }); + this.send(data, { end: true }) } else if (!this.headersSent) { // Send headers with no body - this.setHeader('Content-Length', '0'); - this._applyCorsHeaders(); + this.setHeader('Content-Length', '0') + this._applyCorsHeaders() - const statusText = STATUS_CODES[this.statusCode] || 'OK'; + const statusText = STATUS_CODES[this.statusCode] || 'OK' const headers = Object.entries(this.headers) .map(([key, value]) => `${key}: ${value}`) - .join('\r\n'); + .join('\r\n') this.socket.write( `HTTP/1.1 ${this.statusCode} ${statusText}\r\n` + `${headers}\r\n\r\n` - ); + ) - this.headersSent = true; + this.headersSent = true if (!this.shouldKeepAlive) { - this.socket.end(); + this.socket.end() } } else { if (!this.shouldKeepAlive) { - this.socket.end(); + this.socket.end() } } } } // End of Response class -Response.STATUS_CODES = STATUS_CODES; +Response.STATUS_CODES = STATUS_CODES -module.exports = Response; +module.exports = Response diff --git a/test/battle_test.js b/test/battle_test.js index 50fed49..038481a 100644 --- a/test/battle_test.js +++ b/test/battle_test.js @@ -1,178 +1,178 @@ -const net = require('net'); -const Hasty = require('../src/server/index.js'); +const net = require('net') +const Hasty = require('../src/server/index.js') -const PORT = 3457; -const server = new Hasty(); +const PORT = 3457 +const server = new Hasty() server.post('/echo', (req, res) => { - res.json(req.body); -}); + res.json(req.body) +}) server.get('/ping', (req, res) => { - res.send('pong'); -}); + res.send('pong') +}) server.listen(PORT, async () => { - console.log(`Battle server running on port ${PORT}`); - - const tests = [ - testMalformedJson, - testHugeHeaders, - testGarbageData, - testInvalidMethod, - testSlowloris - ]; - - let failed = 0; - - for (const test of tests) { - try { - await test(); - console.log(`βœ… ${test.name} Passed`); - } catch (error) { - console.error(`❌ ${test.name} Failed:`, error.message); - failed++; - } + console.log(`Battle server running on port ${PORT}`) + + const tests = [ + testMalformedJson, + testHugeHeaders, + testGarbageData, + testInvalidMethod, + testSlowloris + ] + + let failed = 0 + + for (const test of tests) { + try { + await test() + console.log(`βœ… ${test.name} Passed`) + } catch (error) { + console.error(`❌ ${test.name} Failed:`, error.message) + failed++ } - - if (failed > 0) { - console.log(`${failed} tests failed`); - process.exit(1); - } else { - console.log('ALL BATTLE TESTS PASSED'); - process.exit(0); - } -}); - -function connect() { - return new Promise((resolve) => { - const client = new net.Socket(); - client.connect(PORT, '127.0.0.1', () => resolve(client)); - }); + } + + if (failed > 0) { + console.log(`${failed} tests failed`) + process.exit(1) + } else { + console.log('ALL BATTLE TESTS PASSED') + process.exit(0) + } +}) + +function connect () { + return new Promise((resolve) => { + const client = new net.Socket() + client.connect(PORT, '127.0.0.1', () => resolve(client)) + }) } -async function testMalformedJson() { - const client = await connect(); - const body = '{ "key": "value", '; // Incomplete JSON - const request = `POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ${body.length}\r\n\r\n${body}`; - - client.write(request); - - return new Promise((resolve, reject) => { - client.on('data', (data) => { - const response = data.toString(); - // Should probably return empty object or error, but definitely not crash - if (response.includes('200 OK') || response.includes('400 Bad Request')) { - client.end(); - resolve(); - } else { - reject(new Error(`Unexpected response: ${response.split('\r\n')[0]}`)); - } - }); - client.on('error', reject); - }); +async function testMalformedJson () { + const client = await connect() + const body = '{ "key": "value", ' // Incomplete JSON + const request = `POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ${body.length}\r\n\r\n${body}` + + client.write(request) + + return new Promise((resolve, reject) => { + client.on('data', (data) => { + const response = data.toString() + // Should probably return empty object or error, but definitely not crash + if (response.includes('200 OK') || response.includes('400 Bad Request')) { + client.end() + resolve() + } else { + reject(new Error(`Unexpected response: ${response.split('\r\n')[0]}`)) + } + }) + client.on('error', reject) + }) } -async function testHugeHeaders() { - const client = await connect(); - let headers = ''; - for (let i = 0; i < 2000; i++) { - headers += `X-Custom-Header-${i}: value\r\n`; - } - const request = `GET /ping HTTP/1.1\r\nHost: localhost\r\n${headers}\r\n`; - - client.write(request); - - return new Promise((resolve, reject) => { - client.on('data', (data) => { - const response = data.toString(); - // Should ideally reject or handle gracefully - if (response.includes('431') || response.includes('400') || response.includes('500')) { - client.end(); - resolve(); - } else { - // If it processes it, that's technically okay but risky. - // We want to see if it crashes or hangs. - client.end(); - resolve(); - } - }); - // If connection closes without data, that's also a fail/pass depending on implementation - client.on('close', () => resolve()); - client.on('error', reject); - }); +async function testHugeHeaders () { + const client = await connect() + let headers = '' + for (let i = 0; i < 2000; i++) { + headers += `X-Custom-Header-${i}: value\r\n` + } + const request = `GET /ping HTTP/1.1\r\nHost: localhost\r\n${headers}\r\n` + + client.write(request) + + return new Promise((resolve, reject) => { + client.on('data', (data) => { + const response = data.toString() + // Should ideally reject or handle gracefully + if (response.includes('431') || response.includes('400') || response.includes('500')) { + client.end() + resolve() + } else { + // If it processes it, that's technically okay but risky. + // We want to see if it crashes or hangs. + client.end() + resolve() + } + }) + // If connection closes without data, that's also a fail/pass depending on implementation + client.on('close', () => resolve()) + client.on('error', reject) + }) } -async function testGarbageData() { - const client = await connect(); - client.write('NOT A VALID HTTP REQUEST\r\n\r\n'); - - return new Promise((resolve, reject) => { - client.on('data', (data) => { - const response = data.toString(); - if (response.includes('400')) { - client.end(); - resolve(); - } else { - // Some servers might just close the connection - client.end(); - resolve(); - } - }); - client.on('close', () => resolve()); - client.on('error', reject); - }); +async function testGarbageData () { + const client = await connect() + client.write('NOT A VALID HTTP REQUEST\r\n\r\n') + + return new Promise((resolve, reject) => { + client.on('data', (data) => { + const response = data.toString() + if (response.includes('400')) { + client.end() + resolve() + } else { + // Some servers might just close the connection + client.end() + resolve() + } + }) + client.on('close', () => resolve()) + client.on('error', reject) + }) } -async function testInvalidMethod() { - const client = await connect(); - client.write('FOO /ping HTTP/1.1\r\nHost: localhost\r\n\r\n'); - - return new Promise((resolve, reject) => { - client.on('data', (data) => { - const response = data.toString(); - if (response.includes('400') || response.includes('405') || response.includes('500')) { - client.end(); - resolve(); - } else { - reject(new Error(`Unexpected response for invalid method: ${response.split('\r\n')[0]}`)); - } - }); - client.on('error', reject); - }); +async function testInvalidMethod () { + const client = await connect() + client.write('FOO /ping HTTP/1.1\r\nHost: localhost\r\n\r\n') + + return new Promise((resolve, reject) => { + client.on('data', (data) => { + const response = data.toString() + if (response.includes('400') || response.includes('405') || response.includes('500')) { + client.end() + resolve() + } else { + reject(new Error(`Unexpected response for invalid method: ${response.split('\r\n')[0]}`)) + } + }) + client.on('error', reject) + }) } -async function testSlowloris() { - const client = await connect(); - const request = 'GET /ping HTTP/1.1\r\nHost: localhost\r\n'; - - // Send headers slowly - let i = 0; - const interval = setInterval(() => { - if (i < request.length) { - client.write(request[i]); - i++; - } else { - clearInterval(interval); - client.write('\r\n'); // Finish headers - } - }, 10); // 10ms delay per char (fast slowloris for test speed) - - return new Promise((resolve, reject) => { - client.on('data', (data) => { - const response = data.toString(); - if (response.includes('pong')) { - client.end(); - resolve(); - } - }); - client.on('error', reject); - - // Timeout if it takes too long (server should handle it) - setTimeout(() => { - clearInterval(interval); - client.destroy(); - resolve(); // Considered pass if it didn't crash server - }, 5000); - }); +async function testSlowloris () { + const client = await connect() + const request = 'GET /ping HTTP/1.1\r\nHost: localhost\r\n' + + // Send headers slowly + let i = 0 + const interval = setInterval(() => { + if (i < request.length) { + client.write(request[i]) + i++ + } else { + clearInterval(interval) + client.write('\r\n') // Finish headers + } + }, 10) // 10ms delay per char (fast slowloris for test speed) + + return new Promise((resolve, reject) => { + client.on('data', (data) => { + const response = data.toString() + if (response.includes('pong')) { + client.end() + resolve() + } + }) + client.on('error', reject) + + // Timeout if it takes too long (server should handle it) + setTimeout(() => { + clearInterval(interval) + client.destroy() + resolve() // Considered pass if it didn't crash server + }, 5000) + }) } diff --git a/test/compliance_test.js b/test/compliance_test.js index 6cef439..3ea38d2 100644 --- a/test/compliance_test.js +++ b/test/compliance_test.js @@ -1,151 +1,151 @@ -const net = require('net'); -const Hasty = require('../src/server/index.js'); +const net = require('net') +const Hasty = require('../src/server/index.js') -const PORT = 3458; -const server = new Hasty(); -server.cors(true); +const PORT = 3458 +const server = new Hasty() +server.cors(true) // Method Tests -server.get('/methods/get', (req, res) => res.send('GET OK')); -server.post('/methods/post', (req, res) => res.status(201).json(req.body)); -server.put('/methods/put', (req, res) => res.json(req.body)); -server.delete('/methods/delete', (req, res) => res.status(204).end()); -server.patch('/methods/patch', (req, res) => res.json(req.body)); -server.head('/methods/head', (req, res) => res.send('HEAD OK')); // Should not send body -server.options('/methods/options', (req, res) => res.status(204).end()); +server.get('/methods/get', (req, res) => res.send('GET OK')) +server.post('/methods/post', (req, res) => res.status(201).json(req.body)) +server.put('/methods/put', (req, res) => res.json(req.body)) +server.delete('/methods/delete', (req, res) => res.status(204).end()) +server.patch('/methods/patch', (req, res) => res.json(req.body)) +server.head('/methods/head', (req, res) => res.send('HEAD OK')) // Should not send body +server.options('/methods/options', (req, res) => res.status(204).end()) // Body Type Tests -server.post('/types/json', (req, res) => res.json(req.body)); -server.post('/types/form', (req, res) => res.json(req.body)); -server.post('/types/text', (req, res) => res.send(req.body)); +server.post('/types/json', (req, res) => res.json(req.body)) +server.post('/types/form', (req, res) => res.json(req.body)) +server.post('/types/text', (req, res) => res.send(req.body)) // Header Tests server.get('/headers', (req, res) => { - res.json({ - contentType: req.headers['content-type'], - custom: req.headers['x-custom-header'] - }); -}); + res.json({ + contentType: req.headers['content-type'], + custom: req.headers['x-custom-header'] + }) +}) server.listen(PORT, async () => { - console.log(`Compliance server running on port ${PORT}`); - - const tests = [ - testMethods, - testBodyTypes, - testHeaders - ]; - - let failed = 0; - - for (const test of tests) { - try { - await test(); - console.log(`βœ… ${test.name} Passed`); - } catch (error) { - console.error(`❌ ${test.name} Failed:`, error.message); - failed++; - } + console.log(`Compliance server running on port ${PORT}`) + + const tests = [ + testMethods, + testBodyTypes, + testHeaders + ] + + let failed = 0 + + for (const test of tests) { + try { + await test() + console.log(`βœ… ${test.name} Passed`) + } catch (error) { + console.error(`❌ ${test.name} Failed:`, error.message) + failed++ } - - if (failed > 0) { - console.log(`${failed} tests failed`); - process.exit(1); - } else { - console.log('ALL COMPLIANCE TESTS PASSED'); - process.exit(0); - } -}); - -function connect() { - return new Promise((resolve) => { - const client = new net.Socket(); - client.connect(PORT, '127.0.0.1', () => resolve(client)); - }); + } + + if (failed > 0) { + console.log(`${failed} tests failed`) + process.exit(1) + } else { + console.log('ALL COMPLIANCE TESTS PASSED') + process.exit(0) + } +}) + +function connect () { + return new Promise((resolve) => { + const client = new net.Socket() + client.connect(PORT, '127.0.0.1', () => resolve(client)) + }) } -function sendRequest(client, request) { - return new Promise((resolve, reject) => { - client.write(request); - client.on('data', (data) => { - resolve(data.toString()); - client.end(); - }); - client.on('error', reject); - }); +function sendRequest (client, request) { + return new Promise((resolve, reject) => { + client.write(request) + client.on('data', (data) => { + resolve(data.toString()) + client.end() + }) + client.on('error', reject) + }) } -async function testMethods() { - // GET - let client = await connect(); - let res = await sendRequest(client, 'GET /methods/get HTTP/1.1\r\nHost: localhost\r\n\r\n'); - if (!res.includes('200 OK') || !res.includes('GET OK')) throw new Error('GET failed'); - - // POST - client = await connect(); - res = await sendRequest(client, 'POST /methods/post HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{"foo":"bar"}'); - if (!res.includes('201 Created') || !res.includes('{"foo":"bar"}')) throw new Error('POST failed'); - - // PUT - client = await connect(); - res = await sendRequest(client, 'PUT /methods/put HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{"foo":"bar"}'); - if (!res.includes('200 OK') || !res.includes('{"foo":"bar"}')) throw new Error('PUT failed'); - - // DELETE - client = await connect(); - res = await sendRequest(client, 'DELETE /methods/delete HTTP/1.1\r\nHost: localhost\r\n\r\n'); - if (!res.includes('204 No Content')) throw new Error('DELETE failed'); - - // PATCH - client = await connect(); - res = await sendRequest(client, 'PATCH /methods/patch HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{"foo":"bar"}'); - if (!res.includes('200 OK') || !res.includes('{"foo":"bar"}')) throw new Error('PATCH failed'); - - // HEAD - client = await connect(); - res = await sendRequest(client, 'HEAD /methods/head HTTP/1.1\r\nHost: localhost\r\n\r\n'); - if (!res.includes('200 OK') || res.includes('HEAD OK')) throw new Error('HEAD failed (should not have body)'); - - // OPTIONS - client = await connect(); - res = await sendRequest(client, 'OPTIONS /methods/options HTTP/1.1\r\nHost: localhost\r\n\r\n'); - // Note: Hasty server might auto-handle OPTIONS for CORS, but we defined a custom route too. - // Let's check if it returns 204 as defined. - if (!res.includes('204 No Content')) throw new Error('OPTIONS failed'); +async function testMethods () { + // GET + let client = await connect() + let res = await sendRequest(client, 'GET /methods/get HTTP/1.1\r\nHost: localhost\r\n\r\n') + if (!res.includes('200 OK') || !res.includes('GET OK')) throw new Error('GET failed') + + // POST + client = await connect() + res = await sendRequest(client, 'POST /methods/post HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{"foo":"bar"}') + if (!res.includes('201 Created') || !res.includes('{"foo":"bar"}')) throw new Error('POST failed') + + // PUT + client = await connect() + res = await sendRequest(client, 'PUT /methods/put HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{"foo":"bar"}') + if (!res.includes('200 OK') || !res.includes('{"foo":"bar"}')) throw new Error('PUT failed') + + // DELETE + client = await connect() + res = await sendRequest(client, 'DELETE /methods/delete HTTP/1.1\r\nHost: localhost\r\n\r\n') + if (!res.includes('204 No Content')) throw new Error('DELETE failed') + + // PATCH + client = await connect() + res = await sendRequest(client, 'PATCH /methods/patch HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{"foo":"bar"}') + if (!res.includes('200 OK') || !res.includes('{"foo":"bar"}')) throw new Error('PATCH failed') + + // HEAD + client = await connect() + res = await sendRequest(client, 'HEAD /methods/head HTTP/1.1\r\nHost: localhost\r\n\r\n') + if (!res.includes('200 OK') || res.includes('HEAD OK')) throw new Error('HEAD failed (should not have body)') + + // OPTIONS + client = await connect() + res = await sendRequest(client, 'OPTIONS /methods/options HTTP/1.1\r\nHost: localhost\r\n\r\n') + // Note: Hasty server might auto-handle OPTIONS for CORS, but we defined a custom route too. + // Let's check if it returns 204 as defined. + if (!res.includes('204 No Content')) throw new Error('OPTIONS failed') } -async function testBodyTypes() { - // JSON - let client = await connect(); - const jsonBody = '{"a":[1,2],"b":3}'; - let res = await sendRequest(client, `POST /types/json HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ${jsonBody.length}\r\n\r\n${jsonBody}`); - if (!res.includes(jsonBody)) { - console.log('JSON Body Response:', res); - throw new Error('JSON body failed'); - } - - // Form URL Encoded - client = await connect(); - res = await sendRequest(client, 'POST /types/form HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 11\r\n\r\nkey=val&a=b'); - // Expected JSON output from queryParser: {"key":"val","a":"b"} - if (!res.includes('"key":"val"') || !res.includes('"a":"b"')) throw new Error('Form body failed'); - - // Text - client = await connect(); - res = await sendRequest(client, 'POST /types/text HTTP/1.1\r\nHost: localhost\r\nContent-Type: text/plain\r\nContent-Length: 11\r\n\r\nHello World'); - if (!res.includes('Hello World')) throw new Error('Text body failed'); +async function testBodyTypes () { + // JSON + let client = await connect() + const jsonBody = '{"a":[1,2],"b":3}' + let res = await sendRequest(client, `POST /types/json HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ${jsonBody.length}\r\n\r\n${jsonBody}`) + if (!res.includes(jsonBody)) { + console.log('JSON Body Response:', res) + throw new Error('JSON body failed') + } + + // Form URL Encoded + client = await connect() + res = await sendRequest(client, 'POST /types/form HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 11\r\n\r\nkey=val&a=b') + // Expected JSON output from queryParser: {"key":"val","a":"b"} + if (!res.includes('"key":"val"') || !res.includes('"a":"b"')) throw new Error('Form body failed') + + // Text + client = await connect() + res = await sendRequest(client, 'POST /types/text HTTP/1.1\r\nHost: localhost\r\nContent-Type: text/plain\r\nContent-Length: 11\r\n\r\nHello World') + if (!res.includes('Hello World')) throw new Error('Text body failed') } -async function testHeaders() { - const client = await connect(); - // Mixed case headers - const res = await sendRequest(client, 'GET /headers HTTP/1.1\r\nHost: localhost\r\nCoNtEnT-TyPe: text/plain\r\nX-CuStOm-HeAdEr: my-value\r\n\r\n'); - - // Headers should be parsed to lowercase keys in req.headers - if (!res.includes('"contentType":"text/plain"') && !res.includes('"content-type":"text/plain"')) { - // Check how the server echoes it back. The route uses req.headers['content-type'] - // If the parser lowercases keys, it should work. - throw new Error('Header case-insensitivity failed'); - } - if (!res.includes('"custom":"my-value"')) throw new Error('Custom header failed'); +async function testHeaders () { + const client = await connect() + // Mixed case headers + const res = await sendRequest(client, 'GET /headers HTTP/1.1\r\nHost: localhost\r\nCoNtEnT-TyPe: text/plain\r\nX-CuStOm-HeAdEr: my-value\r\n\r\n') + + // Headers should be parsed to lowercase keys in req.headers + if (!res.includes('"contentType":"text/plain"') && !res.includes('"content-type":"text/plain"')) { + // Check how the server echoes it back. The route uses req.headers['content-type'] + // If the parser lowercases keys, it should work. + throw new Error('Header case-insensitivity failed') + } + if (!res.includes('"custom":"my-value"')) throw new Error('Custom header failed') } diff --git a/test/production_test.js b/test/production_test.js index b786aaa..7ddb301 100644 --- a/test/production_test.js +++ b/test/production_test.js @@ -1,126 +1,126 @@ -const net = require('net'); -const assert = require('assert'); -const Hasty = require('../src/server/index.js'); +const net = require('net') +const assert = require('assert') +const Hasty = require('../src/server/index.js') -const PORT = 3456; -const server = new Hasty(); -server.cors(true); +const PORT = 3456 +const server = new Hasty() +server.cors(true) server.post('/echo', (req, res) => { - res.json(req.body); -}); + res.json(req.body) +}) server.get('/ping', (req, res) => { - res.send('pong'); -}); + res.send('pong') +}) server.listen(PORT, async () => { - console.log(`Test server running on port ${PORT}`); - - try { - await testChunkedRequest(); - await testKeepAlive(); - await testCors(); - console.log('ALL TESTS PASSED'); - process.exit(0); - } catch (error) { - console.error('TEST FAILED:', error); - process.exit(1); - } -}); - -function testChunkedRequest() { - return new Promise((resolve, reject) => { - console.log('Testing Chunked Request...'); - const client = new net.Socket(); - const body = JSON.stringify({ message: 'Hello World' }); - const request = `POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ${body.length}\r\n\r\n`; - - client.connect(PORT, '127.0.0.1', () => { - // Send headers first - client.write(request); - - // Send body in chunks with delay - setTimeout(() => { - client.write(body.slice(0, 5)); - setTimeout(() => { - client.write(body.slice(5)); - }, 100); - }, 100); - }); - - client.on('data', (data) => { - const response = data.toString(); - if (response.includes('Hello World')) { - console.log('βœ… Chunked Request Passed'); - client.end(); - resolve(); - } else { - reject(new Error('Chunked response did not contain expected body')); - } - }); - }); + console.log(`Test server running on port ${PORT}`) + + try { + await testChunkedRequest() + await testKeepAlive() + await testCors() + console.log('ALL TESTS PASSED') + process.exit(0) + } catch (error) { + console.error('TEST FAILED:', error) + process.exit(1) + } +}) + +function testChunkedRequest () { + return new Promise((resolve, reject) => { + console.log('Testing Chunked Request...') + const client = new net.Socket() + const body = JSON.stringify({ message: 'Hello World' }) + const request = `POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ${body.length}\r\n\r\n` + + client.connect(PORT, '127.0.0.1', () => { + // Send headers first + client.write(request) + + // Send body in chunks with delay + setTimeout(() => { + client.write(body.slice(0, 5)) + setTimeout(() => { + client.write(body.slice(5)) + }, 100) + }, 100) + }) + + client.on('data', (data) => { + const response = data.toString() + if (response.includes('Hello World')) { + console.log('βœ… Chunked Request Passed') + client.end() + resolve() + } else { + reject(new Error('Chunked response did not contain expected body')) + } + }) + }) } -function testKeepAlive() { - return new Promise((resolve, reject) => { - console.log('Testing Keep-Alive...'); - const client = new net.Socket(); - let responseCount = 0; - - client.connect(PORT, '127.0.0.1', () => { - // Send two requests back-to-back - const req1 = `GET /ping HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n`; - const req2 = `GET /ping HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n`; - - client.write(req1); - setTimeout(() => { - client.write(req2); - }, 100); - }); - - client.on('data', (data) => { - const responses = data.toString().split('HTTP/1.1 200 OK'); - // Note: split will create empty string at start, so length should be > 2 for 2 responses - // Or we can just count "pong" - const pongs = (data.toString().match(/pong/g) || []).length; - responseCount += pongs; - - if (responseCount >= 2) { - console.log('βœ… Keep-Alive Passed'); - client.end(); - resolve(); - } - }); - - client.on('end', () => { - if (responseCount < 2) { - reject(new Error(`Keep-Alive failed: received ${responseCount} responses, expected 2`)); - } - }); - }); +function testKeepAlive () { + return new Promise((resolve, reject) => { + console.log('Testing Keep-Alive...') + const client = new net.Socket() + let responseCount = 0 + + client.connect(PORT, '127.0.0.1', () => { + // Send two requests back-to-back + const req1 = 'GET /ping HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n' + const req2 = 'GET /ping HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' + + client.write(req1) + setTimeout(() => { + client.write(req2) + }, 100) + }) + + client.on('data', (data) => { + const responses = data.toString().split('HTTP/1.1 200 OK') + // Note: split will create empty string at start, so length should be > 2 for 2 responses + // Or we can just count "pong" + const pongs = (data.toString().match(/pong/g) || []).length + responseCount += pongs + + if (responseCount >= 2) { + console.log('βœ… Keep-Alive Passed') + client.end() + resolve() + } + }) + + client.on('end', () => { + if (responseCount < 2) { + reject(new Error(`Keep-Alive failed: received ${responseCount} responses, expected 2`)) + } + }) + }) } -function testCors() { - return new Promise((resolve, reject) => { - console.log('Testing CORS...'); - const client = new net.Socket(); - const request = `OPTIONS /echo HTTP/1.1\r\nHost: localhost\r\nOrigin: http://example.com\r\nAccess-Control-Request-Method: POST\r\n\r\n`; +function testCors () { + return new Promise((resolve, reject) => { + console.log('Testing CORS...') + const client = new net.Socket() + const request = 'OPTIONS /echo HTTP/1.1\r\nHost: localhost\r\nOrigin: http://example.com\r\nAccess-Control-Request-Method: POST\r\n\r\n' - client.connect(PORT, '127.0.0.1', () => { - client.write(request); - }); + client.connect(PORT, '127.0.0.1', () => { + client.write(request) + }) - client.on('data', (data) => { - const response = data.toString(); - if (response.includes('Access-Control-Allow-Origin: *') && + client.on('data', (data) => { + const response = data.toString() + if (response.includes('Access-Control-Allow-Origin: *') && response.includes('Access-Control-Allow-Methods')) { - console.log('βœ… CORS Passed'); - client.end(); - resolve(); - } else { - reject(new Error('CORS response missing headers')); - } - }); - }); + console.log('βœ… CORS Passed') + client.end() + resolve() + } else { + reject(new Error('CORS response missing headers')) + } + }) + }) }