< Back to blog
PUBLISH DATE:
UPD:
Max SavoninMax SavoninChief Executive Officer at KeenEthics

Node.js Inject: How to Conduct and Why to Use

This technique is not as popular as it should be

NodeJS Dependency Injection

What Is Node JS Dependency Injection?

Dependency injection is a code wiring technique, which presumes that the dependencies are provided as an input by an external entity (passed as parameters or references), not hardcoded (created or required) inside a module. Dependencies are the objects or services that a module can use.

Dependency injections are a great benefit of JavaScript and of many other languages because this technique helps to create independent, scalable, and reusable components. At the same time, dependency injections in Node.js web development are not as popular as you might expect. 

Most would say that dependency injection NodeJS is not needed because you can import dependencies to a module using a command “require”. Yet, such an approach creates a code that is more difficult to maintain and to test.

NodeJS dependency injection solves this problem.

Why to Use Node JS Dependency Injection?

JavaScript dependency injection offers four essential benefits:

1. Simple maintenance

The code becomes more reusable and easier to configure since the dependency injection technique uncouples an object and its dependency. Imagine that there are three components: X, Y, and Z. X depends on Y, and Y depends on Z. What if you need to add something to Y or Z? If you do not use dependency injection, you have to do a lot of additional coding and refactoring in X as well. If you use dependency injections, adding anything becomes much simpler. After all, X only cares about Y and not about Z or anything else these depend upon.

2. Simple testing

The unit testing becomes much easier to perform. If Y is directly referenced in X, then, you cannot test X without testing Y. If Y is injected in X, you can create a mocked or stubbed Y to test X. For instance, if Y is a web service response, you can substitute it with a hard-coded file, which would work perfectly for testing purposes.

3. Simple management

When a large team is working on a single project, sooner or later, their tasks will intersect. With the use of dependency injections, team members isolate their components and become less dependent on others’ work. This means that the work on a few components can be conducted simultaneously and compatibility errors are less likely to occur.

4. Single responsibility

Single responsibility is the first of the five principles of the object-oriented design, called SOLID principles. Each component should be responsible for one thing. Otherwise, if your component solves two tasks, with each change in the code, you will need to test both scenarios. JavaScript dependency injection is a way to implement the single responsibility principle.

How to Perform NodeJS Injection?

There are different ways of how dependency injection can be performed. You can find plenty of primitive examples on the web to understand the essence of this technique. Meanwhile, I would like to show you how to execute a dependency injection in an actual code with the help of Frida framework.

Frida Framework

Frida by itself already has an example of code injection for Node.js, but it seems that it is a bit outdated and could work only with a certain Node.js version – GUIDE. It uses V8 embed code to execute a JS string. I have updated the code to Node.js v8.16.0 x64, but I will not elaborate on injection details here. You can easily find out more via the following link, so instead, let’s go to the injected code.

At first, let’s see, which Frida code we will use (you can get more details here).

  1. Module.findExportByName finds C exported function by its name. There is one little caveat – so-called “mangled names”. In short, when we are trying to investigate C++ compiled code, we could see a lot of names like ?GetCurrent@Isolate@v8@@SAPEAV12@XZ. First of our points is to find all required function names in a disassembler like IDA or OllyDbg and to get the memory address of it.

  2. NativeFunction(POINTER, RETURN_VALUE, ARGUMENTS) is used to define in-js bindings for a native function. Here, we need to describe correct arguments count and return values to call these functions later.

  3. NativeCallback is a code that will be executed after a native function call.

  4. WeakRef.bind is used to monitor specified pointer and the callback when it gets garbage-collected or on Frida detach. 

  5. Memory.alloc(N) allocates N bytes in memory and returns pointer for this memory region.

Concepts and Functions Used

Here, I want to define some concepts that I am going to use:

  • Isolate represents an isolated instance of the V8 engine. V8 isolates have completely separate states. Objects from one isolate cannot be used in another isolate. When V8 is initialized, a default isolate is implicitly created and entered. The embedder can create additional isolates and use them in parallel in multiple threads. An isolate can be entered by at most one thread at any given point in time. The Locker/Unlocker API must be used to synchronize. In short, it is a kind of sandbox, which contains its own states.

  • Context stands for a sandboxed execution context with its own set of built-in objects and functions.

  • HandleScope is a stack-allocated class, which governs a number of local handles. After a handle scope has been created, all local handles will be allocated within that handle scope until either the handle scope is deleted or another handle scope is created. If there is already a handle scope and a new one is created, all allocations will take place in the new handle scope until it is deleted. After that, new handles will again be allocated in the original handle scope. After the handle scope of a local handle has been deleted, the garbage collector will no longer track the object stored in the handle and may deallocate it. The behavior of accessing a handle for which the handle scope has been deleted is undefined.

  • Script stands for a compiled JavaScript script.

  • String is a JS string value.

  • Value is a superclass of all JavaScript values and objects.

Next, I describe the functions of libuv that we will use – a lib that allows using an async, event-driven style of programming:

  1. uv_async_init initializes handle, specifies the callback that will be executed inside the event loop;

  2. uv_default_loop takes default event loop;

  3. uv_async_send calls the callback;

  4. uv_unref destroys the object created;

  5. uv_close requests handle to be closed.

