Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@priestine/routing
Advanced tools
@priestine/routing
brings simple and eloquent routing to Node.js. It currently only works with Node.js http
server
yet new releases aim to support other APIs.
import { withHttpRouter, HttpRouter } from '@priestine/routing';
import { createServer } from 'http';
/**
* Create an empty router
*/
const router = HttpRouter.empty();
/**
* Define a set of functions
*/
// Curried for creating header builders
// `ctx` has { request: IncomingMessage, response: ServerResponse, intermediate: {/* Your data */}, error? }
const setHeader = (name) => (value) => (ctx) => {
ctx.response.setHeader(name, value);
};
// Specifically, a Content-Type header builder (we could make it in one go tho)
const setContentTypeHeader = setHeader('Content-Type');
// Anything to be passed to the next middleware should be put to ctx.intermediate
// to keep request and response Node.js-ly pure
const buildHello = (ctx) => {
ctx.intermediate.helloWorld = {
id: 'hello-world',
message: 'Hello World!',
}
};
// You don't need to actually return the ctx. It's done for you automatically
// In case you run async code, you must create and return a Promise that will resolve when necessary
const sayHello = (ctx) => ctx.response.end(JSON.stringify(ctx.intermediate.helloWorld));
/**
* Register them in the router
*/
router
.afterEach([
buildHello,
sayHello,
])
.get('/', [
setContentTypeHeader('application/json'),
])
.get(/^\/(\d+)\/?$/, [
setContentTypeHeader('text/html'),
])
;
/**
* Assign the router to serve Node.js HTTP server.
*/
createServer(withHttpRouter(router)).listen(3000);
// Go get http://localhost:3000 or http://localhost:3000/123123123
npm i --save @priestine/routing
or
yarn add @priestine/routing
Routing consists of a few components one should grasp to build routing efficiently:
HttpRouter
is a kind of fluent interface for registering new routes:router.register
accepts three required arguments:
IMiddlewareLike
that will be processed inside a Pipeline if current IncomingMessage
matches given url and methodsconst router = HttpRouter.empty();
router.register('/', ['POST', 'PUT'], [
(ctx) => ctx.response.setHeader('Content-Type', 'application/json'),
(ctx) => ctx.response.end('{ "success": true }'),
]);
router.get
(or one of get, post, put, patch, delete, options, head) registers a
route with the same method and accepts only url and array of middleware
const router = HttpRouter.empty();
router.get('/', [
(ctx) => ctx.response.setHeader('Content-Type', 'application/json'),
(ctx) => ctx.response.end('{ "success": true }'),
]);
router.beforeEach
and router.afterEach
accept array of middleware that will be executed before/after each
registered pipeline
const runBeforeEach = [
SetContentTypeHeader('application/json'),
ExtractQueryParams,
ExtractRequestParams,
];
const runAfterEach = [
FlushHead,
CreateJSONResponseBody,
EndResponse,
];
HttpRouter
.empty()
.beforeEach(runBeforeEach)
.afterEach(runAfterEach)
.get('/', [
SomeLogic,
])
;
router.concat
allows concatenating multiple routers into one main router to be used app-wide. This is done to
allow building modular routing with separated logic and then merging them to have single route map.
const MainRouter = HttpRouter.empty();
const ApiRouter = HttpRouter.empty();
MainRouter
.get('/', [
SetContentTypeHeader('text/html'),
GetTemplate('./templates/index.html'),
EndResponse,
])
;
ApiRouter
.post('/api/v1/user-actions/subscribe', [
SetContentTypeHeader('application/json'),
ParseRequestBody,
JSONParseLens(['intermediate', 'requestBody']),
UpsertUserSubscriptionStatus,
EndJSONResponse,
])
;
MainRouter.concat(ApiRouter);
Middleware is a reusable piece of business logic that encapsulates one specific step.
Middleware in @priestine/routing
can be functional or class-based. Each middleware is provided with ctx
argument:
ctx = {
request: IncomingMessage,
response: ServerResponse,
intermediate: { route: { url: string | RegExp, method: string } } & object,
error: Error | undefined,
}
For passing any computed data to next middleware, you should assign it to ctx.intermediate
that serves a purpose of
transferring data along the pipeline.
Function middleware is a fancy name for a function that accepts ctx
. You don't need to return ctx
after the
computation is finished as it is done for you automatically. If the computation is asynchronous, you nee to return it
wrapped into a Promise that resolves ctx
object or rejects with an error. This will force the Pipeline to await for
the Promise to resolve before moving on to the next middleware.
// Synchronous function middleware
const MyMiddleware = (ctx) => {
ctx.intermediate.id = 1;
}
// Asynchronous function middleware
const MyAsyncMiddleware = (ctx) => {
return new Promise((resolve) => setTimeout(() => { resolve(ctx); }, 200));
}
router.get('/', [
MyAsyncMiddleware,
MyMiddleware,
]);
Class-based middleware must implement IMiddleware
interface (it must have a $process
) method that accepts ctx
.
You don't need to return ctx
after the computation is finished as it is done for you automatically. If the computation
is asynchronous, you nee to return it wrapped into a Promise that resolves ctx
object or rejects with an error. This
will force the Pipeline to await for the Promise to resolve before moving on to the next middleware.
NOTE: When registering middleware in the Router, you must provide an instance of a class-based middleware.
class SetContentTypeHeader {
static applicationJson() {
return new MyMiddleware('application/json');
}
constructor(value) {
this.value = value;
}
$process(ctx) {
ctx.response.setHeader('Content-Type', this.value);
}
}
router.get('/', [
SetContentTypeHeader.applicationJson(),
])
Asynchronous middleware in the pipeline can be executed in either in parallel or sequentially. Each middleware can dictate which type of execution must be applied to it:
ctx.intermediate
key and awaited later where necessary. This approach
allows doing extra checks simultaneously and exit the pipeline if something is not right. Example:const GetCurrentPost = (ctx) => {
ctx.intermediate.post = db.collection('posts').findOne({ id: ctx.intermediate.params.id });
};
const GetPostComments = async (ctx) => {
ctx.intermediate.body = (await ctx.intermediate.post).comments;
};
HttpRouter.empty()
.get(/^\/posts\/(?<id>(\w+))\/comments/, [
GetCurrentPost,
IsAuthenticated,
GetPostComments,
])
;
In the example above GetCurrentPost
doesn't block the next piece of middleware that can trigger exiting the pipeline
if user is not authenticated thus not allowed to see posts in this imaginary scenario. This allows writing middleware
in arbitrary order in some cases.
ctx
:const IsAuthorized = (ctx) => new Promise((resolve, reject) => {
db.collection('users').findOne({ _id: ctx.intermediate.userId })
.then((u) => {
if (u.roles.includes('admin')) {
ctx.intermediate.userAuhorized = true;
resolve(ctx);
return;
}
reject(new UnauthorizedError());
})
;
});
HttpRouter.empty()
.get(/^\/posts\/(?<id>(\w+))\/comments/, [
IsAuthorized,
GetCurrentPost,
GetPostComments,
])
;
In the example above IsAuthorized
blocks the middleware until the ctx
is resolved. Thus, the Promise is rejected,
the pipeline will be exited and no further computation will be executed, being replaced with HttpRouter.onError
.
@priestine/routing
is written in TypeScript and provides generic context interfaces for providing typings of
ctx.intermediate
:
import { IGenericHttpContext } from '@priestine/routing';
interface IUserAware {
user: {
_id: string;
name: string;
};
}
export const GetUser = (ctx: IGenericHttpContext<IUserAware>) => {
ctx.intermediate.user = {
_id: '123123123123',
name: 'Test User',
};
};
The router itself cannot listen for IncomingMessage's and to make it work you need to wrap it into a helper
withHttpRouter
and pass it to http.createServer
as an argument:
import { createServer } from 'http';
import { HttpRouter, withHttpRouter } from '@priestine/routing';
const router = HttpRouter.empty()
.get('/', [
(ctx) => ctx.response.end('hi'),
])
;
createServer(withHttpRouter(router)).listen(3000);
Error handling in @priestine/routing
is done using the same concept of middleware yet error handlers are registered
statically to be available anywhere:
HttpRouter.onError([
(ctx) => ctx.response.setHeader('Content-Type', 'application/json'),
(ctx) => ctx.intermediate.body = {
success: false,
message: ctx.intermediate.error.message,
},
(ctx) => ctx.response.end(JSON.stringify(ctx.intermediate.body)),
]);
If something goes wrong, the middleware pipeline is aborted and a new pipeline of HttpRouter.errorHandlers
(provided
with HttpRouter.onError
) is issued using the ctx
of the pipeline that broke down. You can debug the nature of the
error by checking the contents of the ctx.intermediate
as it will have all the amendments up to the moment where it
crashed.
NOTE: beforeEach
and afterEach
are not applied to error handlers!
To force quitting current pipeline, you can either throw (synchronous middleware) or reject() (asynchronous middleware).
FAQs
Simple, declarative and dependency-free routing for Node.js
The npm package @priestine/routing receives a total of 3 weekly downloads. As such, @priestine/routing popularity was classified as not popular.
We found that @priestine/routing demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.