paint-brush
¿Cómo compilar código Node.js usando Bytenode?por@pw.osama
43,396 lecturas
43,396 lecturas

¿Cómo compilar código Node.js usando Bytenode?

por Osama Abbas4m2020/02/19
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

Usando la herramienta bytenode, puede distribuir una versión binaria de sus archivos JavaScript. Esto le permite ocultar o proteger su código fuente de una mejor manera que la ofuscación u otros trucos no muy eficientes. El motor V8 (en el que se basa Node.js) usa lo que se llama: compilación justo a tiempo (JIT), donde el código JavaScript se compila justo antes de la ejecución, luego se optimizará posteriormente. Bytenode se puede usar globalmente, globalmente y localmente y localmente. Puede solicitar my-file.jsc como módulo.

Company Mentioned

Mention Thumbnail
featured image - ¿Cómo compilar código Node.js usando Bytenode?
Osama Abbas HackerNoon profile picture

En esta publicación, le mostraré cómo compilar "verdaderamente" el código Node.js (JavaScript) a V8 Bytecode. Esto le permite ocultar o proteger su código fuente de una mejor manera que la ofuscación u otros trucos no muy eficientes (como encriptar su código usando una clave secreta, que se incrustará en los archivos binarios de su aplicación, por eso dije "verdaderamente" arriba).

Entonces, usando la herramienta bytenode, puede distribuir una versión binaria .jsc de sus archivos JavaScript. También puede agrupar todos sus archivos .js usando Browserify, luego compilar ese único archivo en .jsc.

Compruebe el repositorio de bytenode en Github.

Larga historia corta..

  1. Instalar bytenode globalmente:
     [sudo] npm install -g bytenode
  2. Para compilar su archivo .js, ejecute este comando:
     bytenode --compile my-file.js my-file.jsc
  3. Instale bytenode en su proyecto también:
     npm install --save bytenode
  4. En su código, requiera bytenode,
     const bytenode = require('bytenode');

    registrarse
     .jsc
    extensión en el sistema de módulos Node.js. Es por eso que lo instalamos localmente también.
  5. Ahora puede solicitar my-file.jsc como módulo:
     const myFile = require('./my-file.jsc');

    También puedes eliminar
     my-file.js
    desde la construcción de producción.
  6. Y si quieres correr
     my-file.js
    usando
     bytenode
    clic:
     bytenode --run my-file.jsc

Ahora ya sabes cómo compilar

 .js
archivos, cómo requerir la versión compilada en su código y cómo ejecutar
 .jsc
archivos desde la terminal. Pasemos a la larga historia.

El motor V8 (en el que se basa Node.js) usa lo que se llama: compilación justo a tiempo (JIT), donde el código JavaScript se compila justo antes de la ejecución, luego se optimizará posteriormente.

Empezando desde

 Node.js
v5.7.0, el módulo vm introdujo una propiedad llamada
 produceCachedData
en
 vm.Script
Función constructora, así que si haces algo como esto:

 let helloScript = new vm. Script(' console . log ( "Hello World!" ) ;', { produceCachedData: true /* This is required for Node.js < 10.0.0 */ });

Ver en GitHub

Luego, obtenga el código de bytes o

 cachedData
buffer:

 let helloBuffer = helloScript.cachedData; // or in Node.js >= 10 let helloBuffer = helloScript.createCachedData();

Ver en GitHub

Este

 helloBuffer
se puede usar para crear un script idéntico que ejecutará las mismas instrucciones cuando se ejecute, pasándolo al
 vm.Script
Función constructora:

 let anotherHelloScript = new vm.Script( '' , { produceCachedData : true , cachedData : helloBuffer }); // This will fail!

Ver en GitHub

Pero esto fallará, el motor V8 se quejará del primer argumento (esa cadena vacía

 ''
), cuando comprueba si es el mismo código que el que se utilizó para generar
 helloBuffer
tampón en primer lugar. Sin embargo, este proceso de verificación es bastante fácil, lo que importa es la longitud del código. Entonces, esto funcionará:

 let anotherHelloScript = new vm.Script( ' ' .repeat( 28 ), { produceCachedData : true , cachedData : helloBuffer });