We will also use some V8 functions (formatted as real method name --> name of binded js function). In particular:

  1. V8::Isolate::GetCurrent(v8_Isolate_GetCurrent) takes the instance of a current Isolate.

  2. V8::Isolate::GetCurrentContext(v8_Isolate_GetCurrentContext) gets context from the current Isolate.

  3. V8::Context::Enter(v8_Context_Enter) enters this context. After entering a context, all code compiled and run is compiled and run in this context. If another context is already entered, this old context is saved so it can be restored when the new context is exited.

  4. V8::HandleScope::Init(v8_HandleScope_init) initializes HandleScope.

  5. V8::String::NewFromUtf8(v8_String_NewFromUtf8) gets JS string from const char *.

  6. V8::Script::Compile(v8_Script_Compile) compiles the specified script (bound to current context).

  7. V8::Script::Run(v8_Script_Run) runs the script returning the resulting value. If the script is context-independent (created using ::New), it will be run in the currently entered context. If it is context-specific (created using ::Compile), it will be run in the context in which it was compiled.

  8. v8::Value::Int32Value(v8_Value_Int32Value) gets C int from JS value. Value has a lot of functions for different types.

Code Listing

Let’s go through the code listing step by step:

createFunc is a helper that is used to create JS bind with a specified signature to reuse later.

try {
 const createFunc = (name, retval, args) => {
   const _ptr = Module.findExportByName(null, name);
   return new NativeFunction(_ptr, retval, args);
 }

All of the calls like const uv_default_loop = createFunc('uv_default_loop', 'pointer', []) are required to define JS bindings to native functions.


 const uv_default_loop = createFunc('uv_default_loop', 'pointer', []);
 const uv_async_init = createFunc('uv_async_init', 'int', ['pointer', 'pointer', 'pointer']);
 const uv_async_send = createFunc('uv_async_send', 'int', ['pointer']);
 const uv_unref = createFunc('uv_unref', 'void', ['pointer']);
 const v8_Isolate_GetCurrent = createFunc('?GetCurrent@Isolate@v8@@SAPEAV12@XZ', 'pointer', []);
 const v8_Isolate_GetCurrentContext = createFunc('?GetCurrentContext@Isolate@v8@@QEAA?AV?$Local@VContext@v8@@@2@XZ', 'pointer', ['pointer', 'pointer']);
 const v8_Context_Enter = createFunc('?Enter@Context@v8@@QEAAXXZ', 'pointer', ['pointer']);
 const v8_HandleScope_init = createFunc('??0HandleScope@v8@@QEAA@PEAVIsolate@1@@Z', 'void', ['pointer', 'pointer']);
 const v8_String_NewFromUtf8 = createFunc('?NewFromUtf8@String@v8@@SA?AV?$MaybeLocal@VString@v8@@@2@PEAVIsolate@2@PEBDW4NewStringType@2@H@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'int', 'int']);
 const v8_Script_Compile = createFunc('?Compile@ScriptCompiler@v8@@SA?AV?$MaybeLocal@VScript@v8@@@2@V?$Local@VContext@v8@@@2@PEAVSource@12@W4CompileOptions@12@@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
 const v8_Script_Run = createFunc('?Run@Script@v8@@QEAA?AV?$Local@VValue@v8@@@2@XZ', 'pointer', ['pointer', 'pointer']);
 const v8_Value_Int32Value = createFunc('?Int32Value@Value@v8@@QEBAHXZ', 'int64', ['pointer']);
 

scriptToExecute is our injected code.

const scriptToExecute = `((a, b)=>{
   console.log("Hello from Frida", a, b);
   return a+b;
 })(5, 17)`;

We define uv async handler, then bind processPending to the default event loop, then “wake up” that event loop for a callback call. Inside the callback, we receive an Isolate instance, initialize new HandleScope, and take current context. Then, we convert the JS string to V8 string, compile, and run it.  After all, we just extract C int value from the script execution result.

 const processPending = new NativeCallback(function () {
   const isolate = v8_Isolate_GetCurrent();
   const scope = Memory.alloc(128);
   v8_HandleScope_init(scope, isolate);
   const opts = Memory.alloc(128);
   const context = v8_Isolate_GetCurrentContext(isolate, opts);
   const item = scriptToExecute;
   const unkMem = Memory.alloc(128);
   const source = v8_String_NewFromUtf8(unkMem, isolate, Memory.allocUtf8String(item), 0, -1);
   const script = v8_Script_Compile(context, Memory.readPointer(context), source, NULL);
   const result = v8_Script_Run(Memory.readPointer(context), context);
   const intResult = v8_Value_Int32Value(Memory.readPointer(result));
   console.log('Result', intResult);
 }, 'void', ['pointer']);
 const onClose = new NativeCallback(function () { }, 'void', ['pointer']);
 const async = Memory.alloc(24);
 uv_async_init(uv_default_loop(), async, processPending);
 uv_async_send(async);
 uv_unref(async);
}
catch (ex) {
 console.log("Injected code execution error", ex);
}

To Wrap Up

Node js dependency injection is not as popular as it should be. It is designed to make the coding experience simpler and more time-efficient, not vice versa.

There are different ways of how you can perform this technique, and the one I offered you here is just one of them. You can find plenty of other examples on StackOverflow.

Yet, regardless of how you implement it, the benefits remain the same: simple maintenance, testing, management and single responsibility.

Do you have an idea for a Node JS app?

Does it seem to you that Node.js is the framework you need? Please, find out more about Node.js development services that we offer.

Written by Anton Trofimov, Full-stack JavaScript Developer, and Tania Matviiok, Content Manager for helping with the article @ KeenEthics