z-secure-service
Version:
A rate-limiting and API protection middleware that helps developers add security features to their APIs effortlessly.
135 lines (134 loc) • 5.73 kB
JavaScript
// index.ts
import axios from "axios";
import requestIp from 'request-ip';
import { generateApiKey } from "./findIp.js";
import { validateApiKey, validateIdentificationKey, validateRateLimiting, validateShield, } from "./validation.js";
class ZSecure {
constructor(options) {
if (!options.API_KEY) {
throw new Error("API key is required");
}
if (!options.rateLimitingRule && !options.shieldRule) {
throw new Error("At least one of rateLimitingRule or shieldRule must be defined");
}
this.API_KEY = options.API_KEY;
this.rateLimitingRule = options.rateLimitingRule;
this.shieldRule = options.shieldRule;
this.serverUrl = options.ZSECURE_URL ?? process.env.ZSECURE_URL ?? "http://localhost:3000";
this.identificationKey = generateApiKey(16); // Adjust length if necessary
this.logging = options.logs ?? false; // Default to false if not provided
this.bannedUsers = new Map();
this.resetTime = this.calculateResetTime();
if (this.logging) {
console.log("[ZSecure] Initialized with options:", options);
console.log("[ZSecure] Calculated reset time:", this.resetTime);
}
}
calculateResetTime() {
const rateLimitReset = (() => {
if (this.rateLimitingRule) {
if ("refillIntervalMs" in this.rateLimitingRule) {
return this.rateLimitingRule.refillIntervalMs; // TokenBucketRule
}
if ("windowMs" in this.rateLimitingRule) {
return this.rateLimitingRule.windowMs; // FixedWindowRule and SlidingWindowRule
}
if ("timeout" in this.rateLimitingRule) {
return this.rateLimitingRule.timeout; // LeakyBucketRule
}
}
return Infinity; // No rate limit defined
})();
const shieldReset = this.shieldRule?.blocktime ?? Infinity;
// Return the minimum of shield blocktime and rate limit reset time
return Math.min(rateLimitReset, shieldReset);
}
checkBannedUser(userId) {
const resetTime = this.bannedUsers.get(userId);
if (resetTime && resetTime > Date.now()) {
if (this.logging) {
console.log(`[ZSecure] User ${userId} is banned until ${new Date(resetTime).toISOString()}`);
}
return true;
}
return false;
}
async protect(req, userId, requestedTokens = 1) {
if (userId === null) {
userId = requestIp.getClientIp(req)?.toString();
}
if (typeof userId !== "string" && typeof userId !== "number") {
return {
isdenied: true,
message: "Correct userId is required",
};
}
// Check if the user is already banned
if (this.checkBannedUser(userId)) {
return { isdenied: true, status: 403, message: "User is banned" };
}
const shieldRequest = this.shieldRule
? {
params: JSON.stringify(req.params) ?? "",
url: JSON.stringify(req.url),
query: JSON.stringify(req.query) ?? "",
body: JSON.stringify(req.body) ?? "",
}
: undefined;
if (this.logging) {
console.log("[ZSecure] Shield request data:", shieldRequest);
}
const payload = {
key: this.API_KEY,
identificationKey: this.identificationKey,
userId: userId,
rateLimiting: this.rateLimitingRule
? { ...this.rateLimitingRule, requested: requestedTokens }
: undefined,
shield: this.shieldRule
? { ...this.shieldRule, request: shieldRequest }
: undefined,
};
try {
validateApiKey(payload.key);
validateIdentificationKey(payload.identificationKey);
validateRateLimiting(payload.rateLimiting);
validateShield(payload.shield);
}
catch (validationError) {
if (this.logging) {
console.error("[ZSecure] Validation Error:", validationError);
}
return {
isdenied: true,
status: 400,
message: validationError instanceof Error ? validationError.message : "Unknown validation error",
};
}
try {
const response = await axios.post(`${this.serverUrl}/protection`, payload);
if (this.logging) {
console.log("[ZSecure] Server response:", response.data);
}
// If the server denies the user, add them to the banned users map
if (response.data.isDenied || response.data.isDenied === 'true') {
const banResetTime = Date.now() + this.resetTime;
this.bannedUsers.set(userId, banResetTime);
if (this.logging) {
console.log(`[ZSecure] User ${userId} banned until ${new Date(banResetTime).toISOString()}`);
}
}
return response.data;
}
catch (error) {
if (this.logging) {
console.error("[ZSecure] Error in protect function:", error);
}
if (axios.isAxiosError(error) && error.response && error.response.status === 429) {
return { isdenied: true, status: 429, message: "Rate limit exceeded" };
}
return { isdenied: true, status: 500, message: "Internal server error" };
}
}
}
export default (options) => new ZSecure(options);