Cómo crear un limitador de velocidad con Node.js en la plataforma de aplicaciones

El autor seleccionó el Fondo de Ayuda COVID-19 para recibir una donación como parte del programa Write for DOnations .
Introducción
La limitación de velocidad administra el tráfico de su red y limita la cantidad de veces que alguien repite una operación en un período determinado, como usar una API. Un servicio sin una capa de seguridad contra el abuso del límite de velocidad es propenso a sobrecargarse y obstaculiza el funcionamiento adecuado de su aplicación para clientes legítimos.
En este tutorial, creará un servidor Node.js que verificará la dirección IP de la solicitud y también calculará la tasa de estas solicitudes comparando la marca de tiempo de las solicitudes por usuario. Si una dirección IP supera el límite que ha establecido para la aplicación, llamará a la API de Cloudflare y agregará la dirección IP a una lista. Luego, configurará una regla de firewall de Cloudflare que prohibirá todas las solicitudes con direcciones IP en la lista.
Al final de este tutorial, habrá creado un proyecto Node.js implementado en la plataforma de aplicaciones de DigitalOcean que protege un dominio enrutado de Cloudflare con limitación de velocidad.
Prerrequisitos
Antes de comenzar con esta guía, necesitarás:
- Una cuenta de Cloudflare . El plan gratuito de Cloudflare es suficiente para el tutorial. Si va a crear una cuenta nueva, elija el plan gratuito. Esta guía sobre cómo crear una cuenta de Cloudflare y agregar un sitio web puede ayudarlo.
- Un dominio registrado añadido a tu cuenta de Cloudflare. La guía sobre cómo mitigar los ataques DDoS contra tu sitio web con Cloudflare puede ayudarte a configurarlo. Este artículo sobre la introducción a la terminología, los componentes y los conceptos de DNS también puede ser de ayuda.
- Servidor Express básico con Node.js. Siga el artículo Cómo empezar a usar Node.js y Express hasta el paso 2.
- Una cuenta de GitHub y Git instalado en su máquina local. Es necesario tener una cuenta de GitHub y Git instalado, ya que enviará el código a GitHub para implementarlo desde la plataforma de aplicaciones de DigitalOcean.
- Una cuenta de DigitalOcean .
Paso 1: Configuración del proyecto Node.js e implementación en la plataforma de aplicaciones de DigitalOcean
En este paso, ampliará su servidor Express básico, enviará su código a un repositorio de GitHub e implementará su aplicación en App Platform.
Abra el directorio del proyecto del servidor Express básico con su editor de código. Cree un nuevo archivo con el nombre .gitignore
en el directorio raíz del proyecto. Agregue las siguientes líneas al .gitignore
archivo recién creado:
.gitignore
node_modules/.env
La primera línea de tu .gitignore
archivo es una directiva para que Git no realice un seguimiento del node_modules
directorio. Esto te permitirá mantener el tamaño de tu repositorio pequeño. Se node_modules
puede generar cuando sea necesario ejecutando el comando npm install
. La segunda línea evita que se realice un seguimiento de la variable de entorno file. Crearás el .env
archivo en los siguientes pasos.
Vaya a server.js
su editor de código y modifique las siguientes líneas de código:
servidor.js
...app.listen(process.env.PORT || 3000, () = { console.log(`Example app is listening on port ${process.env.PORT || 3000}`);});
El cambio para usar condicionalmente PORT
como una variable de entorno permite que la aplicación tenga dinámicamente el servidor ejecutándose en el asignado PORT
o lo use 3000
como respaldo.
Nota: La cadena console.log()
se encierra entre comillas simples (`) y no entre comillas. Esto le permite usar literales de plantilla , lo que brinda la capacidad de tener expresiones dentro de cadenas.
Visita la ventana de tu terminal y ejecuta tu aplicación:
- node server.js
La ventana de su navegador mostrará Successful response
. En su terminal, verá el siguiente resultado:
OutputExample app is listening on port 3000
Una vez que su servidor Express se esté ejecutando correctamente, ahora podrá implementarlo en App Platform.
Primero, inicializa git
en el directorio raíz del proyecto y envía el código a tu cuenta de GitHub. Navega hasta el panel de App Platform en el navegador y haz clic en el botón Create App . Elige la opción GitHub y autoriza con GitHub, si es necesario. Selecciona el repositorio de tu proyecto de la lista desplegable de proyectos que quieres implementar en App Platform. Revisa la configuración y luego dale un nombre a la aplicación. Para los fines de este tutorial, selecciona el plan Basic , ya que trabajarás en la fase de desarrollo de la aplicación. Una vez que estés listo, haz clic en Launch App .
A continuación, dirígete a la pestaña Configuración y haz clic en la sección Dominios . Agrega tu dominio enrutado a través de Cloudflare en el campo Nombre de dominio o subdominio . Selecciona la viñeta Administra tu dominio para copiar el CNAME
registro que usarás para agregarlo a la cuenta DNS de Cloudflare de tu dominio.
Una vez que hayas implementado tu aplicación en App Platform, dirígete al panel de control de tu dominio en Cloudflare en una nueva pestaña, ya que volverás al panel de control de App Platform más tarde. Navega hasta la pestaña DNS . Haz clic en el botón Agregar registro y selecciona CNAME como tu tipo , @ como raíz y pega el que CNAME
copiaste de App Platform. Haz clic en el botón Guardar , luego navega hasta la sección Dominios en la pestaña Configuración en el panel de control de tu App Platform y haz clic en el botón Agregar dominio .
Haga clic en la pestaña Implementaciones para ver los detalles de la implementación. Una vez que finalice la implementación, puede abrirla your_domain
para verla en el navegador. La ventana de su navegador mostrará: Successful response
. Navegue hasta la pestaña Registros de tiempo de ejecución en el panel de la Plataforma de aplicaciones y obtendrá el siguiente resultado:
OutputExample app is listening on port 8080
Nota: El número de puerto 8080
es el puerto predeterminado asignado por la plataforma de la aplicación. Puede anularlo modificando la configuración mientras revisa la aplicación antes de implementarla.
Ahora que su aplicación está implementada en App Platform, veamos cómo diseñar un caché para calcular las solicitudes al limitador de velocidad.
Paso 2: almacenar en caché la dirección IP del usuario y calcular las solicitudes por segundo
En este paso, almacenará la dirección IP de un usuario en un caché con una matriz de marcas de tiempo para monitorear las solicitudes por segundo de la dirección IP de cada usuario. Un caché es un almacenamiento temporal de datos que una aplicación utiliza con frecuencia. Los datos de un caché generalmente se guardan en hardware de acceso rápido como RAM (memoria de acceso aleatorio). El objetivo fundamental de un caché es mejorar el rendimiento de recuperación de datos al disminuir la necesidad de visitar la capa de almacenamiento más lenta que se encuentra debajo. Utilizará tres paquetes npm: node-cache
, is-ip
y request-ip
para ayudar en el proceso.
El request-ip
paquete captura la dirección IP del usuario que se utiliza para solicitar al servidor. El node-cache
paquete crea un caché en memoria que utilizará para realizar un seguimiento de las solicitudes del usuario. Utilizará el is-ip
paquete para comprobar si una dirección IP es una dirección IPv6. Instale el paquete node-cache
, is-ip
, y request-ip
a través de npm en su terminal.
- npm i node-cache is-ip request-ip
Abra el server.js
archivo en su editor de código y agregue las siguientes líneas de código a continuación const express = require('express');
:
servidor.js
...const requestIP = require('request-ip');const nodeCache = require('node-cache');const isIp = require('is-ip');...
La primera línea aquí toma el requestIP
módulo del request-ip
paquete que instalaste. Este módulo captura la dirección IP del usuario que se utilizó para solicitar al servidor. La segunda línea toma el nodeCache
módulo del node-cache
paquete. nodeCache
Crea un caché en memoria, que usarás para realizar un seguimiento de las solicitudes del usuario por segundo. La tercera línea toma el isIp
módulo del is-ip
paquete. Esto verifica si una dirección IP es IPv6, que formatearás según la especificación de Cloudflare para usar la notación CIDR .
Defina un conjunto de variables constantes en su server.js
archivo. Estas constantes se utilizarán en toda la aplicación.
servidor.js
...const TIME_FRAME_IN_S = 10;const TIME_FRAME_IN_MS = TIME_FRAME_IN_S * 1000;const MS_TO_S = 1 / 1000;const RPS_LIMIT = 2;...
TIME_FRAME_IN_S
es una variable constante que determinará el período durante el cual su aplicación promediará las marcas de tiempo del usuario. Aumentar el período aumentará el tamaño de la memoria caché, por lo tanto, consumirá más memoria. La TIME_FRAME_IN_MS
variable constante también determinará el período de tiempo durante el cual su aplicación promediará las marcas de tiempo del usuario, pero en milisegundos. MS_TO_S
es el factor de conversión que usará para convertir el tiempo en milisegundos a segundos. La RPS_LIMIT
variable es el límite de umbral de la aplicación que activará el limitador de velocidad y cambiará el valor según los requisitos de su aplicación. El valor 2
en la RPS_LIMIT
variable es un valor moderado que se activará durante la fase de desarrollo.
Con Express, puede escribir y utilizar funciones de middleware que tienen acceso a todas las solicitudes HTTP que llegan a su servidor. Para definir una función de middleware, deberá llamar app.use()
y pasarle una función. Cree una función denominada ipMiddleware
middleware.
servidor.js
...const ipMiddleware = async function (req, res, next) { let clientIP = requestIP.getClientIp(req); if (isIp.v6(clientIP)) { clientIP = clientIP.split(':').splice(0, 4).join(':') + '::/64'; } next();};app.use(ipMiddleware);...
La getClientIp()
función proporcionada por requestIP
toma el objeto de solicitud, req
del middleware, como parámetro. La .v6()
función proviene del is-ip
módulo y devuelve true
si el argumento que se le pasa es una dirección IPv6. Las listas de Cloudflare requieren la dirección IPv6 en /64
notación CIDR. Debe formatear la dirección IPv6 para que siga el formato: aaaa:bbbb:cccc:dddd::/64
. El .split(':')
método crea una matriz a partir de la cadena que contiene la dirección IP dividiéndola por el carácter :
. El .splice(0,4)
método devuelve los primeros cuatro elementos de la matriz. El .join(':')
método devuelve una cadena de la matriz combinada con el carácter :
.
La next()
llamada indica al middleware que vaya a la siguiente función de middleware, si existe alguna. En su ejemplo, llevará la solicitud a la ruta GET /
. Es importante incluir esto al final de su función. De lo contrario, la solicitud no avanzará desde el middleware.
Inicialice una instancia node-cache
de agregando la siguiente variable debajo de las constantes:
servidor.js
...const IPCache = new nodeCache({ stdTTL: TIME_FRAME_IN_S, deleteOnExpire: false, checkperiod: TIME_FRAME_IN_S });...
Con la variable constante IPCache
, estás anulando los parámetros predeterminados nativos nodeCache
con las propiedades personalizadas:
stdTTL
:El intervalo en segundos después del cual un par clave-valor de elementos de caché será expulsado de la caché.TTL
significa Time To Live y es una medida del tiempo después del cual la caché expira.deleteOnExpire
:Configúrelofalse
como escribirá una función de devolución de llamada personalizada para manejar elexpired
evento.checkperiod
: El intervalo en segundos después del cual se activa una verificación automática de elementos vencidos. El valor predeterminado es600
, y como la caducidad de los elementos de su aplicación está configurada en un valor menor, la verificación de caducidad también se realizará antes.
Para obtener más información sobre los parámetros predeterminados de node-cache
, encontrará útil la página de documentación del paquete npm node-cache . El siguiente diagrama le ayudará a visualizar cómo almacena datos un caché:
Ahora creará un nuevo par clave-valor para la nueva dirección IP y lo agregará a un par clave-valor existente si existe una dirección IP en la memoria caché. El valor es una matriz de marcas de tiempo correspondientes a cada solicitud realizada a su aplicación. En su server.js
archivo, cree la updateCache()
función debajo de la IPCache
variable constante para agregar la marca de tiempo de la solicitud a la memoria caché:
servidor.js
...const updateCache = (ip) = { let IPArray = IPCache.get(ip) || []; IPArray.push(new Date()); IPCache.set(ip, IPArray, (IPCache.getTtl(ip) - Date.now()) * MS_TO_S || TIME_FRAME_IN_S);};...
La primera línea de la función obtiene la matriz de marcas de tiempo para la dirección IP dada o, si es nula, se inicializa con una matriz vacía. En la siguiente línea, estás insertando la marca de tiempo actual capturada por la new Date()
función en la matriz. La .set()
función proporcionada por node-cache
toma tres argumentos: key
y value
. TTL
Esto TTL
anulará el TTL estándar establecido reemplazando el valor de stdTTL
de la IPCache
variable. Si la dirección IP ya existe en la memoria caché, utilizarás el TTL existente; de lo contrario, establecerás el TTL como TIME_FRAME_IN_S
.
El TTL del par clave-valor actual se calcula restando la marca de tiempo actual de la marca de tiempo de vencimiento. Luego, la diferencia se convierte a segundos y se pasa como tercer argumento a la .set()
función. La getTtl()
función . toma una clave y una dirección IP como argumento y devuelve el TTL del par clave-valor como una marca de tiempo. Si la dirección IP no existe en la memoria caché, devolverá undefined
y utilizará el valor de reserva de TIME_FRAME_IN_S
.
Nota: Necesita las marcas de tiempo de conversión de milisegundos a segundos, ya que JavaScript las almacena en milisegundos mientras que el node-cache
módulo usa segundos.
En el ipMiddleware
middleware, agregue las siguientes líneas después del if
bloque de código if (isIp.v6(clientIP))
para calcular las solicitudes por segundo de la dirección IP que llama a su aplicación:
servidor.js
... updateCache(clientIP); const IPArray = IPCache.get(clientIP); if (IPArray.length 1) { const rps = IPArray.length / ((IPArray[IPArray.length - 1] - IPArray[0]) * MS_TO_S); if (rps RPS_LIMIT) { console.log('You are hitting limit', clientIP); } }...
La primera línea agrega la marca de tiempo de la solicitud realizada por la dirección IP a la memoria caché llamando a la updateCache()
función que usted declaró. La segunda línea recopila la matriz de marcas de tiempo para la dirección IP. Si la cantidad de elementos en la matriz de marcas de tiempo es mayor que uno (el cálculo de solicitudes por segundo necesita un mínimo de dos marcas de tiempo) y las solicitudes por segundo son mayores que el valor de umbral que usted definió en las constantes, obtendrá console.log
la dirección IP. La rps
variable calcula las solicitudes por segundo dividiendo la cantidad de solicitudes con una diferencia de intervalo de tiempo y convierte las unidades a segundos.
Dado que había establecido deleteOnExpire
como valor predeterminado la propiedad false
en la IPCache
variable, ahora deberá controlar el expired
evento manualmente. node-cache
proporciona una función de devolución de llamada que se activa en caso de expired
evento. Agregue las siguientes líneas de código debajo de la IPCache
variable constante:
servidor.js
...IPCache.on('expired', (key, value) = { if (new Date() - value[value.length - 1] TIME_FRAME_IN_MS) { IPCache.del(key); }});...
.on()
es una función de devolución de llamada que acepta key
y value
del elemento vencido como argumentos. En su caché, value
es una matriz de marcas de tiempo de solicitudes. La línea resaltada verifica si el último elemento de la matriz está al menos TIME_FRAME_IN_S
en el pasado que en el presente. A medida que agrega elementos a su matriz de marcas de tiempo, si el último elemento value
está al menos TIME_FRAME_IN_S
en el pasado que en el presente, la .del()
función toma key
como argumento y elimina el elemento vencido de la caché.
En los casos en que algunos elementos de la matriz se encuentran al menos TIME_FRAME_IN_S
en el pasado y no en el presente, es necesario solucionarlo eliminando los elementos vencidos de la memoria caché. Agregue el siguiente código en la función de devolución de llamada después del if
bloque de código if (new Date() - value[value.length - 1] TIME_FRAME_IN_MS)
.
servidor.js
... else { const updatedValue = value.filter(function (element) { return new Date() - element TIME_FRAME_IN_MS; }); IPCache.set(key, updatedValue, TIME_FRAME_IN_S - (new Date() - updatedValue[0]) * MS_TO_S); }...
El filter()
método de matriz nativo de JavaScript proporciona una función de devolución de llamada para filtrar los elementos de su matriz de marcas de tiempo. En su caso, la línea resaltada busca elementos que estén menos TIME_FRAME_IN_S
en el pasado que en el presente. Luego, los elementos filtrados se agregan a la updatedValue
variable. Esto actualizará su caché con los elementos filtrados en la updatedValue
variable y un nuevo TTL. El TTL que coincida con el primer elemento en la updatedValue
variable activará la .on('expired')
función de devolución de llamada cuando la caché elimine el siguiente elemento. La diferencia de TIME_FRAME_IN_S
y el tiempo transcurrido desde la marca de tiempo de la primera solicitud en updatedValue
calcula el TTL nuevo y actualizado.
Con las funciones de middleware ahora definidas, visite su ventana de terminal y ejecute su aplicación:
- node server.js
Luego, visite localhost:3000
su navegador web. La ventana de su navegador mostrará: Successful response
. Actualice la página varias veces para llegar a RPS_LIMIT
. La ventana de su terminal mostrará:
OutputExample app is listening on port 3000You are hitting limit ::1
Nota: La dirección IP del host local se muestra como ::1
. Su aplicación capturará la IP pública de un usuario cuando se implemente fuera del host local.
Ahora, su aplicación puede realizar un seguimiento de las solicitudes de los usuarios y almacenar las marcas de tiempo en la memoria caché. En el siguiente paso, integrará la API de Cloudflare para configurar el firewall.
Paso 3: Configuración del firewall de Cloudflare
En este paso, configurará el firewall de Cloudflare para bloquear direcciones IP cuando se alcance el límite de velocidad, crear variables de entorno y realizar llamadas a la API de Cloudflare.
Visita el panel de Cloudflare en tu navegador, inicia sesión y ve a la página de inicio de tu cuenta. Abre Listas en la pestaña Configuraciones . Crea una nueva Lista con your_list
el nombre .
Nota: La sección Listas está disponible en la página del panel de tu cuenta de Cloudflare y no en la página del panel de tu dominio de Cloudflare.
Vaya a la pestaña Inicio y abra your_domain
el panel de control de . Abra la pestaña Firewall y haga clic en Crear una regla de Firewall en la sección Reglas de Firewall .your_rule_name
Indique al Firewall que desea identificarlo. En el campo , seleccione IP Source Address
en el menú desplegable is in list
Operador y Valor . En el menú desplegable Elegir una acción , seleccione Bloquear y haga clic en Implementar .your_list
Crea un .env
archivo en el directorio raíz del proyecto con las siguientes líneas para llamar a la API de Cloudflare desde tu aplicación:
.env
ACCOUNT_MAIL=your_cloudflare_login_mailAPI_KEY=your_api_keyACCOUNT_ID=your_account_idLIST_ID=your_list_id
Para obtener un valor para API_KEY
, navegue a la pestaña Tokens de API en la sección Mi perfil de su panel de control de Cloudflare. Haga clic en Ver en la sección Clave de API global e ingrese su contraseña de Cloudflare para verla. Visite la sección Listas en la pestaña Configuraciones en la página de inicio de la cuenta. Haga clic en Editar junto a your_list
la lista que creó. Obtenga ACCOUNT_ID
y LIST_ID
de la URL de your_list
en el navegador. La URL tiene el siguiente formato:https://dash.cloudflare.com/your_account_id/configurations/lists/your_list_id
Advertencia: Asegúrese de que el contenido .env
se mantenga confidencial y no se haga público. Asegúrese de que el .env
archivo esté incluido en el .gitignore
archivo que creó en el Paso 1. $
Instale el paquete axios
y dotenv
a través de npm en su terminal.
- npm i axios dotenv
Abra el server.js
archivo en su editor de código y agregue las siguientes líneas de código debajo de la nodeCache
variable constante:
servidor.js
...const axios = require('axios');require('dotenv').config();...
La primera línea aquí toma el axios
módulo del axios
paquete que instalaste. Usarás este módulo para hacer llamadas de red a la API de Cloudflare. La segunda línea requiere y configura el dotenv
módulo para habilitar la process.env
variable global que definirá los valores que colocaste en tu .env
archivo server.js
.
Agregue lo siguiente a la if (rps RPS_LIMIT)
condición ipMiddleware
anterior console.log('You are hitting limit', clientIP)
para llamar a la API de Cloudflare.
servidor.js
... const url = `https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/rules/lists/${process.env.LIST_ID}/items`; const body = [{ ip: clientIP, comment: 'your_comment' }]; const headers = { 'X-Auth-Email': process.env.ACCOUNT_MAIL, 'X-Auth-Key': process.env.API_KEY, 'Content-Type': 'application/json', }; try { await axios.post(url, body, { headers }); } catch (error) { console.log(error); }...
Ahora estás llamando a la API de Cloudflare a través de la URL para agregar un elemento, en este caso una dirección IP, a your_list
. La API de Cloudflare toma tu ACCOUNT_MAIL
y API_KEY
en el encabezado de la solicitud con la clave como X-Auth-Email
y X-Auth-Key
. El cuerpo de la solicitud toma una matriz de objetos con ip
como la dirección IP para agregar a la lista y un comment
con el valor your_comment
para identificar la entrada. Puedes modificar el valor de comment
con tu propio comentario personalizado. La solicitud POST realizada a través de axios.post()
se envuelve en un bloque try-catch para manejar los errores, si los hubiera, que puedan ocurrir. La axios.post
función toma el url
y body
un objeto con headers
para realizar la solicitud.
Cambie la clientIP
variable dentro de la ipMiddleware
función al probar las solicitudes de API con una dirección IP de prueba, ya 198.51.100.0/24
que Cloudflare no acepta la dirección IP del host local en sus listas.
servidor.js
...let clientIP = '198.51.100.0/24';...
Visita la ventana de tu terminal y ejecuta tu aplicación:
- node server.js
Luego, visite localhost:3000
su navegador web. La ventana de su navegador mostrará: Successful response
. Actualice la página varias veces para llegar a RPS_LIMIT
. La ventana de su terminal mostrará:
OutputExample app is listening on port 3000You are hitting limit ::1
Cuando hayas alcanzado el límite, abre el panel de Cloudflare y navega hasta la your_list
página de . Verás que la dirección IP que ingresaste en el código se agregó a la lista de Cloudflare denominada your_list
. La página de Firewall se mostrará después de enviar los cambios a GitHub.
$[warning] Advertencia: asegúrese de cambiar el valor de su clientIP
variable requestIP.getClientIp(req)
antes de implementar o enviar el código a GitHub.
Implementa tu aplicación confirmando los cambios y enviando el código a GitHub. Como has configurado la implementación automática, el código de GitHub se implementará automáticamente en la plataforma de aplicaciones de DigitalOcean. Como tu .env
archivo no se agrega a GitHub, tendrás que agregarlo a la plataforma de aplicaciones a través de la pestaña Configuración en la sección Variables de entorno a nivel de aplicación . Agrega el par clave-valor del archivo de tu proyecto .env
para que tu aplicación pueda acceder a su contenido en la plataforma de aplicaciones. Después de guardar las variables de entorno, abre your_domain
en tu navegador una vez que finalice la implementación y actualiza la página repetidamente para llegar al RPS_LIMIT
límite. Una vez que alcances el límite, el navegador mostrará la página Firewall de Cloudflare.
Vaya a la pestaña Registros de tiempo de ejecución en el panel de la plataforma de aplicaciones y verá el siguiente resultado:
Output...You are hitting limit your_public_ip
Puedes abrir your_domain
desde un dispositivo diferente o a través de una VPN para ver que el Firewall solo prohíbe la dirección IP en your_list
. Puedes eliminar la dirección IP desde your_list
tu panel de control de Cloudflare.
Nota: Ocasionalmente, el Firewall tarda algunos segundos en activarse debido a la respuesta almacenada en caché del navegador.
Ha configurado el firewall de Cloudflare para bloquear direcciones IP cuando los usuarios alcanzan el límite de velocidad al realizar llamadas a la API de Cloudflare.
Conclusión
En este artículo, creaste un proyecto Node.js implementado en la plataforma de aplicaciones de DigitalOcean conectada a tu dominio enrutado a través de Cloudflare. Protegiste tu dominio contra el uso indebido del límite de velocidad configurando una regla de firewall en Cloudflare. Desde aquí, puedes modificar la regla de firewall para mostrar el desafío de JS o CAPTCHA en lugar de prohibir al usuario. La documentación de Cloudflare detalla el proceso.
Deja una respuesta