Ver en GitHub

Le damos una cadena vacía con la misma longitud (28) que el código original (

 console.log("Hello World!");
) . ¡Eso es todo!

Esto es interesante, utilizando el búfer almacenado en caché y la longitud del código original, pudimos crear un script idéntico. Ambos scripts se pueden ejecutar usando

 .runInThisContext();
función. Entonces, si los ejecutó:

 helloScript.runInThisContext(); anotherHelloScript.runInThisContext();

Ver en GitHub

verás '¡Hola Mundo!' dos veces.

(Tenga en cuenta que si ha utilizado la longitud incorrecta o si ha utilizado otra versión de Node.js/V8:

 anotherHelloScript
no se ejecutará, y su propiedad
 cachedDataRejected
se establecerá en
 true
).

Ahora a nuestro último paso, cuando definimos

 anotherHelloScript
usamos un valor codificado (28) como la longitud de nuestro código. ¿Cómo podemos cambiar esto, para que en el tiempo de ejecución no tengamos que saber exactamente cuánto tiempo tenía el código fuente original?

Después de investigar un poco en el código fuente de V8, descubrí que la información del encabezado se define aquí (en este archivo

 code-serializer.h
):

 // The data header consists of uint32_t-sized entries: // [0] magic number and (internally provided) external reference count // [1] version hash // [2] source hash // [3] cpu features // [4] flag hash

Ver en GitHub

Pero, el búfer de Node.js es una matriz de tipo Uint8Array. Esto significa que cada entrada de la

 uint32
matriz tomará cuatro entradas en el
 uint8
buffer. Entonces, la longitud de la carga útil (que es
 source
hash en el índice
 [2]
, cual es
 [8, 9, 10, 11]
bytes en el búfer de nodo) será:

 let payloadLengthBytes = whateverBufferYouHave.slice( 8 , 12 );

Ver en GitHub

Será algo como esto:

 <Buffer 1c 00 00 00>
, que es Little Endian, por lo que dice:
 0x0000001c
. Esa es la longitud de nuestro código (28 en decimal).

Para convertir estos cuatro bytes en un valor numérico, puede hacer algo como esto:

 firstByte + (secodeByte * 256) + (thirdByte * 256**2) + (forthByte * 256**3),

O de una manera más elegante, puedes hacer esto:

 let length = payloadLengthBytes.reduce( ( sum, number, power ) => sum += number * 256 **power , 0 );

Ver en GitHub

Como hice aquí en mi biblioteca, verifíquelo para ver la receta completa.

Alternativamente, podríamos usar

 buf.readIntLE()
función, que hace exactamente lo que queremos:

 let length = whateverBufferYouHave.readIntLE( 8 , 4 ); // 8 is the offset, 4 is the number of bytes to read

Ver en GitHub

Una vez que haya leído la longitud del código original (que se utilizó para generar el

 cachedData
buffer), ahora puede crear su script:

 let anotherHelloScript = new vm.Script( ' ' .repeat(length), { produceCachedData : true , cachedData : helloBuffer }); // later in your code anotherHelloScript.runInThisContext();

Ver en GitHub

Finalmente, ¿esta técnica tiene un impacto en el rendimiento? Bueno, en las versiones recientes de v8 (y Node.js), el rendimiento es casi el mismo. Usando el índice de octancia no encontré ninguna diferencia en el rendimiento. Sé que Google desaprobó octance (porque los navegadores y los motores JS estaban haciendo trampa), pero los resultados en nuestra situación son significativos, porque estamos comparando el mismo código en el mismo motor JS. Entonces, la respuesta final es: Bytenode NO tiene un impacto negativo en el rendimiento.

Consulte mi repositorio de Github , donde puede encontrar ejemplos de trabajo completos. He agregado un ejemplo para Electron (que no tiene ninguna protección de código fuente) y para NW.js (que tiene una herramienta similar a nwjc, pero solo funciona con el código del lado del navegador). Agregaré más ejemplos (y pruebas) pronto, con suerte.