New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@ceicc/range

Package Overview
Dependencies
Maintainers
1
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ceicc/range - npm Package Compare versions

Comparing version 2.2.1 to 3.0.0-beta.1

362

index.js
const
fs = require("fs"),
{ pipeline } = require("stream"),
{ contentType } = require("mime-types");
{ promisify } = require("util"),
{ contentType } = require("mime-types"),
optionsChecker = require("@ceicc/options-checker"),
{ URL } = require("url"),
{ IncomingMessage, ServerResponse } = require("http"),
// Im not going to use `stream/promises` liberary because it was added in
// version 15.0, Not all hosting providers support that version (including mine)
pipelinePromised = promisify(pipeline),
nextTick = () => new Promise(r => process.nextTick(r))
module.exports = range
/**
* @typedef {object} options
* @property {object} [headers] the request headers object `req.headers`
*
* if `range` and/or `conditionalRequest` are true,
* then the headers object is required.
*
* you can pass the whole headers object, or only the conditional and range headers
*
* @property {boolean} [conditional] whether to respect conditional requests or not - default false
*
* if true, the headers object is required
* @property {boolean} [range] accept range request - default false
*
* if true, the headers object is required
* @property {number} [maxAge] max age of caching in seconds - default 0
* @property {boolean} [etag] add Etag header - default true
* @property {boolean} [lastModified] add last-modified header - default true
*
* @property {number} [maxAge] caching period in seconds - default `10800` (3 hours)
*
* @property {boolean} [etag] add Etag header - default `true`
*
* @property {boolean} [lastModified] add `last-modified` header - default `true`
*
* @property {boolean} [conditional] whether to respect conditional requests or not - default `true`
*
* @property {boolean} [range] accept range requests - default `true`
*
* @property {string|boolean} [notFound] a handler for non existing files
*
* `notFound: false` a rejection will be thrown (default).
*
* `notFound: true` empty body with response code '404' will be sent.
*
* `notFound: <string>` send a file with response code '404', the given string is the path to file.
* if the path doesn't led to a file, a rejection will be thrown
* @property {boolean|Array<string>} [implicitIndex=false] Check for index files if the request path is a directory. default: `false`
*
* Pass an array of extensions to check against. e.g. _`["html", "css"]`_
*
* Or simply pass `true` to check for html extension only
*
* `notFound: false` `next` will be called.
*
* `notFound: true` empty body with response code '404' will be sent (default).
*
* `notFound: <string>` send a file with response code '404', the given string is the path to file.
*
* if the path doesn't led to a file, `next` will be called.
*
* ***Note:*** The path is relative to the `baseDir` path.
*
* @property {boolean|Array<string>} [implicitIndex] Check for index files if the request path is a directory - default: `true`
*
* Pass an array of extensions to check against. e.g. _`["html", "css"]`_
*
* Or simply pass `true` to check for html extension only
*
* @property {string} [baseDir] the base dirctory - default: `'.'`
*
* @property {boolean} [hushErrors] Whether to ignore errors and reply with status code `500`, or pass the error to `next` function - default: `false`
*/

@@ -42,129 +58,185 @@

