From 22740ef89267057c7e6c2257ba2530f746016b0c Mon Sep 17 00:00:00 2001 From: BlazeInferno64 <96913755+BlazeInferno64@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:08:48 +0530 Subject: [PATCH] blazed.js version 1.0.32 Patch notes - 1. Minor bug fixes. 2. Added a new promise based DNS resolving method. Checkout docs for more info. 3. Improved overall performance. --- lib/node/README.md | 87 +++++++++++++++++++++++++++++ lib/node/index.js | 2 +- lib/node/lib/blazed.js | 26 +++++++-- lib/node/lib/utils/dns.js | 75 +++++++++++++++++++++++++ lib/node/lib/utils/errors.js | 81 +++++++++++++++++++++++++++ lib/node/lib/utils/header-parser.js | 80 ++++++++++++++++++++++++++ lib/node/lib/utils/url-parser.js | 82 +++++++++++++++++++++++++++ lib/node/package-lock.json | 4 +- lib/node/package.json | 4 +- lib/node/test/blazed-test.js | 3 +- lib/node/typings/index.d.ts | 66 +++++++++++++++++++++- 11 files changed, 496 insertions(+), 14 deletions(-) create mode 100644 lib/node/lib/utils/dns.js create mode 100644 lib/node/lib/utils/errors.js create mode 100644 lib/node/lib/utils/header-parser.js create mode 100644 lib/node/lib/utils/url-parser.js diff --git a/lib/node/README.md b/lib/node/README.md index 2a46d2e..6d33643 100644 --- a/lib/node/README.md +++ b/lib/node/README.md @@ -510,6 +510,93 @@ blazed.get("https://api.github.com/users") Stay up-to-date with our project for upcoming features and enhancements, including additional events that will be introduced in future releases. +# DNS resolving + +In addition to making HTTP requests, `blazed.js` also provides an asynchronous way to resolve the DNS of various hostnames. + +You can use the `blazed.resolve()` method to achieve this. It returns a promise that resolves with an IP object containing the resolved IP addresses and its format. + +The object has the following structre: + +```js + +{ + Format: string, // The IP address format (e.g., IPv4, IPv6). Optional. If not specified, **blazed.js** will resolve the promise with the first IP address found after performing a DNS lookup for the host + Addresses: Array, // The ip address of the host which has been resolved (Present in array) +} + +``` + +### Accessing IP Object Properties + +When logging the IP object, you can access its properties as follows (assuming the object is named `ipObj`): + +* `ipObj.format`: The IP address format (e.g., 'IPv4' or 'IPv6'). +* `ipObj.addresses`: The resolved IP addresses of the host. + +Example demonstrating DNS resolving: + +```js +// Resolving DNS using blazed.resolve() with specified ip format. + +blazed.resolve({ + /** + * The IP address format (e.g., 'IPv4' or 'IPv6'). Optional. + * If not specified, **blazed.js** will resolve the promise with the first IP address found after performing a DNS lookup for the host. + */ + format: "IPv4", + + /** + * The hostname to resolve (e.g., 'www.google.com'). + * Note: if you omit the protocol (http/https), you will get an error of invalid url. + */ + hostname: "https://www.google.com" // Let's take google.com here +}) +.then(ipObj => { + // Logging the ipObj to the console. + return console.log(ipObj); + // ipObj contains: + // - Format (The format of the ip of the host) + // - Address (Array containing the list of ip addresses) +}) +.catch(err => { + // Logging any errors to the console. + return console.error(err); +}); +``` + + +```js + // Resolving DNS using blazed.resolve() with specified ip format. + // Starting the request to resolve the hostname. + +blazed.request({ + /** + * The hostname to resolve (e.g., 'https://www.google.com'). + * Note: if you omit the protocol (http/https), you will get an error of invalid url. + */ + hostname: "https://www.google.com" // Let's take google.com here +}) +.then(ipObj => { + // Logging the ipObj to the console. + return console.log(ipObj); + // ipObj contains: + // - Format (The format of the ip of the host) + // - Address (Array containing the list of ip addresses) +}) +.catch(err => { + // Logging any errors to the console. + return console.error(err); +}); + +``` + +### Underlying Technology + +This feature is built on top of Node.js's built-in `dns` module, which provides an asynchronous network wrapper for performing DNS lookups. + +For more information about the `dns` module, please refer to the [official Node.js documentation](https://nodejs.org/api/dns.html). + # Validating Header Names and Values In addition to sending requests, you can also validate header names and values using the `blazed.validateHeaderName()` and `blazed.validateHeaderValue()` functions. diff --git a/lib/node/index.js b/lib/node/index.js index 30e5147..23e73ba 100644 --- a/lib/node/index.js +++ b/lib/node/index.js @@ -19,7 +19,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -'use strict'; +"use strict"; /** * blazed.js is a blazing-fast, light weight, high-performance, promise-based HTTP client * diff --git a/lib/node/lib/blazed.js b/lib/node/lib/blazed.js index e132cec..6c34784 100644 --- a/lib/node/lib/blazed.js +++ b/lib/node/lib/blazed.js @@ -1,6 +1,6 @@ // Copyright (c) 2024 BlazeInferno64 --> https://github.com/blazeinferno64 -'use strict'; +"use strict"; const http = require('http'); const https = require('https'); @@ -8,9 +8,10 @@ const https = require('https'); const { EventEmitter } = require("events"); const emitter = new EventEmitter(); -const urlParser = require("./utils/tools/url-parser"); -const headerParser = require("./utils/tools/header-parser"); -const httpErrors = require("./utils/tools/http-errors"); +const urlParser = require("./utils/url-parser"); +const headerParser = require("./utils/header-parser"); +const httpErrors = require("./utils/errors"); +const dnsResolver = require("./utils/dns"); const packageJson = require("../package.json"); @@ -328,6 +329,20 @@ const validateHeaderValue = async (name, value) => { return await headerParser.parseThisHeaderValue(name, value); } +/** + * Resolves a hostname's dns with a ip object contaning the ip addresses. + * @param {Object} hostObject - The object containing the host data + * @param {('IPv4'|'IPv6')} hostObject.format - Optional ip address format + * @param {string} hostObject.hostname - The hostname which you want to resolve + * @returns {Promise} Returns a promise which contains the resolved ip data + */ + +const resolve = async (hostObject) => { + const url = hostObject.hostname; + const format = hostObject.format; + const parsedURL = new URL(url); + return await dnsResolver.lookupForIp(parsedURL.hostname, format, url); +} /** * @returns {Object} Returns a object which contains some info regarding blazed.js. */ @@ -472,7 +487,7 @@ module.exports = { const method = object.method; const headers = object.headers; const data = object.body; - let redirectCount = object.redirectCount; + let redirectCount = object.limit; if (!redirectCount) return redirectCount = 5; // If not specified make it 5 by default. if (!http.METHODS.includes(method.toUpperCase())) { @@ -491,6 +506,7 @@ module.exports = { about, validateHeaderName, validateHeaderValue, + resolve, /** * Attaches a listener to the on event * Fires up whenever a request is ready to send diff --git a/lib/node/lib/utils/dns.js b/lib/node/lib/utils/dns.js new file mode 100644 index 0000000..1f690e7 --- /dev/null +++ b/lib/node/lib/utils/dns.js @@ -0,0 +1,75 @@ +const dns = require("dns"); +const net = require("net"); + +const { processError } = require("./errors"); + +/** + * + * @param {string} hostname - The hostname you want to resolve + * @param {string} type - The type/format of ip (eg. IPv4, IPv6) + * @returns {Promise} - Returns a promise which resolves with the ip data + */ +const lookupForIp = async(hostname, type) => { + return await new Promise(async(resolve, reject) => { + + //Define an ip object + const ipObj = { + Format: "", + Addresses: [] + } + + if (!type && typeof type === 'undefined') { + try { + return dns.lookup(hostname, async(err, address) => { + if (err) return reject({ error: await processError(err, hostname, 'Yes') }); + ipObj.Addresses.push(address); + if (net.isIPv4(address)) { + ipObj.Format = "IPv4"; + return resolve(ipObj); + } + else if (net.isIPv6(address)) { + ipObj.Format = "IPv6"; + return resolve(ipObj); + } + else { + ipObj.Format = 'Unknown Format'; + return resolve(ipObj); + } + }) + } catch (error) { + return reject(await processError(error, hostname, 'Yes')); + } + } + else if (type !== '' && typeof type !== 'undefined') { + if (type === 'IPv4') { + try { + return dns.resolve4(hostname, async(err, addresses) => { + if (err) return reject({ error: await processError(err, hostname, 'Yes') }); + ipObj.Format = 'IPv4'; + ipObj.Addresses = addresses; + return resolve(ipObj); + }) + } catch (error) { + return reject(await processError(error, hostname, 'Yes')); + } + } else if (type === "IPv6") { + try { + return dns.resolve6(hostname, async(err, addresses) => { + if (err) return reject({ error: await processError(err, hostname, 'Yes') }); + ipObj.Format = 'IPv6'; + ipObj.Addresses = addresses; + return resolve(ipObj); + }) + } catch (error) { + return reject(await processError(error, hostname, 'Yes')); + } + } else { + return reject(`Unknown ip address format specified! Available formats - IPv4, IPv6`); + } + } + }) +} + +module.exports = { + lookupForIp +} \ No newline at end of file diff --git a/lib/node/lib/utils/errors.js b/lib/node/lib/utils/errors.js new file mode 100644 index 0000000..7c29385 --- /dev/null +++ b/lib/node/lib/utils/errors.js @@ -0,0 +1,81 @@ +"use strict"; +/** + * Util tool for processing http based common errors. + * @param {Object} error - The HTTP error you want to process. + * @returns {Promise} returns the processed error object as a promise. + */ +const processError = (error, url, dns) => { + return new Promise((resolve, reject) => { + if (error.code === 'ENOTFOUND') { + const err = new Error(`DNS Resolution Error`); + err.code = error.code; + err.name = "DNS_Resolution_Error"; + err.hostname = url; + err.message = `Failed to resolve the DNS of '${url}'`; + return reject(err); + } else if (error.code === 'ETIMEOUT') { + const err = new Error(`Request Timeout`); + err.code = error.code; + err.name = "Request_Timeout_Error"; + err.message = dns ? `The DNS request was timed out` : `The HTTP request to ${url} was timed out!`; + err.hostname = url; + return reject(err); + } else if (error.type === 'abort' || error.code === 'ABORT_ERR') { + const err = new Error(`Request Aborted`); + err.code = error.code; + err.name = "Request_Abort_Error"; + err.message = `HTTP ${method} request to ${url} was aborted`; + return reject(err); + } else if (error.code === 'ECONNREFUSED') { + const err = new Error(`Connection Refused`); + err.code = error.code; + err.name = "Connection_Refused_Error" + err.message = dns? `Failed to lookup DNS of '${url}'` : `The server refused the connection for the HTTP ${method} request to ${url}`; + return reject(err); + } else if (error.code === 'ECONNRESET') { + const err = new Error(`Connection Reset`); + err.code = error.code; + err.name = "Connection_Reset_Error"; + err.message = dns? `Connection reset while looking up for the DNS of '${url}'` : `The server reset the connection while sending the HTTP ${method} request to ${url}`; + return reject(err); + } else if (error.code === 'EPIPE') { + const err = new Error(`Broken Pipe`); + err.code = error.code; + err.name = "Broken_Pipe_Error"; + err.url = url; + err.message = `The connection to ${url} was closed unexpectedly while sending the HTTP ${method} request`; + return reject(err); + } else if (error.code === 'EHOSTUNREACH') { + const err = new Error(`Host Unreachable`); + err.code = error.code; + err.name = "Host_Unreachable_Error"; + err.message = `The host '${url}' is unreachable`; + return reject(err); + } else if (error.code === 'ENETUNREACH') { + const err = new Error(`Network Unreachable`); + err.code = error.code; + err.name = "Network_Unreachable_Error"; + err.message = `The network is unreachable`; + return reject(err); + } else if (error.code === 'EHOSTDOWN') { + const err = new Error(`Host is down`); + err.code = error.code; + err.name = "Host_Down_Error"; + err.message = `The host '${url}' is down`; + return reject(err); + } else if (error.code === 'ENETDOWN') { + const err = new Error(`Network is down`); + err.code = error.code; + err.name = "Network_Down_Error"; + err.message = `The network is down`; + return reject(err); + } + else { + return reject(error); + } + }) +} + +module.exports = { + processError +} \ No newline at end of file diff --git a/lib/node/lib/utils/header-parser.js b/lib/node/lib/utils/header-parser.js new file mode 100644 index 0000000..8debf25 --- /dev/null +++ b/lib/node/lib/utils/header-parser.js @@ -0,0 +1,80 @@ +// Copyright (c) 2024 BlazeInferno64 --> https://github.com/blazeinferno64 +"use strict"; + +const { validateHeaderName, validateHeaderValue } = require("http"); +const { resolve } = require("path"); + +/** + * + * @param {string} header The Header Name for checking + * @returns {Promise} A promise that resolves as true if the header name parsing is successfull, else it will reject with an error + */ + +const parseThisHeaderName = (header) => { + return new Promise((resolve, reject) => { + try { + validateHeaderName(header); + return resolve(true); + } catch (error) { + if(error.code === 'ERR_INVALID_HTTP_TOKEN') { + const error = new TypeError(); + error.code = 'ERR_INVALID_HTTP_HEADER'; + error.message = `Header name must be a valid HTTP token! Recieved token: ["${header}"]`; + return reject(error); + } + return reject(error); + } + }) +} + +/** + * Validates header name and values + * @param {*} name The Header name to check + * @param {*} value The Header value to check + * @return {Promise} A promise that resolves with the header name and value as an JSON object if the Header parsing is successfull, else it will reject it with the error. + */ + +const parseThisHeaderValue = (name, value) => { + return new Promise((resolve, reject) => { + if(!name) { + const error = new TypeError(); + error.code = 'ENULLHEADER'; + error.message = `Header name is empty!`; + return reject(error); + } + if(!value) { + const error = new TypeError(); + error.code = 'ENULLVALUE'; + error.message = `Header value is empty!`; + return reject(error); + } + try { + + validateHeaderValue(name, value); + const headerObj = { + "name": name, + "value": value + } + return resolve(headerObj); + } catch (err) { + if(err.code === 'ERR_HTTP_INVALID_HEADER_VALUE') { + const error = new TypeError(`Invalid Header Value`); + error.code = err.code; + error.message = `Invalid value: ${value} for header "${name}"`; + return reject(error); + } + if(err.code === 'ERR_INVALID_CHAR') { + const error = new TypeError(`Invalid Header Value`); + error.code = err.code; + error.message = `Invalid character: ${value} in header content ["${name}"]`; + return reject(error); + } + return reject(err); + } + }) +} + +module.exports = { + parseThisHeaderName, + parseThisHeaderValue +} \ No newline at end of file diff --git a/lib/node/lib/utils/url-parser.js b/lib/node/lib/utils/url-parser.js new file mode 100644 index 0000000..7ddc2c2 --- /dev/null +++ b/lib/node/lib/utils/url-parser.js @@ -0,0 +1,82 @@ +// Copyright (c) 2024 BlazeInferno64 --> https://github.com/blazeinferno64 + +"use strict"; + +/** + * Represents a parsed URL. + * @typedef {Object} ParsedURL + * @property {string} hash + * @property {string} host + * @property {string} hostname + * @property {string} href + * @property {string} origin + * @property {string} password + * @property {string} pathname + * @property {string} protocol + * @property {string} search + * @property {URLSearchParams} searchParams + */ + +/** + * Checks whether a provided URL is valid or not. + * @param {string} url The URL to check. + * @returns {Promise} A promise that resolves with the parsed URL as a JSON Object. + */ + +const parseThisURL = (url) => { + if (!url) { + throw new Error('No URL Provided!'); + } + return new Promise((resolve, reject) => { + try { + const parsedURL = new URL(url); + const urlData = { + hash: parsedURL.hash, + host: parsedURL.host, + hostname: parsedURL.hostname, + href: parsedURL.href, + origin: parsedURL.origin, + password: parsedURL.password, + pathname: parsedURL.pathname, + protocol: parsedURL.protocol, + search: parsedURL.search, + searchParams: parsedURL.searchParams, + }; + return resolve(urlData); + } catch (error) { + if (error.code === 'ERR_INVALID_URL') { + const err = new TypeError('Invalid URL!'); + err.message = `Invalid URL provided "${url}"`; + err.code = error.code; + err.input = url; + err.name = `URL Error` + return reject(err); + } + return reject(error); + } + }); + }; + +/** + * Checks if a given string is a valid URL. + * @param {string} url The URL to check. + * @returns {boolean} True if the URL is valid, false otherwise. + */ +const isValidURL = (url) => { + return new Promise((resolve, reject) => { + try { + new URL(url); + return resolve(true); + } catch (error) { + const err = new TypeError(`Not a valid URL`); + err.code = error.code; + err.message = `${url} isn't a valid URL!`; + return reject(err); + } + }) +} + +module.exports = { + parseThisURL, + isValidURL +} \ No newline at end of file diff --git a/lib/node/package-lock.json b/lib/node/package-lock.json index 0127143..1ae0014 100644 --- a/lib/node/package-lock.json +++ b/lib/node/package-lock.json @@ -1,12 +1,12 @@ { "name": "blazed.js", - "version": "1.0.31", + "version": "1.0.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blazed.js", - "version": "1.0.31", + "version": "1.0.32", "license": "MIT" } } diff --git a/lib/node/package.json b/lib/node/package.json index 09bdbbf..976f040 100644 --- a/lib/node/package.json +++ b/lib/node/package.json @@ -1,6 +1,6 @@ { "name": "blazed.js", - "version": "1.0.31", + "version": "1.0.32", "main": "index.js", "types": "typings/index.d.ts", "scripts": { @@ -42,7 +42,7 @@ ], "author": "BlazeInferno64 -> https://github.com/blazeinferno64 <-", "license": "MIT", - "homepage": "https://blazeinferno64.github.io/blazed.js/lib/node", + "homepage": "https://github.com/blazeinferno64/blazed.js/tree/main/lib/node#readme", "repository": { "type": "git", "url": "git+https://github.com/blazeinferno64/blazed.js.git" diff --git a/lib/node/test/blazed-test.js b/lib/node/test/blazed-test.js index c0a0fb7..c2ab9ea 100644 --- a/lib/node/test/blazed-test.js +++ b/lib/node/test/blazed-test.js @@ -6,7 +6,8 @@ async function testThis() { const headers = { /* your headers here*/ }; // Leave it empty if it's not necessary to add custom headers const url = 'https://jsonplaceholder.typicode.com/posts/1'; const response = await blazed.get(url, headers); - console.log(`TEST PASSED SUCCESSFULLY!\n`); + console.log(`TEST PASSED SUCCESSFULLY!`); + console.log(`RESPONSE RECEIVED:\n`); return console.log(response); } catch (error) { return console.error(error); diff --git a/lib/node/typings/index.d.ts b/lib/node/typings/index.d.ts index 0719b7b..d9b465f 100644 --- a/lib/node/typings/index.d.ts +++ b/lib/node/typings/index.d.ts @@ -22,6 +22,30 @@ // Type definitions for blazed.js +interface IpObject { + /** + * The format of the resolved ip + */ + Format: string; + /** + * The ip address which has been resolved (Present in array) + */ + Addresses: Array +} + +interface HostObject { + /** + * The hostname (eg. https://www.google.com) + */ + hostname?: string; + /** + * The IP address format (e.g., IPv4, IPv6) + * + * Optional. If not specified, **blazed.js** will resolve the promise with the first IP address found after performing a DNS lookup for the host. + */ + format: ?'IPv4' | 'IPv6'; +} + interface ConnectionObject { /** * A success message indicating the status of the connection (e.g. "Successfully established a connection to..."). @@ -71,9 +95,9 @@ interface RequestObject { */ body?: any; /** - * The redirect count + * The no of redirects to accept. By default its set to 5 */ - redirectCount?: number; + limit?: number; } interface URLParser { @@ -192,6 +216,42 @@ interface HeaderObject { } interface blazed { +/** + * Check the docs for more info. + * + * Resolves a hostname's DNS to an IP object containing the IP addresses. + * @param {Object} hostObject - The object containing the host data. + * @param {('IPv4'|'IPv6')} hostObject.format - Optional. The IP address format. If not specified, + * blazed.js will resolve the promise with the first IP address found after performing a DNS lookup for the host. + * @param {string} hostObject.hostname - The hostname to be resolved. + * @returns {Promise} Returns a promise containing the resolved IP data. + * @example + * // Example usage demonstrating DNS resolving with specified format + * // Starting the request + * blazed.request({ + * format: "IPv6", + * hostname: "https://www.google.com" + * }).then(res => { + * return console.log(res.data); + * // It will return all the addresses after resolving the DNS + * }).catch(err => { + * return console.error(err); + * // handling errors + * }) + * + * // Example usage demonstrating DNS resolving without specified format + * // Starting the request + * blazed.request({ + * hostname: "https://www.google.com" + * }).then(res => { + * return console.log(res.data); + * // It will return only the fist ip address which is found after dns has been resolved + * }).catch(err => { + * return console.error(err); + * // handling errors + * }) + */ + resolve(hostObj: HostObject): Promise; /** * Performs an HTTP GET request. * @param {Object} url The URL to request. @@ -313,7 +373,7 @@ interface blazed { * @param {string} requestObj.method - The HTTP method to use (e.g. GET, POST, PUT, DELETE, etc.). * @param {Object} requestObj.headers - Optional headers to include in the request. * @param {Object} request.body - Optional data to send in the request body. - * @param {number} requestObj.redirectCount - The redirect count for the http request. By default it's set to 5. + * @param {number} requestObj.limit - The limit for the number of redirects for the http request. By default it's set to 5. * @returns {Promise} A promise that resolves with the response data. * @example * // Starting the request