/**
*
* @param {string} path path to file, `req.url`
* @param {object} res http.ServerResponse object `res`
* @param {options} options optional
* @returns {Promise<number>} A Promise with the response status code
* @param {options} [options] configuration object
* @returns {Function} Middleware function
*/
const range = async (path, res, options) => new Promise(async (resolve, rejects) => {
if (typeof path !== "string") return rejects(new Error("path argument is required!"));
if (typeof res !== "object") return rejects(new Error("res object is required!"));
if ((options?.conditional || options?.range) && !options?.headers) return rejects(new Error("headers object is required!"));
function range(options = {}) {
const stat = await fs.promises.stat(path).catch(err => {
if (err.code === "ENOENT") {
optionsChecker(options, {
baseDir: { default: '.', type: "string" },
hushErrors: { default: false, type: "boolean" },
conditional: { default: true, type: "boolean" },
range: { default: true, type: "boolean" },
maxAge: { default: 10800, type: "number" },
etag: { default: true, type: "boolean" },
lastModified: { default: true, type: "boolean" },
notFound: { default: true, type: "boolean|string" },
implicitIndex: { default: true, type: "boolean|array" },
})
if (options?.notFound === true) {
res.statusCode = 404;
res.end();
resolve(404);
return false;
}
if (typeof options?.notFound === "string") {
range(options.notFound, res, {
...options,
notFound: false
}).then(resolve).catch(rejects);
return false;
}
return async function rangeMiddleware(req, res, next = console.error) {
const e = new Error("File Not Found");
e.code = 404;
e.path = path;
rejects(e);
return false;
if (!(req instanceof IncomingMessage)) {
await nextTick() // To make it truly asynchronous
next(TypeError("Request object is not an instance of IncomingMessage"))
}
if (!(res instanceof ServerResponse)) {
await nextTick() // To make it truly asynchronous
next(TypeError("Response object is not an instance of ServerResponse"))
}
rejects(err);
return false;
});
if (!stat) return;
if (stat.isDirectory()) {
req.pathname = new URL(`https://example.com${req.url}`).pathname
if (!options?.implicitIndex)
return resolve(forgetAboutIt(res, 404))
try {
const extensions = new Set()
// Using `var` to get function scope
var stat = await fs.promises.stat(options.baseDir + req.pathname)
if (Array.isArray(options.implicitIndex))
options.implicitIndex.forEach(v => extensions.add(v))
else if (options.implicitIndex === true)
extensions.add("html")
} catch (error) {
let resolved = false
if (error.code === "ENOENT") {
const directory = await fs.promises.readdir(path)
if (options.notFound === true)
return forgetAboutIt(res, 404)
for (const extension of extensions) {
if (!directory.includes(`index.${extension}`))
continue
if (typeof options.notFound === "string") {
await range(`${path}/index.${extension}`, res, { ...options }).then(resolve).catch(rejects)
resolved = true
break
req.url = `/${options.notFound}`
options.notFound = false
return rangeMiddleware(req, res, next)
}
return options.hushErrors ? forgetAboutIt(res, 404) : next(error)
}
return options.hushErrors ? forgetAboutIt(res, 500) : next(error)
}
return resolved ? null : resolve(forgetAboutIt(res, 404))
}
const
headers = options?.headers,
accRange = options?.range === true,
conditional = options?.conditional === true,
maxAge = typeof options?.maxAge === 'number' ? options?.maxAge : 0,
etag = options?.etag === false ? false : getEtag(stat.mtime, stat.size),
lastMod = options?.lastModified === false ? false : new Date(stat.mtime).toUTCString();
if (stat.isDirectory()) {
etag && res.setHeader("etag", etag);
lastMod && res.setHeader("last-modified", lastMod);
maxAge && res.setHeader("cache-control", `max-age=${maxAge}`);
accRange && res.setHeader("accept-ranges", "bytes"); // Hint to the browser range is supported
if (!options.implicitIndex)
return forgetAboutIt(res, 404)
res.setHeader("content-type", contentType(path.split(".").pop()));
res.setHeader("content-length", stat.size);
const extensions = new Set()
// check conditional request, calclute a diff up to 2 sec because browsers sends seconds and javascript uses milliseconds
if (conditional && (headers["if-none-match"] === etag || (Date.parse(headers["if-modified-since"]) - stat.mtime.getTime()) >= -2000))
return resolve(forgetAboutIt(res, 304));
if (Array.isArray(options.implicitIndex))
options.implicitIndex.forEach(v => extensions.add(v))
if (accRange && headers["range"]) {
if (headers["if-range"] && headers["if-range"] !== etag) {
res.statusCode = 200;
streamIt(path, res, resolve, rejects);
return;
else if (options.implicitIndex === true)
extensions.add("html")
const directory = await fs.promises.readdir(options.baseDir + req.pathname)
for (const extension of extensions) {
if (!directory.includes(`index.${extension}`))
continue
req.url = `${req.pathname}/index.${extension}`
return rangeMiddleware(req, res, next)
}
return forgetAboutIt(res, 404)
}
if (headers["if-match"] && headers["if-match"] !== etag || (Date.parse(headers["if-unmodified-since"]) - stat.mtime.getTime()) < -2000)
return resolve(forgetAboutIt(res, 412));
return rangeReq(path, res, resolve, rejects, headers["range"], stat.size);
}
const etag = options.etag && getEtag(stat.mtime, stat.size)
res.statusCode = 200;
streamIt(path, res, resolve, rejects);
return;
etag && res.setHeader("etag", etag)
options.lastModified && res.setHeader("last-modified", stat.mtime.toUTCString())
options.maxAge && res.setHeader("cache-control", `max-age=${options.maxAge}`)
options.range && res.setHeader("accept-ranges", "bytes") // Hint to the browser range is supported
});
res.setHeader("content-type", contentType(req.pathname.split(".").pop()))
res.setHeader("content-length", stat.size)
module.exports = range;
// check conditional request, calclute a diff up to 2 sec because browsers sends seconds and javascript uses milliseconds
if ( options.conditional && (
req.headers["if-none-match"] === etag ||
// No need to check if the header exist because `Date.parse` will return `NaN` to falsy inputs,
// any arithmetic to `NaN` will result in `NaN`,
// and any compartion to `NaN` will result in `false`
( Date.parse(req.headers["if-modified-since"]) - stat.mtime.getTime() ) >= -2000 )
)
return forgetAboutIt(res, 304)
function streamIt(path, res, resolve, rejects, opts) {
pipeline(
if (options.range && req.headers["range"]) {
if (req.headers["if-range"] && req.headers["if-range"] !== etag) {
res.statusCode = 200
try {
await streamIt(options.baseDir + req.pathname, res)
} catch (error) {
options.hushErrors ? hush(res) : next(error)
}
return
}
if (
req.headers["if-match"] && req.headers["if-match"] !== etag ||
(Date.parse(req.headers["if-unmodified-since"]) - stat.mtime.getTime()) < -2000
)
return forgetAboutIt(res, 412)
try {
await rangeRequest(options.baseDir + req.pathname, res, req.headers["range"], stat.size)
} catch (error) {
options.hushErrors ? hush(res) : next(error)
}
return
}
res.statusCode = 200
try {
await streamIt(options.baseDir + req.pathname, res)
} catch (error) {
options.hushErrors ? hush(res) : next(error)
}
}
}
function hush(res) {
if (!res.headersSent) {
res.statusCode = 500
res.end()
}
}
async function streamIt(path, res, opts) {
return pipelinePromised(
fs.createReadStream(path, opts ? { start: opts.start, end: opts.end} : null),
res,
err => {
if (err && err.code !== "ERR_STREAM_PREMATURE_CLOSE") // Stream closed (normal)
return rejects(err);
resolve(res.statusCode);
}
);
).catch(async (err) => {
if (!err || err.code === "ERR_STREAM_PREMATURE_CLOSE") // Stream closed (normal)
return
else
throw err
})
}

@@ -182,16 +254,16 @@

res.end();
return status;
}
function rangeReq(path, res, resolve, rejects, Range, size) {
function rangeRequest(path, res, range, size) {
res.removeHeader("content-length");
Range = Range.match(/bytes=([0-9]+)?-([0-9]+)?/i);
res.removeHeader("content-length")
if (!Range) { // Incorrect pattren
res.setHeader("content-range", `bytes */${size}`);
return resolve(forgetAboutIt(res, 416));
range = range.match(/bytes=([0-9]+)?-([0-9]+)?/i)
if (!range) { // Incorrect pattren
res.setHeader("content-range", `bytes */${size}`)
return forgetAboutIt(res, 416)
}
let [start, end] = [Range[1], Range[2]];
let [start, end] = [range[1], range[2]]

@@ -201,28 +273,28 @@ switch (true) {

case !!start && !end: // Range: <unit>=<range-start>-
start = Number(start);
end = size - 1;
break;
start = Number(start)
end = size - 1
break
case !start && !!end: // Range: <unit>=-<suffix-length>
start = size - end;
end = size - 1;
break;
start = size - end
end = size - 1
break
case !start && !end: // Range: <unit>=-
start = 0;
end = size - 1;
break;
start = 0
end = size - 1
break
default: // Range: <unit>=<range-start>-<range-end>
[start, end] = [Number(start), Number(end)];
[start, end] = [Number(start), Number(end)]
}
if (start < 0 || start > end || end >= size) { // Range out of order or bigger than file size
res.setHeader("content-range", `bytes */${size}`);
return resolve(forgetAboutIt(res, 416));
res.setHeader("content-range", `bytes */${size}`)
return forgetAboutIt(res, 416)
}
res.statusCode = 206; // partial content
res.setHeader("content-range", `bytes ${start}-${end}/${size}`);
streamIt(path, res, resolve, rejects, { start, end });
res.statusCode = 206 // partial content
res.setHeader("content-range", `bytes ${start}-${end}/${size}`)
return streamIt(path, res, { start, end })
}
{
"name": "@ceicc/range",
"version": "2.2.1",
"version": "3.0.0-beta.1",
"description": "http range request handler",

@@ -9,2 +9,3 @@ "main": "index.js",

"app-dev": "nodemon test/app.js",
"dev-v3": "node test/v3.js",
"postversion": "echo 'Pushing to Github ----------' && git push && git push --tags && echo 'Pushing to NPM -----------' && npm publish"

@@ -33,4 +34,8 @@ },

"dependencies": {
"@ceicc/options-checker": "^1.0.2",
"mime-types": "^2.1.32"
},
"devDependencies": {
"express": "^4.17.2"
}
}
# range
static files request handler
A static files middleware
# Installation
## Installation
```
npm i @ceicc/range
npm i @ceicc/range@3.0.0-beta.1
```
# Usage
start by importing `http` and `range`
## Usage
add `range` to an existence express app
```js
const
http = require("http"),
range = require("@ceicc/range");
import range from "@ceicc/range"
// CommonJS
// const range = require("@ceicc/range")
app.get('/public/*', range())
app.listen(3000)
```
This will server every request starts with `/public/` with `range`.
Then make a simple server that responds with a file based on the requested path
```js
http.createServer((req, res) => {
The base directory will be `.` or the current working directory, unless specified in the `options` object.
range(__dirname + req.url, res).catch(console.error)
## Options Object
}).listen(2000);
```
# Parameters
1. file path.
2. the response object.
3. optional object
#### `maxAge`
# Options Object
1. `maxAge` max age of caching in seconds - default 0
2. `etag` add Etag header - default true
3. `lastModified` add last-modified header - default true
4. `conditional` whether to respect conditional requests or not - default false
if true, the headers object is required.
5. `range` accept range request - default false
if true, the headers object is required.
6. `headers` the request headers object `req.headers`
if `range` and/or `conditionalRequest` are true, then the headers object is required.
you can pass the whole headers object, or only the conditional and range headers.
- default: `10800`
- type: `number`
7. `notFound` handler for non existing files
`notFound: false` - a rejection will be thrown (default).
`notFound: true` - empty body with response code '404' will be sent.
`notFound: <string>` - send a file with response code '404', the given string is the path to file.
if the path doesn't led to a file, a rejection will be thrown.
8. `implicitIndex` Check for index files if the request path is a directory. default: `false`
Pass an array of extensions to check against. e.g. _`["html", "css"]`_
Or simply pass `true` to check for html extension only.
caching period in seconds.
# Resolves
the response status code
# Rejects
'File Not Found' error.
Any other unexpected error
# Real World Example
```js
const
express = require("express"),
range = require("@ceicc/range"),
app = express();
#### `etag`
app.get('/', (req, res, next) => range('./public/index.html', res).catch(next));
- default: `true`
- type: `boolean`
app.get('/public/*', (req, res, next) => {
range('.' + req.path, res, {
headers: req.headers,
range: true,
conditional: true,
maxAge: 2592000, // 30 Days
notFound: './test/public/404.html',
}).catch(next);
});
add Etag header.
app.use((err, req, res, next) => {
console.dir(err);
if (!res.headersSent)
res.sendStatus(500);
});
#### `lastModified`
app.listen(2000);
```
- default: `true`
- type: `boolean`
add last-modified header.
#### `conditional`
- default: `true`
- type: `boolean`
whether to respect conditional requests or not.
#### `range`
- default: `true`
- type: `boolean`
accept range request.
#### `notFound`
- default: `true`
- type: `boolean|string`
a handler for non existing files
`notFound: false` `next` will be called.
`notFound: true` empty body with status code '404' will be sent.
`notFound: <string>` send a file with status code '404', the given string is the path to file.
if the path doesn't led to a file, `next` will be called.
***Note:*** The path is relative to the `baseDir` path.
#### `implicitIndex`
- default: `true`
- type: `boolean|Array<string>`
Check for index files if the request path is a directory.
Pass an array of extensions to check against. e.g. _`["html", "css"]`_
Or simply pass `true` to check for html extension only.
#### `baseDir`
- default: `'.'`
- type: `string`
the base dirctory.
#### `hushErrors`
- default: `false`
- type: `boolean`
Whether to ignore errors and reply with status code `500`, or pass the error to `next` function.
## Real World Example
```js
import express from "express"
import range from "@ceicc/range"
const app = express()
app.get('*', range({ baseDir: './public/' }))
app.use((error, req, res, next) => {
console.error(error)
res.sendStatus(500)
})
app.listen(80, () => console.log("server listening on localhost"))
```

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc