├── .DS_Store ├── README.md ├── api ├── utils.js ├── controller.js ├── middlewares.js └── model.js ├── ipban.sh ├── ipunban.sh ├── restore_banned_ips.sh ├── unbanall.sh ├── def.js ├── ecosystem.config.js ├── LICENSE ├── interface.js ├── .env.example ├── package.json ├── types.js ├── db ├── Adapter.js └── DBSqlite3.js ├── example_api └── multi.py ├── .gitignore ├── telegram └── tg.js ├── app.js ├── utils.js ├── README-EN.md ├── README-RU.md └── config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sm1ky/luIP-marzban/HEAD/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # luIP-marzban (Fixed) 2 | 3 | README | Установка | Manual: 4 | 5 | 6 | [README Instructions in English](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md) 7 | 8 | [README (Установка) на Русском](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md) 9 | -------------------------------------------------------------------------------- /api/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {ApiResponseType} params 4 | */ 5 | 6 | const response = (params) => { 7 | if (!params?.data) params.data = null; 8 | if (!params?.error) params.error = null; 9 | if (!params?.status) params.status = 0; 10 | 11 | return params; 12 | }; 13 | 14 | module.exports = { response }; 15 | -------------------------------------------------------------------------------- /ipban.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BLOCK_TIME_MINUTES=$2 4 | BLOCK_TIME_SECONDS=$((BLOCK_TIME_MINUTES * 60)) 5 | 6 | CSV_FILE="blocked_ips.csv" 7 | 8 | function block_ip() { 9 | local ip=$1 10 | local end_time=$(( $(date +%s) + BLOCK_TIME_SECONDS )) 11 | 12 | if ufw deny from $ip &> /dev/null; then 13 | ufw delete deny from $ip 14 | fi 15 | 16 | if ! awk -F',' -v ip="$ip" '$1 == ip' "$CSV_FILE" | grep -q .; then 17 | echo "$ip,$end_time" >> "$CSV_FILE" 18 | fi 19 | 20 | INTERFACE=$(ip route get 8.8.8.8 | awk '{print $5}') 21 | 22 | ufw insert 1 deny from $ip 23 | /usr/sbin/tcpkill -i $INTERFACE host $ip 24 | } 25 | 26 | block_ip $1 27 | -------------------------------------------------------------------------------- /ipunban.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CSV_FILE="blocked_ips.csv" 4 | 5 | function check_ip() { 6 | local ip=$1 7 | local current_time=$(date +%s) 8 | local end_time=$(awk -F',' -v ip="$ip" '$1 == ip {print $2}' "$CSV_FILE") 9 | 10 | if [ -n "$end_time" ]; then 11 | local block_time_seconds=$((end_time - current_time)) 12 | 13 | if ((block_time_seconds <= 0)); then 14 | if ufw status | grep -q $ip; then 15 | ufw delete deny from $ip 16 | echo "Unbanned IP: $ip" 17 | fi 18 | 19 | awk -F',' -v ip="$ip" '$1 != ip' "$CSV_FILE" > "$CSV_FILE.tmp" && mv "$CSV_FILE.tmp" "$CSV_FILE" 20 | fi 21 | fi 22 | } 23 | 24 | if [ -f "$CSV_FILE" ]; then 25 | while IFS=',' read -r ip _; do 26 | check_ip "$ip" 27 | done < "$CSV_FILE" 28 | fi 29 | -------------------------------------------------------------------------------- /restore_banned_ips.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | touch blocked_ips.csv 4 | CSV_FILE="blocked_ips.csv" 5 | 6 | function restore_banned_ips() { 7 | while IFS=',' read -r csv_line; do 8 | ip=$(echo "$csv_line" | cut -d ',' -f1) 9 | end_time=$(echo "$csv_line" | cut -d ',' -f2) 10 | 11 | current_time=$(date +%s) 12 | echo "Read from CSV: IP=$ip, End Time=$end_time" 13 | 14 | if (( current_time >= end_time )); then 15 | if ufw status | grep -F -q "$ip" || true; then 16 | ufw delete deny from $ip 17 | echo "Unbanned IP: $ip" 18 | fi 19 | else 20 | if ! ufw status | grep -F -q "$ip" || true; then 21 | ufw deny from $ip 22 | echo "Banned IP: $ip" 23 | fi 24 | fi 25 | done < "$CSV_FILE" 26 | } 27 | 28 | 29 | restore_banned_ips 30 | 31 | -------------------------------------------------------------------------------- /unbanall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CSV_FILE="blocked_ips.csv" 4 | TMP_FILE="blocked_ips_tmp.csv" 5 | touch "blocked_ips_tmp.csv" 6 | 7 | function restore_banned_ips() { 8 | while IFS=',' read -r csv_line; do 9 | ip=$(echo "$csv_line" | cut -d ',' -f1) 10 | end_time=$(echo "$csv_line" | cut -d ',' -f2) 11 | 12 | current_time=$(date +%s) 13 | echo "Read from CSV: IP=$ip, End Time=$end_time" 14 | 15 | if ufw status | grep -F -q "$ip" || true; then 16 | ufw delete deny from $ip 17 | echo "Unbanned IP: $ip" 18 | else 19 | # Если IP не заблокирован, добавляем его во временный файл 20 | echo "$csv_line" >> "$TMP_FILE" 21 | fi 22 | done < "$CSV_FILE" 23 | 24 | # Переименовываем временный файл в оригинальный 25 | mv "$TMP_FILE" "$CSV_FILE" 26 | } 27 | 28 | restore_banned_ips 29 | -------------------------------------------------------------------------------- /def.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { File } = require("./utils"); 3 | 4 | function def() { 5 | new File().ForceExistsFile( 6 | join(__dirname, "users.json"), 7 | JSON.stringify( 8 | [ 9 | ["user1", 1], 10 | ["user2", 2], 11 | ], 12 | 0, 13 | 1, 14 | ), 15 | ); 16 | 17 | new File().ForceExistsFile(join(__dirname, "blocked_ips.csv"), ""); 18 | // new File().ForceExistsFile( 19 | // join(__dirname, "session.json"), 20 | // JSON.stringify({ 21 | // api: { 22 | // key: "", 23 | // expireAt: 0, 24 | // }, 25 | // }), 26 | // ); 27 | 28 | if (process.env?.TARGET === "PROXY") { 29 | new File().ForceExistsFile( 30 | join(__dirname, "deactives.json"), 31 | JSON.stringify([]), 32 | ); 33 | } 34 | } 35 | 36 | module.exports = def; 37 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "luIP-marzban", 5 | script: "./app.js", 6 | instances: 1, 7 | max_memory_restart: "300M", 8 | 9 | out_file: "./out.log", 10 | error_file: "./error.log", 11 | merge_logs: true, 12 | max_size: "10M", 13 | max_files: 5, 14 | 15 | env_production: { 16 | NODE_ENV: "production", 17 | PORT: 3000, 18 | exec_mode: "cluster_mode", 19 | }, 20 | 21 | env_development: { 22 | NODE_ENV: "development", 23 | PORT: 4000, 24 | watch: true, 25 | ignore_watch: [ 26 | "./node_modules", 27 | "./package.json", 28 | "./package-lock.json", 29 | "./error.log", 30 | "./out.log", 31 | "./ban.sqlite", 32 | "./db.sqlite", 33 | ], 34 | }, 35 | }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 MohammadAli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /interface.js: -------------------------------------------------------------------------------- 1 | class DBInterface { 2 | read(email) { 3 | throw new Error("This method must be implemented in the class"); 4 | } 5 | 6 | readAll() { 7 | throw new Error("This method must be implemented in the class"); 8 | } 9 | 10 | deleteUser(email) { 11 | throw new Error("This method must be implemented in the class"); 12 | } 13 | 14 | deleteIp(email, ip) { 15 | throw new Error("This method must be implemented in the class"); 16 | } 17 | 18 | addUser(data) { 19 | throw new Error("This method must be implemented in the class"); 20 | } 21 | 22 | addIp(email, ip) { 23 | throw new Error("This method must be implemented in the class"); 24 | } 25 | 26 | deleteInactiveUsers() { 27 | throw new Error("This method must be implemented in the class"); 28 | } 29 | 30 | deleteLastIp(email) { 31 | throw new Error("This method must be implemented in the class"); 32 | } 33 | } 34 | 35 | class LuIPInterface { 36 | ban() { 37 | throw new Error("This method must be implemented in the class"); 38 | } 39 | 40 | unban() { 41 | throw new Error("This method must be implemented in the class"); 42 | } 43 | 44 | setUser() { 45 | throw new Error("This method must be implemented in the class"); 46 | } 47 | 48 | unsetUser() { 49 | throw new Error("This method must be implemented in the class"); 50 | } 51 | 52 | clear() { 53 | throw new Error("This method must be implemented in the class"); 54 | } 55 | } 56 | 57 | module.exports = { DBInterface, LuIPInterface }; 58 | -------------------------------------------------------------------------------- /api/controller.js: -------------------------------------------------------------------------------- 1 | const { AuthApiKey, Validator } = require("./middlewares"); 2 | const Model = require("./model"); 3 | const router = require("express").Router(); 4 | 5 | const validator = new Validator(); 6 | const model = new Model(); 7 | 8 | // Send your api username and password in json format to generate api_key 9 | router.post("/token", validator.token, (req, res) => { 10 | const result = model.token(); 11 | 12 | return res.json(result); 13 | }); 14 | 15 | // Send your proxy email and limit data in json format to be applied 16 | router.post("/add", AuthApiKey, validator.add, (req, res) => { 17 | const result = model.add(req.body); 18 | 19 | return res.json(result); 20 | }); 21 | 22 | // Send your proxy email and limit data in json format to be updated 23 | router.post("/update", AuthApiKey, validator.add, (req, res) => { 24 | const result = model.update(req.body); 25 | 26 | return res.json(result); 27 | }); 28 | 29 | // Send your proxy email data to be removed 30 | router.get("/delete/:email", AuthApiKey, (req, res) => { 31 | const result = model.delete(req.params); 32 | 33 | return res.json(result); 34 | }); 35 | 36 | // Send your proxy email data to get the user 37 | router.get("/user/:email", AuthApiKey, (req, res) => { 38 | const result = model.getUser(req.params); 39 | 40 | return res.json(result); 41 | }); 42 | 43 | // Clear users.csv file 44 | router.get("/clear", AuthApiKey, (req, res) => { 45 | const result = model.clear(); 46 | 47 | return res.json(result); 48 | }); 49 | 50 | module.exports = router; 51 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # You can set target to IP or PROXY. Read more in README.md 2 | TARGET=IP 3 | 4 | # Address Configuration 5 | 6 | ADDRESS=example.com 7 | PORT_ADDRESS=443 8 | SSL=false 9 | 10 | # ------------------------------ 11 | 12 | # Marzban Configuration 13 | 14 | P_USER=admin 15 | P_PASS=admin 16 | 17 | # ------------------------------ 18 | 19 | # App Configuration 20 | 21 | # By default: each proxy can only handle 1 client. 22 | MAX_ALLOW_USERS=2 23 | 24 | # By default: 4 minutes 25 | BAN_TIME=4 26 | 27 | # e.g: RU, CN, IR 28 | COUNTRY_CODE=IR 29 | 30 | # ------------------------------ 31 | 32 | # Advanced Configuration 33 | 34 | # By default: every 1 second 35 | FETCH_INTERVAL_LOGS_WS=1 36 | 37 | # By default: every 5 minutes 38 | CHECK_INACTIVE_USERS_DURATION=5 39 | 40 | # By default: every 1 minutes 41 | CHECK_IPS_FOR_UNBAN_USERS=1 42 | 43 | SSH_PORT=22 44 | 45 | TESTSCRIPT=false 46 | 47 | # ------------------------------ 48 | 49 | # API Configuration 50 | 51 | # true | false 52 | API_ENABLE=false 53 | 54 | # Secret key for your tokens 55 | API_SECRET=S@CrET 56 | 57 | # e.g: https://domain.com/api 58 | API_PATH=/api 59 | 60 | LISTEN_PATH=/socket 61 | 62 | # Set your username:password 63 | API_LOGIN=admin:admin 64 | 65 | # By default: 30 day 66 | API_EXPIRE_TOKEN_AT=30 67 | 68 | # e.g: https://domain.com:4000/api 69 | API_PORT=3000 70 | 71 | # ------------------------------ 72 | 73 | # Telegram bot Configuration 74 | 75 | TG_ENABLE=false 76 | 77 | # Your telegram bot token 78 | TG_TOKEN=1234566789:CVDfsdcxCGERdfsC3DFs42FScxdvxcvfsd 79 | 80 | # Get from @userinfobot 81 | TG_ADMIN=123456789 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luip-marzban", 3 | "version": "1.0.0", 4 | "description": "Limit users in each proxy configuration", 5 | "main": "app.js", 6 | "scripts": { 7 | "dev": "pm2 start ecosystem.config.js --env development", 8 | "start": "./restore_banned_ips.sh && pm2 start ecosystem.config.js --env production" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sm1ky/luIP-marzban.git" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/sm1ky/luIP-marzban/issues" 19 | }, 20 | "homepage": "https://github.com/sm1ky/luIP-marzban#readme", 21 | "dependencies": { 22 | "@grammyjs/files": "^1.0.4", 23 | "@grammyjs/menu": "^1.2.1", 24 | "@grammyjs/runner": "^2.0.3", 25 | "@types/socket.io": "^3.0.2", 26 | "axios": "^1.4.0", 27 | "body-parser": "^1.20.2", 28 | "crypto-js": "^4.1.1", 29 | "dotenv": "^16.3.1", 30 | "express": "^4.18.2", 31 | "grammy": "^1.18.1", 32 | "joi": "^17.9.2", 33 | "node-cron": "^3.0.2", 34 | "node-html-parser": "^6.1.5", 35 | "pm2": "^5.3.0", 36 | "reconnecting-websocket": "^4.4.0", 37 | "socket.io": "^4.7.2", 38 | "socks-proxy-agent": "^8.0.1", 39 | "sqlite3": "^5.1.6", 40 | "ws": "^8.13.0" 41 | }, 42 | "devDependencies": { 43 | "@types/axios": "^0.14.0", 44 | "@types/body-parser": "^1.19.2", 45 | "@types/crypto-js": "^4.1.1", 46 | "@types/dotenv": "^8.2.0", 47 | "@types/express": "^4.17.17", 48 | "@types/node": "^20.4.9", 49 | "@types/ws": "^8.5.5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | // Globally 2 | 3 | /** 4 | * @typedef {Object} IPSDataType 5 | * @property {string} ip 6 | * @property {number} port 7 | * @property {string} date 8 | * @property {boolean} first 9 | */ 10 | 11 | /** 12 | * @typedef {IPSDataType[]} IPSType 13 | */ 14 | 15 | /** 16 | * @typedef {Object} DataInterfaceType 17 | * @property {string} email 18 | * @property {number} allowedUsers 19 | * @property {IPSType} ips 20 | */ 21 | 22 | /** 23 | * @typedef {object} WebSocketConfigType 24 | * @property {string} url 25 | * @property {string} accessToken 26 | * @property {object} DB 27 | * @property {string} node 28 | * @property {Object} api 29 | * @property {import("./config").Socket} socket 30 | */ 31 | 32 | /** 33 | * @typedef {Object} NewUserIpType 34 | * @property {string} ip 35 | * @property {string} port 36 | * @property {string} email 37 | */ 38 | 39 | /** 40 | * @typedef {Object} BanIpConfigAddType 41 | * @property {string} ip 42 | */ 43 | 44 | // API 45 | 46 | /** 47 | * @typedef {Object} ApiSetTokenType 48 | * @property {string} username 49 | * @property {string} password 50 | */ 51 | 52 | /** 53 | * @typedef {"INVALID" | "NOT_MATCH" | "AUTH" | "DUPLICATE" | "NOT_FOUND"} ApiErrorTypes 54 | */ 55 | 56 | /** 57 | * @typedef {Object} ApiResponseErrorType 58 | * @property {ApiErrorTypes} type 59 | * @property {string} reason 60 | */ 61 | 62 | /** 63 | * @typedef {Object} ApiResponseType 64 | * @property {Object | null} data 65 | * @property {ApiResponseErrorType} error 66 | * @property {0|1} status 67 | */ 68 | 69 | /** 70 | * @typedef {Object} IpGuardType 71 | * @property {*} banDB 72 | * @property {import("./config").Socket} socket 73 | * @property {import("./config").Api} api 74 | * @property {import("./db/Adapter").DBAdapter} db 75 | */ 76 | -------------------------------------------------------------------------------- /db/Adapter.js: -------------------------------------------------------------------------------- 1 | const { DBInterface } = require("../interface"); 2 | 3 | class DBAdapter { 4 | /** 5 | * @type {DataInterfaceType} 6 | */ 7 | DataInterface = {}; 8 | 9 | /** 10 | * @param {DBInterface} database 11 | */ 12 | constructor(database) { 13 | this.database = database; 14 | this.DataInterface = database.DataInterface; 15 | } 16 | 17 | /** 18 | * @description Read the user from the database 19 | * @param {string} email User email 20 | * @returns {DataInterfaceType | Promise} 21 | */ 22 | read(email) { 23 | return this.database.read(email); 24 | } 25 | 26 | /** 27 | * @description Read all database data. 28 | * @returns {DataInterfaceType[]} 29 | */ 30 | readAll() { 31 | return this.database.readAll(); 32 | } 33 | 34 | /** 35 | * @description Delete a user from the database 36 | * @param {string} email User email 37 | * @returns {void} 38 | */ 39 | deleteUser(email) { 40 | return this.database.deleteUser(email); 41 | } 42 | 43 | /** 44 | * @description Delete a ip from the user field 45 | * @param {string} email User email 46 | * @param {string} ip websocket connection ip 47 | * @returns {void} 48 | */ 49 | deleteIp(email, ip) { 50 | return this.database.deleteIp(email, ip); 51 | } 52 | 53 | /** 54 | * @description Add a user to database 55 | * @param {DataInterfaceType} data 56 | * @returns {void} 57 | */ 58 | addUser(data) { 59 | return this.database.addUser(data); 60 | } 61 | 62 | /** 63 | * @description Add a ip from the user field 64 | * @param {string} email User email 65 | * @param {IPSDataType} ip ip data 66 | * @returns {void} 67 | */ 68 | addIp(email, ip) { 69 | return this.database.addIp(email, ip); 70 | } 71 | 72 | /** 73 | * @description delete inactive users from database record 74 | * @returns {void} 75 | */ 76 | deleteInactiveUsers() { 77 | return this.database.deleteInactiveUsers(); 78 | } 79 | 80 | /** 81 | * @param {string} email 82 | * @returns {void} 83 | */ 84 | deleteLastIp(email) { 85 | return this.database.deleteLastIp(email); 86 | } 87 | } 88 | 89 | module.exports = { DBAdapter }; 90 | -------------------------------------------------------------------------------- /example_api/multi.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | BASE_URL = "http://127.0.0.1:3000/api" 4 | 5 | USERNAME = "admin" 6 | PASSWORD = "admin" 7 | 8 | def get_token(): 9 | endpoint = "/token" 10 | data = {"username": USERNAME, "password": PASSWORD} 11 | response = requests.post(BASE_URL + endpoint, json=data) 12 | return response.json() 13 | 14 | def add_user(email, limit, api_key): 15 | endpoint = "/add" 16 | headers = {"api_key": api_key} 17 | data = {"email": email, "limit": limit} 18 | response = requests.post(BASE_URL + endpoint, headers=headers, json=data) 19 | returned = response.json() 20 | if returned['status'] == 0: 21 | # ОБНОВЛЕНИЕ ЛИМИТОВ ЕСЛИ ПОЛЬЗОВАТЕЛЬ В БАЗЕ 22 | returned = update_user(name=email, limit=limit, api_key=api_key) 23 | return returned 24 | 25 | def update_user(email, limit, api_key): 26 | endpoint = "/update" 27 | headers = {"api_key": api_key} 28 | data = {"email": email, "limit": limit} 29 | response = requests.post(BASE_URL + endpoint, headers=headers, json=data) 30 | return response.json() 31 | 32 | def delete_user(email, api_key): 33 | endpoint = f"/delete/{email}" 34 | headers = {"api_key": api_key} 35 | response = requests.get(BASE_URL + endpoint, headers=headers) 36 | return response.json() 37 | 38 | def clear_users(api_key): 39 | endpoint = "/clear" 40 | headers = {"api_key": api_key} 41 | response = requests.get(BASE_URL + endpoint, headers=headers) 42 | return response.json() 43 | 44 | if __name__ == "__main__": 45 | # Получение токена 46 | token_data = get_token() 47 | api_key = token_data["data"]["api_key"] 48 | print(f"Token: {api_key}") 49 | 50 | # Пример добавления нового пользователя 51 | result_add = add_user("admin", 2, api_key) 52 | print("Add User Result:", result_add) 53 | 54 | # # Пример обновления данных пользователя 55 | # result_update = update_user("admin", 2, api_key) 56 | # print("Update User Result:", result_update) 57 | 58 | # # Пример удаления пользователя 59 | # result_delete = delete_user("test@example.com", api_key) 60 | # print("Delete User Result:", result_delete) 61 | 62 | # Пример очистки файла users.csv 63 | # result_clear = clear_users(api_key) 64 | 65 | 66 | # print("Clear Users Result:", result_clear) -------------------------------------------------------------------------------- /api/middlewares.js: -------------------------------------------------------------------------------- 1 | const { response } = require("./utils"); 2 | const crypto = require("crypto-js"); 3 | const Joi = require("joi"); 4 | 5 | const AuthApiKey = (req, res, next) => { 6 | let apiKey = (req.headers["api_key"] || "").trim(); 7 | 8 | if (!apiKey) 9 | return res.status(403).json( 10 | response({ 11 | error: { 12 | type: "AUTH", 13 | reason: "api_key Not found!", 14 | }, 15 | }), 16 | ); 17 | 18 | let decryptedKey = crypto.AES.decrypt( 19 | apiKey, 20 | process.env.API_SECRET, 21 | ).toString(crypto.enc.Utf8); 22 | 23 | const parseKey = JSON.parse(decryptedKey); 24 | 25 | if (Date.now() > +parseKey.expireAt) 26 | return res.status(403).json( 27 | response({ 28 | error: { 29 | type: "AUTH", 30 | reason: "api_key expired Please get a new api_key", 31 | }, 32 | }), 33 | ); 34 | 35 | return next(); 36 | }; 37 | 38 | class Validator { 39 | token(req, res, next) { 40 | /** 41 | * @type {ApiSetTokenType} 42 | */ 43 | const data = req.body; 44 | 45 | const schema = Joi.object({ 46 | username: Joi.string().required(), 47 | password: Joi.string().required(), 48 | }); 49 | 50 | const { error } = schema.validate(data); 51 | 52 | if (error) { 53 | return res.status(403).json( 54 | response({ 55 | error: { 56 | type: "INVALID", 57 | reason: error.message, 58 | }, 59 | }), 60 | ); 61 | } 62 | 63 | if (process.env.API_LOGIN !== `${data.username}:${data.password}`) 64 | return res.status(403).json( 65 | response({ 66 | error: { 67 | type: "NOT_MATCH", 68 | reason: "Username or password doesn't match", 69 | }, 70 | }), 71 | ); 72 | 73 | return next(); 74 | } 75 | 76 | add(req, res, next) { 77 | const schema = Joi.object({ 78 | email: Joi.string().required(), 79 | limit: Joi.number().required(), 80 | }); 81 | 82 | const data = req.body; 83 | 84 | const { error } = schema.validate(data); 85 | 86 | if (error) { 87 | return res.status(403).json( 88 | response({ 89 | error: { 90 | type: "INVALID", 91 | reason: error.message, 92 | }, 93 | }), 94 | ); 95 | } 96 | 97 | return next(); 98 | } 99 | } 100 | 101 | module.exports = { 102 | AuthApiKey, 103 | Validator, 104 | }; 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # db 2 | *.sqlite 3 | 4 | users.json 5 | blocked_ips.csv 6 | users.csv 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | .pnpm-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional stylelint cache 65 | .stylelintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variable files 83 | .env 84 | .env.development.local 85 | .env.test.local 86 | .env.production.local 87 | .env.local 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # vuepress v2.x temp and cache directory 111 | .temp 112 | .cache 113 | 114 | # Docusaurus cache and generated files 115 | .docusaurus 116 | 117 | # Serverless directories 118 | .serverless/ 119 | 120 | # FuseBox cache 121 | .fusebox/ 122 | 123 | # DynamoDB Local files 124 | .dynamodb/ 125 | 126 | # TernJS port file 127 | .tern-port 128 | 129 | # Stores VSCode versions used for testing VSCode extensions 130 | .vscode-test 131 | 132 | # yarn v2 133 | .yarn/cache 134 | .yarn/unplugged 135 | .yarn/build-state.yml 136 | .yarn/install-state.gz 137 | .pnp.* 138 | -------------------------------------------------------------------------------- /telegram/tg.js: -------------------------------------------------------------------------------- 1 | const { Bot, session, InputFile } = require("grammy"); 2 | const { run } = require("@grammyjs/runner"); 3 | const { SocksProxyAgent } = require("socks-proxy-agent"); 4 | const { Menu } = require("@grammyjs/menu"); 5 | const { hydrateFiles } = require("@grammyjs/files"); 6 | const fs = require("fs"); 7 | const { join } = require("path"); 8 | const { File } = require("../utils"); 9 | 10 | function tg() { 11 | if (process.env.TG_ENABLE === "false") return; 12 | 13 | let bot = null; 14 | 15 | if (process.env.NODE_ENV.includes("development")) { 16 | const socksAgent = new SocksProxyAgent("socks://127.0.0.1:10808"); 17 | 18 | bot = new Bot(process.env.TG_TOKEN, { 19 | client: { 20 | baseFetchConfig: { 21 | agent: socksAgent, 22 | }, 23 | }, 24 | }); 25 | } else bot = new Bot(process.env.TG_TOKEN); 26 | 27 | bot.use((ctx, next) => { 28 | if (+ctx.chat.id !== +process.env.TG_ADMIN) return; 29 | 30 | return next(); 31 | }); 32 | 33 | bot.api.setMyCommands([ 34 | { 35 | command: "start", 36 | description: "Start the bot", 37 | }, 38 | ]); 39 | 40 | bot.api.config.use(hydrateFiles(bot.token)); 41 | 42 | const initial = () => ({ 43 | waitingFor: "", 44 | }); 45 | 46 | bot.use(session({ initial })); 47 | 48 | const menu = new Menu("main-menu") 49 | .text("Import backup", async (ctx) => { 50 | ctx.reply("Send me users.csv or users.json or both files"); 51 | 52 | ctx.session.waitingFor = "FILE"; 53 | }) 54 | .row() 55 | .text("Export backup", async (ctx) => { 56 | await ctx.replyWithDocument( 57 | new InputFile(join(__dirname, "../", "users.json"), "users.json"), 58 | ); 59 | 60 | try { 61 | await ctx.replyWithDocument( 62 | new InputFile(join(__dirname, "../", "users.csv"), "users.csv"), 63 | ); 64 | } catch (e) {} 65 | }); 66 | 67 | bot.use(menu); 68 | 69 | bot.command("start", (ctx) => { 70 | ctx.reply( 71 | ` 72 | Hi 73 | How can i help you ? 74 | `, 75 | { 76 | reply_markup: menu, 77 | }, 78 | ); 79 | }); 80 | 81 | bot.on( 82 | ":document", 83 | (ctx, next) => { 84 | if (ctx.session.waitingFor === "FILE") return next(); 85 | }, 86 | async (ctx) => { 87 | const file = await ctx.getFile(); 88 | 89 | let filename = ""; 90 | 91 | if (file.file_path.includes(".csv")) { 92 | filename = join(__dirname, "../", "users.csv"); 93 | } else if (file.file_path.includes(".json")) { 94 | filename = join(__dirname, "../", "users.json"); 95 | } else return ctx.reply("Unknown file type"); 96 | 97 | fs.rmSync(filename); 98 | await file.download(filename); 99 | 100 | ctx.reply("The backup was registered successfully"); 101 | }, 102 | ); 103 | 104 | const runner = run(bot); 105 | 106 | if (runner.isRunning()) console.log("Bot is running"); 107 | 108 | bot.catch((err) => { 109 | console.log(err); 110 | }); 111 | 112 | globalThis.bot = bot; 113 | 114 | return bot; 115 | } 116 | 117 | module.exports = tg; 118 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const { Server } = require("./utils"); 2 | const { Ws, Api, Socket, IPGuard } = require("./config"); 3 | const DBSqlite3 = require("./db/DBSqlite3"); 4 | const def = require("./def"); 5 | const app = require("express")(); 6 | const nodeCron = require("node-cron"); 7 | const { DBAdapter } = require("./db/Adapter"); 8 | const { exec } = require("child_process"); 9 | const { join } = require("path"); 10 | const router = require("./api/controller"); 11 | const bodyParser = require("body-parser"); 12 | const tg = require("./telegram/tg"); 13 | const server = require("http").createServer(app); 14 | 15 | require("dotenv").config(); 16 | 17 | const api = new Api(); 18 | const DBType = new DBSqlite3(); 19 | 20 | DBType.deleteInactiveUsers(); 21 | 22 | def(); 23 | 24 | const socket = new Socket({ 25 | server, 26 | callback: (io) => { 27 | console.log("Start new socket server [app.js]."); 28 | const clientIp = io.handshake.address; 29 | const ipv4Address = clientIp.includes('::ffff:') ? clientIp.split('::ffff:')[1] : clientIp; 30 | console.log(`[SOCKET.IO LOG] Client connected from IP: ${ipv4Address}`); 31 | }, 32 | }); 33 | 34 | (async () => { 35 | tg(); 36 | 37 | api.create(); 38 | 39 | await api.token(); 40 | 41 | const url = new Server().CleanAddress( 42 | `${process.env.ADDRESS}:${process.env.PORT_ADDRESS}`, 43 | false, 44 | false, 45 | ); 46 | 47 | const wsData = { 48 | url, 49 | accessToken: `${api.accessToken}`, 50 | DB: DBType, 51 | api, 52 | socket, 53 | }; 54 | 55 | const ws = new Ws(wsData); 56 | 57 | const nodes = await api.getNodes(); 58 | 59 | let websockets = []; 60 | 61 | const wss = ws.logs(); 62 | 63 | websockets.push(wss); 64 | 65 | for (let i in nodes) { 66 | const node = nodes[i]; 67 | 68 | const ws = new Ws({ ...wsData, node }); 69 | 70 | const wss = ws.logs(); 71 | 72 | websockets.push(wss); 73 | } 74 | 75 | nodeCron.schedule(`*/30 * * * *`, async () => { 76 | await api.token(); 77 | 78 | for (let i in websockets) { 79 | const ws = websockets[i]; 80 | 81 | ws.access_token = api.accessToken; 82 | } 83 | }); 84 | })(); 85 | 86 | 87 | nodeCron.schedule( 88 | `*/${process.env.CHECK_INACTIVE_USERS_DURATION} * * * *`, 89 | () => { 90 | const db = new DBAdapter(DBType); 91 | db.deleteInactiveUsers(); 92 | }, 93 | ); 94 | 95 | if (process.env?.TARGET === "IP") { 96 | nodeCron.schedule( 97 | `*/${process.env.CHECK_IPS_FOR_UNBAN_USERS} * * * *`, 98 | () => { 99 | 100 | socket.UnbanIP(); 101 | 102 | exec("bash ./ipunban.sh", (error, stdout, stderr) => { 103 | if (error) { 104 | console.error(`Error executing ipunban.sh: ${error.message}`); 105 | return; 106 | } 107 | if (stderr) { 108 | console.error(`ipunban.sh stderr: ${stderr}`); 109 | return; 110 | } 111 | // console.log(`ipunban.sh stdout: ${stdout}`); 112 | }); 113 | }, 114 | ); 115 | } 116 | 117 | if (process.env?.TARGET === "PROXY") { 118 | nodeCron.schedule( 119 | `*/${process.env.CHECK_IPS_FOR_UNBAN_USERS} * * * *`, 120 | () => { 121 | // console.log("Check for unban users"); 122 | new IPGuard({ 123 | api, 124 | db: DBType, 125 | }).activeUsersProxy(); 126 | }, 127 | ); 128 | } 129 | 130 | 131 | // Api server 132 | if (process.env?.API_ENABLE === "true") { 133 | const PORT = process.env?.API_PORT || 3000; 134 | 135 | let address = new Server().CleanAddress( 136 | `${process.env.ADDRESS}:${PORT}`, 137 | false, 138 | true, 139 | ); 140 | 141 | address = `${address}${process.env.API_PATH}`; 142 | 143 | app.use( 144 | bodyParser.urlencoded({ 145 | extended: true, 146 | }), 147 | ); 148 | app.use(bodyParser.json()); 149 | 150 | app.use(`${process.env.API_PATH}`, router); 151 | 152 | server.listen(PORT, () => { 153 | console.log(`Server running: ${address}`); 154 | }); 155 | } 156 | -------------------------------------------------------------------------------- /api/model.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto-js"); 2 | const { File } = require("../utils"); 3 | const { join } = require("path"); 4 | const fs = require("fs"); 5 | const { response } = require("./utils"); 6 | const DBSqlite3 = require("../db/DBSqlite3"); 7 | 8 | class Model { 9 | constructor() { 10 | this.usersCsvPath = join(__dirname, "../", "users.csv"); 11 | this.usersJsonPath = join(__dirname, "../", "users.json"); 12 | } 13 | 14 | token() { 15 | const expireAt = 16 | Date.now() + 1000 * 60 * 60 * 24 * +process.env.API_EXPIRE_TOKEN_AT; 17 | 18 | const token = crypto.AES.encrypt( 19 | JSON.stringify({ 20 | expireAt, 21 | }), 22 | process.env.API_SECRET, 23 | ).toString(); 24 | 25 | return response({ 26 | data: { api_key: token }, 27 | status: 1, 28 | }); 29 | } 30 | 31 | /** 32 | * @typedef {Object} ApiAddDataType 33 | * @property {string} email 34 | * @property {string} limit 35 | * 36 | * @param {ApiAddDataType} data 37 | */ 38 | 39 | add(data) { 40 | let file = new File().GetJsonFile(this.usersJsonPath); 41 | 42 | if (file.some((item) => item[0] === data.email) === true) 43 | return response({ 44 | error: { 45 | type: "DUPLICATE", 46 | reason: "This email already exists", 47 | }, 48 | }); 49 | 50 | file.push([data.email, data.limit]); 51 | 52 | fs.writeFileSync(this.usersJsonPath, JSON.stringify(file)); 53 | 54 | return response({ 55 | status: 1, 56 | }); 57 | } 58 | 59 | /** 60 | * @typedef {Object} ApiAddDataType 61 | * @property {string} email 62 | * @property {string} limit 63 | * 64 | * @param {ApiAddDataType} data 65 | */ 66 | 67 | update(data) { 68 | let file = new File().GetJsonFile(this.usersJsonPath); 69 | 70 | let check = false; 71 | for (const el of file) { 72 | if (el.includes(data.email)) 73 | {check = true}; 74 | } 75 | 76 | if (check === false) 77 | return response({ 78 | error: { 79 | type: "NOT_FOUND", 80 | reason: "This email does not exist", 81 | }, 82 | }); 83 | 84 | file = file.map( el => { 85 | if (el.includes(data.email)) { 86 | return [data.email, data.limit] 87 | } else return el 88 | }) 89 | 90 | fs.writeFileSync(this.usersJsonPath, JSON.stringify(file)); 91 | 92 | return response({ 93 | status: 1, 94 | }); 95 | } 96 | 97 | /** 98 | * @typedef {Object} ApiAddDataType 99 | * @property {string} email 100 | * 101 | * @param {ApiAddDataType} data 102 | */ 103 | delete(data) { 104 | let file = new File().GetJsonFile(this.usersJsonPath); 105 | 106 | let check = false; 107 | for (const el of file) { 108 | if (el.includes(data.email)) 109 | {check = true}; 110 | } 111 | 112 | if (check === false) 113 | return response({ 114 | error: { 115 | type: "NOT_FOUND", 116 | reason: "This email does not exist", 117 | }, 118 | }); 119 | 120 | file = file.filter( el => { 121 | if (el.includes(data.email)) 122 | { 123 | return false; 124 | } else {return true} 125 | }) 126 | 127 | fs.writeFileSync(this.usersJsonPath, JSON.stringify(file)); 128 | 129 | return response({ 130 | status: 1, 131 | }); 132 | } 133 | 134 | async getUser(email) { 135 | const db = new DBSqlite3(); 136 | 137 | let user = null; 138 | 139 | try { 140 | user = await db.read(email); 141 | } catch (e) { 142 | 143 | return response({ 144 | error: { 145 | type: "NOT_FOUND", 146 | reason: "This email does not exist", 147 | }, 148 | }); 149 | } 150 | 151 | return response({ 152 | data: { 153 | email, 154 | ips: user.ips, 155 | connections: user.ips.length, 156 | }, 157 | status: 1, 158 | }); 159 | } 160 | 161 | clear() { 162 | fs.writeFileSync(this.usersJsonPath, ""); 163 | 164 | return response({ 165 | status: 1, 166 | }); 167 | } 168 | } 169 | 170 | module.exports = Model; 171 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { spawn } = require("child_process"); 3 | const axios = require("axios"); 4 | const { join } = require("path"); 5 | 6 | function banIP(ip, email) { 7 | const scriptPath = "./ipban.sh"; 8 | const args = [ 9 | scriptPath, 10 | ip, 11 | `${process.env.BAN_TIME}`, 12 | `${process.env.SSH_PORT}`, 13 | ]; 14 | 15 | const childProcess = spawn("bash", args); 16 | 17 | childProcess.on("close", (code) => { 18 | if (code === 0) { 19 | if (process.env.TG_ENABLE === "true") 20 | globalThis.bot.api.sendMessage( 21 | process.env.TG_ADMIN, 22 | `${email}: IP ${ip} banned successfully. 23 | Duration: ${process.env.BAN_TIME} minutes 24 | `, 25 | ); 26 | 27 | console.log(`IP ${ip} banned successfully.`); 28 | } else { 29 | console.error(`Failed to ban IP ${ip}.`); 30 | } 31 | }); 32 | } 33 | 34 | class User { 35 | /** 36 | * @param {string} data Raw websocket data 37 | * @returns {Promise} 38 | */ 39 | GetNewUserIP = async (data) => { 40 | let lines = data 41 | .split(/\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}/g) 42 | .map((item) => item.trim()) 43 | .filter((item) => item); 44 | 45 | lines = lines.filter((item) => item.includes("accepted")); 46 | 47 | if (lines.length === 0) return []; 48 | 49 | const getIp = async (params) => { 50 | const chunks = params.split(":"); 51 | 52 | if (/[a-zA-Z]/g.test(params)) chunks.shift(); 53 | 54 | // try { 55 | // const { data } = await axios.get(`http://ip-api.com/json/${chunks[0]}`); 56 | 57 | // if ( 58 | // data.countryCode !== 59 | // (process.env?.COUNTRY_CODE?.toUpperCase() || "IR") 60 | // ) 61 | // return {}; 62 | // } catch (e) { 63 | // console.error(e); 64 | // return {}; 65 | // } 66 | 67 | return { ip: chunks[0], port: chunks[1] }; 68 | }; 69 | 70 | let newLines = []; 71 | 72 | for (let i in lines) { 73 | const item = lines[i]; 74 | const res = await getIp(item.split(" ")[0]); 75 | 76 | if (Object.keys(res).length === 0) continue; 77 | 78 | //console.log(`Line: ${item}`) 79 | 80 | //const email = item.split(" ").slice(-1)[0].replace(/\d\./g, "").substring(item.indexOf('.') + 1); 81 | //const email = item.split(" ").slice(-1)[0].replace(/\d\./g, "").substring(item.indexOf('.') + 2); 82 | //const emailMatch = item.match(/\S+\.([^.]+)/); 83 | //const email = emailMatch ? emailMatch[1] : ""; 84 | const emailMatch = item.match(/email: (\S+)/); 85 | const email = emailMatch ? emailMatch[1] : ""; 86 | const cleanedEmail = email.replace(/^\d+\./, ""); 87 | 88 | //console.log(`Email: ${cleanedEmail}`) 89 | 90 | newLines.push({ 91 | ...res, 92 | email: cleanedEmail, 93 | }); 94 | } 95 | 96 | // console.log("lines", lines); 97 | 98 | // console.log("newLines Before", newLines); 99 | 100 | return newLines.reduce((prev, curr) => { 101 | const index = prev.findIndex((item) => item.ip === curr.ip); 102 | if (index !== -1) prev[index] = curr; 103 | else prev.push(curr); 104 | 105 | return prev; 106 | }, []); 107 | }; 108 | 109 | /** 110 | * 111 | * @param {string} data Raw websocket data 112 | * @returns {string} 113 | */ 114 | GetEmail = (data) => { 115 | let returnData = ""; 116 | 117 | const chunks = data.split(" "); 118 | 119 | if (!chunks.includes("email:")) return returnData; 120 | 121 | const index = chunks.findIndex((item) => item === "email:"); 122 | 123 | const email = chunks[index + 1].split(".")[1].split("\n")[0]; 124 | 125 | return email; 126 | }; 127 | 128 | /** 129 | * 130 | * @param {string} data Raw websocket data 131 | * @returns {number} 132 | */ 133 | GetConnectionId(data) { 134 | const chunks = data.split(" "); 135 | 136 | const index = chunks.findIndex((item) => item === "[Info]"); 137 | 138 | if (index === -1) return 0; 139 | 140 | const d = chunks[index + 1]; 141 | 142 | return JSON.parse(d)[0]; 143 | } 144 | 145 | /** 146 | * 147 | * @param {string} data Raw websocket data 148 | * @returns {number} 149 | */ 150 | Closed = (data) => { 151 | const chunks = data.split(" "); 152 | 153 | const ends = 154 | chunks.includes("connection") && 155 | chunks.includes("ends") && 156 | chunks.includes("websocket:"); 157 | 158 | if (!ends) return 0; 159 | 160 | return this.GetConnectionId(data); 161 | }; 162 | } 163 | 164 | class Server { 165 | /** 166 | * @param {string} address address with port. Like: example.com:443 167 | * @param {boolean} api Return address with api 168 | * @returns {string} 169 | */ 170 | CleanAddress(address, api = true, showHttp = true, socket = false) { 171 | const [ADDRESS, port] = address.split(":"); 172 | 173 | let _address = address; 174 | 175 | if (+port === 443) _address = ADDRESS; 176 | 177 | if (showHttp) 178 | if (process.env.SSL == "true") _address = `https://${_address}`; 179 | else _address = `http://${_address}`; 180 | 181 | if (api) _address += "/api"; 182 | if (socket) _address += process.env.LISTEN_PATH; 183 | 184 | return _address; 185 | } 186 | } 187 | 188 | class File { 189 | constructor() {} 190 | 191 | ForceExistsFile(path, data) { 192 | if (!fs.existsSync(path)) fs.writeFileSync(path, data || ""); 193 | 194 | return; 195 | } 196 | 197 | GetJsonFile(path,replace) { 198 | this.ForceExistsFile(path,replace); 199 | 200 | return JSON.parse(fs.readFileSync(path)); 201 | } 202 | 203 | GetCsvFile(path) { 204 | this.ForceExistsFile(path, ""); 205 | 206 | return fs.readFileSync(path); 207 | } 208 | } 209 | 210 | module.exports = { User, Server, File, banIP }; 211 | -------------------------------------------------------------------------------- /db/DBSqlite3.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { DBInterface } = require("../interface"); 3 | const { File } = require("../utils"); 4 | const sqlite3 = require("sqlite3").verbose(); 5 | 6 | function connect() { 7 | const dbPath = join(__dirname, "../", "db.sqlite"); 8 | 9 | new File().ForceExistsFile(dbPath, ""); 10 | 11 | return new sqlite3.Database(dbPath); 12 | } 13 | 14 | const db = connect(); 15 | 16 | db.serialize(() => { 17 | db.run("CREATE TABLE IF NOT EXISTS users (email TEXT UNIQUE, ips TEXT)"); 18 | }); 19 | 20 | class DBSqlite3 extends DBInterface { 21 | async read(email) { 22 | return await new Promise((resolve, reject) => { 23 | db.serialize(() => { 24 | db.get("SELECT * FROM users WHERE email = ?", [email], (err, row) => { 25 | if (err) { 26 | reject(err); 27 | } else { 28 | if (row) { 29 | row.ips = JSON.parse(row.ips); 30 | // console.log("Read db:", row); 31 | resolve(row); 32 | } else resolve(null); 33 | } 34 | }); 35 | }); 36 | }); 37 | } 38 | 39 | async readAll() { 40 | return await new Promise((resolve, reject) => { 41 | db.serialize(() => { 42 | db.get("SELECT * FROM users", (err, row) => { 43 | if (err) { 44 | reject(err); 45 | } else { 46 | // console.log(row); 47 | } 48 | }); 49 | }); 50 | }); 51 | } 52 | 53 | addUser(data) { 54 | db.serialize(() => { 55 | db.run( 56 | "INSERT INTO users (email, ips) VALUES (?, ?)", 57 | [data.email, JSON.stringify(data.ips)], 58 | (err, res) => { 59 | if (err) { 60 | throw new Error(err); 61 | } 62 | }, 63 | ); 64 | }); 65 | } 66 | 67 | getUserIps(email) { 68 | return new Promise((resolve, reject) => { 69 | db.serialize(() => { 70 | db.get("SELECT ips FROM users WHERE email = ?", [email], (err, row) => { 71 | if (err) { 72 | console.error(`Ошибка при получении IP-адресов для пользователя с email ${email}:`, err); 73 | reject(err); 74 | } else { 75 | if (!row) { 76 | console.log(`Пользователь с email ${email} не найден.`); 77 | resolve([]); 78 | } else { 79 | const ips = JSON.parse(row.ips); 80 | if (ips.length === 0) { 81 | console.log(`Пользователь с email ${email} не имеет ассоциированных IP-адресов.`); 82 | } else { 83 | console.log(`IP-адреса для пользователя с email ${email}:`); 84 | ips.forEach((ip) => { 85 | console.log(`- ${ip.ip} (${ip.date})`); 86 | }); 87 | } 88 | resolve(ips); 89 | } 90 | } 91 | }); 92 | }); 93 | }); 94 | } 95 | 96 | 97 | addIp(email, ipData) { 98 | // Do not continue if the email is empty 99 | if (!email?.trim()) return; 100 | if (!ipData?.ip) return; 101 | 102 | db.serialize(() => { 103 | // Find user based on email 104 | db.get("SELECT * FROM users WHERE email = ?", [email], (err, row) => { 105 | if (err) { 106 | throw new Error(err); 107 | } else { 108 | // If the email does not exist, create it and assign the input IP to its first IP 109 | if (!row) { 110 | this.addUser({ 111 | email, 112 | ips: [{ ...ipData, first: true }], 113 | }); 114 | return; 115 | } 116 | 117 | const ips = JSON.parse(row.ips); 118 | const indexOfIp = ips.findIndex((item) => item.ip === ipData.ip); 119 | 120 | // Get the users.json file 121 | const usersJson = new File().GetJsonFile( 122 | join(__dirname, "../", "users.json"), 123 | ); 124 | const indexOfUser = usersJson.findIndex((item) => item[0] === email); 125 | 126 | const userJson = usersJson[indexOfUser] || [ 127 | email, 128 | process.env.MAX_ALLOW_USERS, 129 | ]; 130 | 131 | // If the IP is not already available and if there is enough space in database, add it 132 | if (indexOfIp === -1 && ips.length < +userJson[1]) { 133 | ips.push(ipData); 134 | 135 | db.run( 136 | 'UPDATE users SET ips = JSON_REPLACE(ips, "$", ?) WHERE email = ?', 137 | [JSON.stringify(ips), email], 138 | (updateErr, updateRow) => { 139 | if (updateErr) { 140 | throw new Error(updateErr); 141 | } else { 142 | console.log(" "+ email +" | Ip | "+ ipData.ip +" | Successfully Added"); 143 | this.getUserIps(email) 144 | } 145 | }, 146 | ); 147 | 148 | return; 149 | } else if (indexOfIp !== -1) { 150 | ips[indexOfIp].date = new Date().toISOString().toString(); 151 | 152 | db.run( 153 | 'UPDATE users SET ips = JSON_REPLACE(ips, "$", ?) WHERE email = ?', 154 | [JSON.stringify(ips), email], 155 | (updateErr, updateRow) => { 156 | if (updateErr) { 157 | throw new Error(updateErr); 158 | } else { 159 | console.log(" "+ email +" | Ip | "+ ipData.ip +" | Successfully Added"); 160 | this.getUserIps(email) 161 | } 162 | }, 163 | ); 164 | } 165 | } 166 | }); 167 | }); 168 | } 169 | 170 | deleteIp(email, ip) { 171 | db.serialize(() => { 172 | db.get("SELECT * FROM users WHERE email = ?", [email], (err, row) => { 173 | if (err) throw new Error(err); 174 | else { 175 | if (!row) return; 176 | 177 | const ips = JSON.parse(row.ips); 178 | const newIps = ips.filter((item) => item.ip !== ip); 179 | 180 | db.run( 181 | 'UPDATE users SET ips = JSON_REPLACE(ips, "$", ?) WHERE email = ?', 182 | [JSON.stringify(newIps), email], 183 | (updateErr, updateRow) => { 184 | if (updateErr) { 185 | throw new Error(updateErr); 186 | } else { 187 | // console.log("Ip Successfully Added"); 188 | } 189 | }, 190 | ); 191 | console.log(`Пользователь: ${email} | Удален IP: ${ip}`) 192 | const newIpsMessage = newIps.map((item) => `${item.ip}`).join('\n'); 193 | globalThis.bot.api.sendMessage( 194 | process.env.TG_ADMIN, 195 | "Пользователь " + data.email + ": IP " + ip + " удален.\n\nIP в базе:\n"+ newIpsMessage +"", 196 | { parse_mode: "HTML" } 197 | ); 198 | } 199 | }); 200 | }); 201 | } 202 | 203 | deleteLastIp(email) { 204 | db.serialize(() => { 205 | db.get("SELECT * FROM users WHERE email = ?", [email], (err, row) => { 206 | if (err) throw new Error(err); 207 | else { 208 | if (!row) return; 209 | 210 | const ips = JSON.parse(row.ips); 211 | 212 | ips.pop(); 213 | 214 | db.run( 215 | 'UPDATE users SET ips = JSON_REPLACE(ips, "$", ?) WHERE email = ?', 216 | [JSON.stringify(ips), email], 217 | (updateErr, updateRow) => { 218 | if (updateErr) { 219 | throw new Error(updateErr); 220 | } else { 221 | console.log("Ip Successfully Remove [last]"); 222 | } 223 | }, 224 | ); 225 | } 226 | }); 227 | }); 228 | } 229 | 230 | deleteUser(email) { 231 | db.run("DELETE FROM users WHERE email = ?", email, function (err) { 232 | if (err) { 233 | console.log(err); 234 | } else { 235 | console.log("Successfully deleted email from db.sqlite"); 236 | } 237 | }); 238 | } 239 | 240 | deleteInactiveUsers() { 241 | // const currentTime = new Date().getTime(); 242 | // const fewMinutesLater = new Date( 243 | // currentTime - +process.env.CHECK_INACTIVE_USERS_DURATION * 60 * 1000, 244 | // ); 245 | const currentTime = Date.now(); // текущее время в миллисекундах с 1 января 1970 года (Unix time) 246 | const durationInMinutes = +process.env.CHECK_INACTIVE_USERS_DURATION; 247 | const fewMinutesLater = new Date(currentTime - durationInMinutes * 60 * 1000); 248 | const currentTimeISO = new Date(currentTime).toISOString(); 249 | console.log(`NOW: ${currentTimeISO} | CHECK: ${fewMinutesLater.toISOString()}`); 250 | 251 | db.serialize(function () { 252 | db.all( 253 | `SELECT * FROM users WHERE json_extract(ips, '$[0].date') <= ?`, 254 | fewMinutesLater.toISOString().toString(), 255 | function (err, rows) { 256 | if (err) { 257 | console.error(err); 258 | return; 259 | } 260 | 261 | rows.forEach(function (row) { 262 | const email = row.email; 263 | const ips = JSON.parse(row.ips); 264 | 265 | const updatedIds = ips.filter(function (id) { 266 | const idDate = new Date(id.date); 267 | return idDate > fewMinutesLater; 268 | }); 269 | 270 | console.log(" "+email+" | updateIds", updatedIds); 271 | 272 | db.run( 273 | `UPDATE users SET ips = ? WHERE email = ?`, 274 | JSON.stringify(updatedIds), 275 | email, 276 | function (err) { 277 | if (err) { 278 | console.error(err); 279 | return; 280 | } 281 | console.log(`Record with email ${email} updated.`); 282 | }, 283 | ); 284 | }); 285 | }, 286 | ); 287 | }); 288 | } 289 | } 290 | 291 | module.exports = DBSqlite3; 292 | -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 | # luIP-marzban (EN & Fixed) 2 | 3 | User connection limits for a proxy based on Xray. Panel [Marzban](https://github.com/Gozargah/Marzban) 4 | 5 | Community where you can ask for help - [Telegram chat](https://t.me/gozargah_marzban) 6 | 7 | To use luIP-marzban on nodes, you will need this [repository](https://github.com/sm1ky/luIP-marzban-node) 8 | 9 | ## Attention 10 | Before configuring the script, remember that you need to set up at least ssh (ufw allow ssh) access before enabling ufw (ufw enable), otherwise, you'll have to go through VNC :) 11 | 12 | And don't forget to allow the port specified in .env (By default, 3000) 13 | 14 | Instructions: 15 | 16 | Main: [UFW Main Panel](https://docs.marzban.ru/advanced/ufw_main_panel/) 17 | 18 | Node: [UFW Node](https://docs.marzban.ru/advanced/ufw_node/) 19 | 20 | ## Instructions 21 | 22 | - [Mechanism](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#mechanism) 23 | - [Features](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#features) 24 | - [Requirements](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#installation) 25 | - [Install Node.js](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#install-nodejs) 26 | - [Install ufw / dsniff / gawk / csvtool](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#installation-other-dependencies) 27 | - [Install luIP-marzban](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#installation-luip-marzban) 28 | - [Environment](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#luip-marzbanenv-file) 29 | - [users.json](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#usersjson) 30 | - [Permission](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#permission-to-use-ipbansh--ipunbansh--restore_banned_ipssh--unbanallsh) 31 | - [Run the project](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#run-the-project) 32 | - [Stop the project](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#stop-luip-with-kill-process) 33 | - [Check banned IP](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#checking-blocked-ips) 34 | - [Unblock a specific IP](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#unblock-ip) 35 | - [Unblock all IPs](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#unblock-all-ips) 36 | - [Uninstall project](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#uninstall) 37 | - [API Reference](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#api-reference) 38 | - [FAQ](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#faq) 39 | - [Donate](https://github.com/sm1ky/luIP-marzban/blob/main/README-EN.md#donate) 40 | 41 | ## Mechanism 42 | 43 | The luIP-marzban project was created and developed based on node.js and uses the marzban API. 44 | 45 | luIP stores connected and authorized users in the sqlite database. Saving and updating users occur through a WebSocket, where traffic is intercepted by luIP-marzban, and data, including IP addresses, is obtained from there. 46 | 47 | Users are updated via WebSocket and on a schedule based on the `FETCH_INTERVAL_LOGS_WS` variable located in `.env`. 48 | 49 | Every x minutes, a check is performed based on the `CHECK_INACTIVE_USERS_DURATION` variable: if the last update of the connected IP was y minutes ago, based on the `CHECK_INACTIVE_USERS_DURATION` variable, the user's IP address will be removed from the connected list. And this feature is provided to leave free space and allow other clients to connect. 50 | 51 | IPs are blocked through [ufw](https://help.ubuntu.com/community/UFW), then incoming traffic to this IP is blocked for the time specified in the `BAN_TIME` variable. 52 | 53 | Blocked IP addresses are automatically saved in the `blocked_ips.csv` file, then every x minutes, based on the value of the `CHECK_IPS_FOR_UNBAN_USERS` variable, the `ipunban.sh` file is executed and checks: if the saved IP addresses were in jail for y minutes or more, they will be released from the ban. 54 | 55 |

56 | 57 |

58 | 59 | ## Features 60 | 61 | - Automatic logs 62 | - Interaction with the Telegram bot 63 | - API 64 | - Identification of specific users 65 | - Import/Export backup of users 66 | - IP blocking 67 | - Support for [Marzban Node](https://github.com/Gozargah/Marzban-node) 68 | 69 | ## Installation 70 | 71 | If you do not have Node.js installed on your server, install it using nvm. 72 | 73 | #### Install Node.js 74 | ```bash 75 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash 76 | source ~/.bashrc 77 | nvm install --lts 78 | ``` 79 | 80 | #### Installation other dependencies 81 | 82 | ```bash 83 | sudo apt-get update 84 | sudo apt-get install -y ufw 85 | sudo apt-get install -y dsniff 86 | sudo apt-get install -y gawk 87 | sudo apt-get install -y csvtool 88 | npm install pm2 -g 89 | ``` 90 | 91 | 92 | #### Installation luIP-marzban 93 | ```bash 94 | git clone https://github.com/sm1ky/luIP-marzban.git 95 | cd luIP-marzban 96 | cp .env.example .env 97 | npm install 98 | ``` 99 | 100 | ## luIP-marzban/.env file 101 | ```bash 102 | # Open the project folder, then execute the following command 103 | nano .env 104 | ``` 105 | 106 | 107 | #### Address configuration 108 | | Parameter | Description | 109 | | :-------- | :------------------------- | 110 | | `ADDRESS` | Your domain or sub domain. e.g: example.com or sub.example.com | 111 | | `PORT_ADDRESS` | Your domain port. e.g: 443 | 112 | | `SSL` | Did you get domain SSL? e.g: true or false | 113 | 114 | 115 | #### Marzban configuration 116 | 117 | | Parameter | Description | 118 | | :-------- | :------------------------- | 119 | | `P_USER` | Enter the username of Marzban panel e.g: admin | 120 | | `P_PASS` | Enter the password of Marzban panel e.g: admin | 121 | 122 | #### App configuration 123 | 124 | | Parameter | Description | 125 | | :-------- | :------------------------- | 126 | | `MAX_ALLOW_USERS` | The maximum number of users that can connect to a proxy. e.g: 1 | 127 | | `BAN_TIME` | The length of time an IP is in jail based on minutes. e.g: 5 | 128 | 129 | #### Advance configuration 130 | 131 | | Parameter | Description | 132 | | :-------- | :------------------------- | 133 | | `FETCH_INTERVAL_LOGS_WS` | Based on this, websocket logs are checked every x seconds to track traffic. e.g: 1 | 134 | | `CHECK_INACTIVE_USERS_DURATION` | It is checked every x minutes, users whose last update was x minutes ago or more are disabled. e.g: 5 | 135 | | `CHECK_IPS_FOR_UNBAN_USERS` | Every x minutes it checks all ips, if they are in prison for more than the time specified in `BAN_TIME`, they will be unbanned. e.g: 1 | 136 | | `SSH_PORT` | Enter your ssh port in this section. 22 is set by default | 137 | | `TESTSCRIPTS` | If you want to test the blocking system without actually blocking the user, set it to true. By default, it is set to false. | 138 | 139 | #### Telegram bot configuration 140 | 141 | | Parameter | Description | 142 | | :-------- | :------------------------- | 143 | | `TG_ENABLE` | If you want to use Telegram bot for logs, set this value to `true` | 144 | | `TG_TOKEN` | The bot token you received from @botfather | 145 | | `TG_ADMIN` | Your user ID that you received from @userinfobot | 146 | 147 | ## users.json 148 | You can set specific users in the users.json file 149 | 150 | - Priority is always with this file 151 | 152 | In the example below, email1 is the proxy name and 2 represents the maximum number of users that can be connected. 153 | 154 | #### luIP-marzban/users.json 155 | ```json 156 | [ 157 | ["admin", 2], 158 | ["user", 10] 159 | ] 160 | ``` 161 | 162 | ## Permission to use ipban.sh && ipunban.sh && restore_banned_ips.sh && unbanall.sh 163 | In order for the file to work, permission must be obtained to use it 164 | ```bash 165 | # Open the project folder, then execute the follow command 166 | chmod +x ./ipban.sh 167 | chmod +x ./ipunban.sh 168 | chmod +x ./restore_banned_ips.sh 169 | chmod +x ./unbanall.sh 170 | ``` 171 | 172 | 173 | ## Run the project 174 | After configuring the project, run it 175 | ```bash 176 | # Open the project folder, then execute the follow command 177 | npm start 178 | 179 | ``` 180 | 181 | ## Stop luIP with kill process 182 | You can run the command below, but whenever you want, you can go to the project path [ `cd /luIP-marzban` ] and type `npm start`, luIP will run again. 183 | ```bash 184 | pm2 kill 185 | pm2 flush # Delete logs 186 | ``` 187 | 188 | ## Checking blocked IPs 189 | ```bash 190 | sudo ufw status numbered | awk '/DENY/ {gsub(/[\[\]]/, ""); for(i=1; i<=NF; i++) { if ($i == "DENY") printf "%s | %s | %s %s | %s\n", $1, $(i-1), $i, $(i+1), $(i+2) }}' 191 | ``` 192 | 193 | ## Unblock IP 194 | NUM - obtained from the command above, it is the first one. 195 | ```bash 196 | sudo ufw delete NUM 197 | ``` 198 | 199 | ## Unblock all IPs 200 | ```bash 201 | bash ./unbanall.sh 202 | ``` 203 | 204 | ## Uninstall 205 | ```bash 206 | pm2 kill 207 | sudo rm -rf /luIP-marzban 208 | ``` 209 | 210 | 211 | ## API Reference 212 | 213 | We get to know the following environment variables that are located in the .env file by default. 214 | 215 | ##### When you use the api, the data will be stored in a file called `users.csv`, and this file has a higher priority in reading than `MAX_ALLOW_USERS` and `users.json`, just as `users.json` has a higher priority than `MAX_ALLOW_USERS`. 216 | 217 | 218 | | Parameter | Description | 219 | | :-------- | :------------------------- | 220 | | `API_ENABLE` | If you want to use api, set the value of this variable equal to `true` | 221 | | `API_SECRET` | Short secret for access_token. The encryption type of access_tokens is AES, and only the expiration date of the token is included in the access_token. secret is a password to encrypt and decrypt access_token with AES encryption type. | 222 | | `API_PATH` | Displays api path by default /api | 223 | | `API_LOGIN` | Enter a desired username and password in the username:password format so that you can be identified to receive the token | 224 | | `API_EXPIRE_TOKEN_AT` | Each access_token you receive has an expiration date. You can set it here | 225 | | `API_PORT` | Choose a port for your api address. Also make sure it is not occupied. By default 4000 | 226 | 227 | Your default api address: https://example.com:4000/api 228 | 229 | #### Get access_token 230 | 231 | ```http 232 | POST /api/token 233 | ``` 234 | 235 | | Parameter | Type | Description | 236 | | :-------- | :------- | :------------------------- | 237 | | `username` | `string` | **Required**. Your `API_LOGIN` username | 238 | | `password` | `string` | **Required**. Your `API_LOGIN` password | 239 | 240 | 241 | #### Note: In all the following apis, send the value of api_key: YOUR_ACCESS_TOKEN as header. (Fill YOUR_ACCESS_TOKEN with the value you received from /api/token) 242 | 243 | #### Add user 244 | 245 | ```http 246 | POST /api/add 247 | ``` 248 | 249 | | Parameter | Type | Description | 250 | | :-------- | :------- | :-------------------------------- | 251 | | `email` | `string` | **Required**. The name of your target config. For example test | 252 | | `limit` | `number` | **Required**. What is the maximum limit? | 253 | 254 | #### Update user 255 | 256 | ```http 257 | POST /api/update 258 | ``` 259 | 260 | | Parameter | Type | Description | 261 | | :-------- | :------- | :-------------------------------- | 262 | | `email` | `string` | **Required**. The name of your target config. For example test | 263 | | `limit` | `number` | **Required**. What is the maximum limit? | 264 | 265 | #### Delete user 266 | 267 | ```http 268 | GET /api/delete/ 269 | ``` 270 | 271 | | Parameter | Type | Description | 272 | | :-------- | :------- | :-------------------------------- | 273 | | `email` | `string` | **Required**. The name of your target config. For example test | 274 | 275 | #### Clear luIP database (users.json) 276 | 277 | ```http 278 | GET /api/clear 279 | ``` 280 | 281 | 282 | ## FAQ 283 | 284 | #### If there are changes in marzban-node, should I restart luIP? 285 | 286 | Yes, to apply the changes, it is necessary to restart luIP through the following command 287 | 288 | ```bash 289 | # first Open the project dir with follow command 290 | cd /luIP-marzban 291 | 292 | # then run follow command 293 | pm2 kill 294 | npm start 295 | ``` 296 | 297 | ## Donate 298 | If you like it and it works for you, you can donate to support, develop and improve luIP-marzban. We wish the best for you 299 | 300 | 1. Tron: `TSrhAJEYqYHzuGYjsUqC46mmCx7Jp27dvX` 301 | 2. Tinkoff (RU BANK): `2200700951484392` 302 | 3. Write to me on [Telegram](https://t.me/sm1ky), and I will provide you with the necessary details. 303 | 304 | ## Author of the Original Script and Repository 305 | Author: [mmdzdov](https://github.com/mmdzov) 306 | Repository: [luIP-marzban](https://github.com/mmdzov/luIP-marzban) 307 | 308 | -------------------------------------------------------------------------------- /README-RU.md: -------------------------------------------------------------------------------- 1 | # luIP-marzban (RU & Fixed) 2 | Лимитные подключения пользователей для прокси на базе Xray. Панель [Marzban](https://github.com/Gozargah/Marzban) 3 | 4 | Сообщество где можно попросить помощи - [Telegram чат](https://t.me/gozargah_marzban) 5 | 6 | Для использования luIP-marzban на нодах вам потребуется данный [репозиторий](https://github.com/sm1ky/luIP-marzban-node) 7 | 8 | ## Внимание 9 | Прежде чем настраивать скрипт, помните что нужно настроить хотя-бы ssh (ufw allow ssh) доступ перед включением ufw (ufw enable) иначе придется лезть через VNC :) 10 | 11 | И не забудье разрешить порт указанный в .env (По стандарту 3000) 12 | 13 | Инструкции: 14 | 15 | Main: https://docs.marzban.ru/advanced/ufw_main_panel/ 16 | 17 | Node: https://docs.marzban.ru/advanced/ufw_node/ 18 | 19 | ## Инструкции 20 | 21 | - [Механизм](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#Механизм) 22 | - [Функции](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#Функции) 23 | - [Требования](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#Установка) 24 | - [Установка node.js](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#Установка-Node.js) 25 | - [Установка ufw / dsniff / gawk / csvtool](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#Установка-Node.js) 26 | - [Установка luIP-marzban](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#Установка-Node.js) 27 | - [Окружение](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#файл-env-для-luip-marzban) 28 | - [users.json](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#usersjson) 29 | - [Разрешение](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#разрешение-на-использование-ipbansh--ipunbansh--restore_banned_ipssh--unbanallsh) 30 | - [Запуск проекта](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#запуск-проекта) 31 | - [Остановка проекта](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#остановка-luip) 32 | - [Проверка заблокированных IP](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#проверка-заблокированных-ip-адресов) 33 | - [Разблокировка определенного IP](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#удалить-заблокированный-ip-адрес) 34 | - [Разблокировка всех IP](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#разблокировать-все-ip-адреса) 35 | - [Удаление проекта](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#Удаление) 36 | - [API Reference](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#api-reference) 37 | - [FAQ](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#часто-задаваемые-вопросы) 38 | - [Пожертвовать](https://github.com/sm1ky/luIP-marzban/blob/main/README-RU.md#часто-задаваемые-вопросы) 39 | 40 | 41 | ## Механизм 42 | 43 | Проект luIP-marzban был создан и разработан на основе node.js и использует API marzban. 44 | 45 | luIP хранит подключенных и авторизованных пользователей в базе данных sqlite. Сохранение и обновление пользователей происходит через веб-сокет, где трафик перехватывается luIP-marzban, и данные, включая IP-адреса, получаются оттуда. 46 | 47 | Пользователи обновляются через веб-сокет и по расписанию на основе переменной `FETCH_INTERVAL_LOGS_WS`, расположенной в `.env`. 48 | 49 | Каждые x минут выполняется проверка на основе переменной `CHECK_INACTIVE_USERS_DURATION`: если последнее обновление подключенного IP было y минут назад, основываясь на переменной `CHECK_INACTIVE_USERS_DURATION`, IP-адрес пользователя будет удален из списка подключенных. И эта возможность предоставляется для того, чтобы оставалось свободное место, и другим клиентам разрешено подключаться. 50 | 51 | IP блокируются через [ufw](https://help.ubuntu.ru/wiki/руководство_по_ubuntu_server/безопасность/firewall#ufw_-_простой_firewall), затем входящий трафик на этот IP блокируется на время, указанное в переменной `BAN_TIME`. 52 | 53 | Заблокированные IP-адреса автоматически сохраняются в файле `blocked_ips.csv`, затем каждые x минут, основываясь на значении переменной `CHECK_IPS_FOR_UNBAN_USERS`, выполняется файл `ipunban.sh` и проверяет: если сохраненные IP-адреса были в тюрьме y минут или более, они будут выпущены из бана 54 | 55 |

56 | 57 |

58 | 59 | ## Функции 60 | 61 | - Автоматические журналы (логи) 62 | - Работа с Telegram ботом 63 | - API 64 | - Определение конкретных пользователей 65 | - Импорт/Экспорт резевной копии пользователей 66 | - Блокировка по IP 67 | - Поддержка [Marzban Node](https://github.com/Gozargah/Marzban-node) 68 | 69 | ## Установка 70 | 71 | Если у вас нет установленного node.js на вашем сервере, установите его с помощью nvm 72 | 73 | #### Установка Node.js 74 | ```bash 75 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash 76 | source ~/.bashrc 77 | nvm install --lts 78 | ``` 79 | 80 | 81 | #### Установка других зависимостей 82 | 83 | ```bash 84 | sudo apt-get update 85 | sudo apt-get install -y ufw 86 | sudo apt-get install -y dsniff 87 | sudo apt-get install -y gawk 88 | sudo apt-get install -y csvtool 89 | npm install pm2 -g 90 | ``` 91 | 92 | 93 | #### Установка luIP-marzban 94 | ```bash 95 | git clone https://github.com/sm1ky/luIP-marzban.git 96 | cd luIP-marzban 97 | cp .env.example .env 98 | npm install 99 | ``` 100 | 101 | ## Файл .env для luIP-marzban 102 | ```bash 103 | # Откройте папку проекта, затем выполните следующую команду 104 | nano .env или vim .nano 105 | ``` 106 | 107 | 108 | #### Конфигурация адреса Marzban 109 | | Parameter | Description | 110 | | :-------- | :------------------------- | 111 | | `ADDRESS` | Ваш домен или поддомен. Например: example.com или sub.example.com (Поддерживает IP) | 112 | | `PORT_ADDRESS` | Порт вашего домена. Например: 443 | 113 | | `SSL` | Есть ли SSL на домене? Например: true или false | 114 | 115 | 116 | #### Конфигурация Marzban 117 | 118 | | Parameter | Description | 119 | | :-------- | :------------------------- | 120 | | `P_USER` | Введите имя пользователя панели Marzban, например: admin | 121 | | `P_PASS` | Введите пароль панели Marzban, например: admin | 122 | 123 | #### Конфигурация luIP-marzban 124 | 125 | | Parameter | Description | 126 | | :-------- | :------------------------- | 127 | | `MAX_ALLOW_USERS` | Максимальное количество пользователей, которые могут подключиться к прокси, например: 1 | 128 | | `BAN_TIME` | Продолжительность времени, в течение которой IP находится в бане в минутах, например: 5 | 129 | 130 | #### Расширенная конфигурация 131 | 132 | | Parameter | Description | 133 | | :-------- | :------------------------- | 134 | | `FETCH_INTERVAL_LOGS_WS` | На его основе каждые x секунд проверяются журналы веб-сокета для отслеживания трафика, например: 1 | 135 | | `CHECK_INACTIVE_USERS_DURATION` | Каждые x минут проверяются пользователи, последнее обновление которых было x минут назад или ранее, например: 5 | 136 | | `CHECK_IPS_FOR_UNBAN_USERS` | Каждые x минут проверяются все IP-адреса, если они находятся в бане больше времени, указанного в BAN_TIME, они будут разблокированы, например: 1 | 137 | | `SSH_PORT` | Введите свой порт ssh в этом разделе. 22 установлен по умолчанию | 138 | | `TESTSCRIPTS` | Если вы хотите проверить систему блировки и при этом не блокировать юзера. Установите true. По стандарту false | 139 | 140 | #### Конфигурация Telegram бота 141 | 142 | | Parameter | Description | 143 | | :-------- | :------------------------- | 144 | | `TG_ENABLE` | Если вы хотите использовать бота Telegram для логов, установите этому значению `true` | 145 | | `TG_TOKEN` | Токен бота, который вы получили от @botfather | 146 | | `TG_ADMIN` | Ваш идентификатор пользователя, который вы получили от @userinfobot | 147 | 148 | ## users.json 149 | Вы можете установить конкретных пользователей в файле users.json 150 | 151 | - Приоритет всегда у этого файла 152 | 153 | В приведенном ниже примере admin - это имя прокси, и 2 представляет максимальное количество пользователей, которые могут быть подключены. 154 | 155 | #### luIP-marzban/users.json 156 | ```json 157 | [ 158 | ["admin", 2], 159 | ["user", 10] 160 | ] 161 | ``` 162 | 163 | ## Разрешение на использование ipban.sh && ipunban.sh && restore_banned_ips.sh && unbanall.sh 164 | Для того чтобы файлы работали, необходимо дать разрешение на их использование. 165 | ```bash 166 | # Откройте папку проекта, затем выполните следующую команду 167 | chmod +x ./ipban.sh 168 | chmod +x ./ipunban.sh 169 | chmod +x ./restore_banned_ips.sh 170 | chmod +x ./unbanall.sh 171 | ``` 172 | 173 | 174 | ## Запуск проекта 175 | После настройки проекта запустите его 176 | ```bash 177 | # Откройте папку проекта, затем выполните следующую команду 178 | npm start 179 | 180 | ``` 181 | 182 | ## Остановка luIP 183 | 184 | Вы можете выполнить команду ниже, но когда угодно вы можете перейти в путь проекта [ `cd /luIP-marzban` ] и ввести `npm start`, luIP снова запустится. 185 | 186 | ```bash 187 | pm2 kill 188 | pm2 flush # Удаляет логи 189 | ``` 190 | 191 | ## Проверка заблокированных IP-адресов 192 | 193 | ```bash 194 | sudo ufw status numbered | awk '/DENY/ {gsub(/[\[\]]/, ""); for(i=1; i<=NF; i++) { if ($i == "DENY") printf "%s | %s | %s %s | %s\n", $1, $(i-1), $i, $(i+1), $(i+2) }}' 195 | ``` 196 | 197 | ## Удалить заблокированный IP-адрес 198 | NUM - полученный из команды выше, содержится первым 199 | ```bash 200 | sudo ufw delete NUM 201 | ``` 202 | 203 | ## Разблокировать все IP-адреса 204 | ```bash 205 | bash ./unbanall.sh 206 | ``` 207 | 208 | ## Удаление 209 | ```bash 210 | pm2 kill 211 | sudo rm -rf /luIP-marzban 212 | ``` 213 | 214 | 215 | ## API Reference 216 | 217 | Мы узнаем следующие переменные среды, которые расположены в файле .env по умолчанию. 218 | 219 | ##### При использовании api данные будут сохранены в файле с именем users.csv, и этот файл имеет более высокий приоритет при чтении, чем MAX_ALLOW_USERS и users.json, так же как users.json имеет более высокий приоритет, чем MAX_ALLOW_USERS. 220 | 221 | 222 | | Параметр | Описание | 223 | | :-------- | :------------------------- | 224 | | `API_ENABLE` | Если вы хотите использовать API, установите значение этой переменной равным `true` | 225 | | `API_SECRET` | Краткий секрет для access_token. Тип шифрования access_token - AES, и в access_token включается только срок действия токена. secret - это пароль для шифрования и дешифрования access_token с использованием AES. | 226 | | `API_PATH` | Отображает путь api по умолчанию /api | 227 | | `API_LOGIN` | Введите желаемое имя пользователя и пароль в формате username:password, чтобы вы могли авторизоваться для получения токена | 228 | | `API_EXPIRE_TOKEN_AT` | У каждого полученного access_token есть срок действия. Вы можете установить его здесь | 229 | | `API_PORT` | Выберите порт для вашего api-адреса. Также убедитесь, что он не занят. По умолчанию 3000 | 230 | 231 | Ссылка по умолчанию: https://example.com:4000/api 232 | 233 | #### Получение access_token 234 | 235 | ```http 236 | POST /api/token 237 | ``` 238 | 239 | | Параметр | Тип | Описание | 240 | | :-------- | :------- | :------------------------- | 241 | | `username` | `string` | **Обязательно**. Ваше имя пользователя `API_LOGIN` | 242 | | `password` | `string` | **Обязательно**. Ваш пароль `API_LOGIN` | 243 | 244 | 245 | #### Примечание: Во всех следующих API отправляйте значение api_key: YOUR_ACCESS_TOKEN в заголовке. (Замените YOUR_ACCESS_TOKEN на значение, которое вы получили из /api/token) 246 | 247 | #### Добавить пользователя 248 | 249 | ```http 250 | POST /api/add 251 | ``` 252 | 253 | | Параметр | Тип | Описание | 254 | | :-------- | :------- | :-------------------------------- | 255 | | `email` | `string` | **Обязательно**. Имя пользователя. Например: admin | 256 | | `limit` | `number` | **Обязательно**. Сколько одновременно подключений может быть? | 257 | 258 | #### Обновить пользователя 259 | 260 | ```http 261 | POST /api/update 262 | ``` 263 | 264 | | Параметр | Тип | Описание | 265 | | :-------- | :------- | :-------------------------------- | 266 | | `email` | `string` | **Обязательно**. Имя пользователя. Например: admin | 267 | | `limit` | `number` | **Обязательно**. Сколько одновременно подключений может быть? | 268 | 269 | #### Удалить пользователя 270 | 271 | ```http 272 | GET /api/delete/ 273 | ``` 274 | 275 | | Параметр | Тип | Описание | 276 | | :-------- | :------- | :-------------------------------- | 277 | | `email` | `string` | **Обязательно**. Имя пользователя. Например: admin | 278 | 279 | #### Очистить luIP базу (users.json) 280 | 281 | ```http 282 | GET /api/clear 283 | ``` 284 | 285 | 286 | 287 | ## Часто задаваемые вопросы 288 | 289 | #### Если есть изменения в marzban-node, нужно ли перезапускать luIP? 290 | 291 | Да, чтобы применить изменения, необходимо перезапустить luIP с помощью следующей команды 292 | 293 | ```bash 294 | # Сначала откройте папку проекта с помощью следующей команды 295 | cd /luIP-marzban 296 | 297 | # Затем выполните следующуюкоманду 298 | pm2 kill 299 | npm start 300 | ``` 301 | 302 | ## Пожертвовать 303 | Если вам нравится и это работает для вас, вы можете сделать пожертвование на поддержку, разработку и улучшение luIP-marzban для русскоговорящих людей. Желаем вам всего наилучшего 304 | 305 | 1. Tron: `TSrhAJEYqYHzuGYjsUqC46mmCx7Jp27dvX` 306 | 2. Тинькофф: `2200700951484392` 307 | 3. Напишите мне в [Telegram](https://t.me/sm1ky) и я предоставлю нужные вам реквизиты 308 | 309 | ## Автор оригинального скрипта и его репозиторий 310 | Автор: [mmdzdov](https://github.com/mmdzov) 311 | Репозиторий: [luIP-marzban](https://github.com/mmdzov/luIP-marzban) 312 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require("ws"); 2 | const { User, Server, File, banIP } = require("./utils"); 3 | const { default: axios } = require("axios"); 4 | const { DBAdapter } = require("./db/Adapter"); 5 | const { join } = require("path"); 6 | const sqlite3 = require("sqlite3").verbose(); 7 | const DBSqlite3 = require("./db/DBSqlite3"); 8 | const crypto = require("crypto-js"); 9 | const socket = require("socket.io"); 10 | const nodeCron = require("node-cron"); 11 | const fs = require("fs"); 12 | 13 | class Ws { 14 | /** 15 | * @param {WebSocketConfigType} params 16 | */ 17 | constructor(params) { 18 | let patch = params?.node ? `node/${params.node}` : "core"; 19 | this.access_token = params.accessToken; 20 | this.params = params; 21 | 22 | const url = `${process.env.SSL === "true" ? "wss" : "ws"}://${ 23 | params.url 24 | }/api/${patch}/logs?interval=${process.env.FETCH_INTERVAL_LOGS_WS}&token=${ 25 | this.access_token 26 | }`; 27 | 28 | const db = new DBAdapter(params.DB); 29 | const ws = new WebSocket(url); 30 | 31 | nodeCron.schedule(`*/1 * * * *`, async () => { 32 | console.log("Schedule", ws.url, ws.isPaused); 33 | }); 34 | 35 | const user = new User(); 36 | const ipGuard = new IPGuard({ 37 | banDB: new DBSqlite3(), 38 | socket: params.socket, 39 | api: params.api, 40 | db: db, 41 | }); 42 | 43 | this.db = db; 44 | this.user = user; 45 | this.ws = ws; 46 | this.ipGuard = ipGuard; 47 | 48 | // retry to get token 49 | const retryGetToken = async (error, response) => { 50 | const token = await params.api.token(); 51 | 52 | const _ws = new Ws({ ...params, accessToken: token }); 53 | _ws.logs(); 54 | 55 | console.log("Websocket response", ws.url); 56 | }; 57 | 58 | ws.on("error", retryGetToken); 59 | ws.on("close", retryGetToken); 60 | } 61 | 62 | logs() { 63 | // Opened connections 64 | 65 | if (process.env?.NODE_ENV?.includes("development")) { 66 | this.ws.on("message", async (msg) => { 67 | const bufferToString = msg.toString(); 68 | 69 | const data = await this.user.GetNewUserIP(bufferToString); 70 | 71 | //console.log("Data: ", data); 72 | //console.log(this.access_token); 73 | }); 74 | 75 | return this; 76 | } 77 | 78 | this.ws.on("message", async (msg) => { 79 | const bufferToString = msg.toString(); 80 | 81 | const data = await this.user.GetNewUserIP(bufferToString); 82 | 83 | if (data.length === 0) return; 84 | 85 | console.log(`Обновление ${new Date().toLocaleString('ru-RU')}: `, data); 86 | 87 | let num = data.length; 88 | while (num--) { 89 | const item = data[num]; 90 | 91 | if (item.email.toLowerCase() === "api]") { 92 | console.log(`Notification: Received data for "api]" email. SKIP`); 93 | } else { 94 | //console.log(`Пользователь: ${item.email} | IP: ${item.ip} | Порт: ${item.port}`) 95 | await this.ipGuard.use( 96 | item.ip, 97 | () => this.db.read(item.email), 98 | () => 99 | this.db.addIp(item.email, { 100 | ip: item.ip, 101 | port: item.port, 102 | date: new Date().toISOString().toString(), 103 | }), 104 | () => this.db.deleteLastIp(item.email), 105 | ); 106 | } 107 | } 108 | }); 109 | return this; 110 | } 111 | } 112 | 113 | class Api { 114 | /** 115 | * @description Marzban access_token 116 | */ 117 | accessToken = ""; 118 | 119 | /** 120 | * @description Default: Bearer 121 | */ 122 | accessTokenType = "Bearer"; 123 | 124 | accessTokenExpireAt = null; 125 | 126 | /** 127 | * @description Creates an instance to communicate with the marzban api 128 | * @returns {void} 129 | */ 130 | create() { 131 | const url = new Server().CleanAddress( 132 | `${process.env.ADDRESS}:${process.env.PORT_ADDRESS}`, 133 | ); 134 | 135 | this.axios = axios.create({ 136 | baseURL: url, 137 | headers: { 138 | accept: "application/json", 139 | "Content-Type": "application/x-www-form-urlencoded", 140 | }, 141 | }); 142 | 143 | this.axios.interceptors.response.use( 144 | (value) => value, 145 | async (error) => { 146 | if ( 147 | error?.response?.data?.detail === "Could not validate credentials" 148 | ) { 149 | await this.token(); 150 | console.log("New Token:", this.accessToken); 151 | } 152 | 153 | return error; 154 | }, 155 | ); 156 | } 157 | 158 | /** 159 | * @description It receives access_token from Marzban api 160 | * @returns {Promise} 161 | */ 162 | async token() { 163 | // if (this.accessTokenExpireAt && Date.now() < +this.accessTokenExpireAt) 164 | // return; 165 | 166 | try { 167 | const { data } = await this.axios.post("/admin/token", { 168 | username: process.env.P_USER, 169 | password: process.env.P_PASS, 170 | }); 171 | 172 | this.accessToken = data.access_token; 173 | this.axios.defaults.headers.common.Authorization = `Bearer ${data.access_token}`; 174 | this.accessTokenType = data.token_type; 175 | this.accessTokenExpireAt = new Date() + 1000 * 60 * 60; 176 | 177 | return data.access_token; 178 | } catch (e) { 179 | console.error(e); 180 | } 181 | } 182 | 183 | async getNodes() { 184 | let nodes = []; 185 | 186 | try { 187 | const { data } = await this.axios.get("/nodes"); 188 | 189 | if (!data) return nodes; 190 | 191 | nodes = data 192 | .filter((item) => item.status === "connected") 193 | .map((item) => item.id); 194 | } catch (e) { 195 | console.error(e); 196 | } 197 | 198 | return nodes; 199 | } 200 | 201 | /** 202 | * @param {string} email 203 | * @param {"disabled" | "active"} status 204 | */ 205 | async changeUserProxyStatus(email, status) { 206 | try { 207 | const { data } = await this.axios.get(`/user/${email}`); 208 | 209 | const res = await this.axios.put( 210 | `/user/${email}`, 211 | { 212 | ...data, 213 | status, 214 | }, 215 | { 216 | headers: { 217 | "Content-Type": "application/json", 218 | }, 219 | }, 220 | ); 221 | } catch (e) { 222 | console.error(e); 223 | } 224 | } 225 | } 226 | 227 | class Socket { 228 | connected = false; 229 | 230 | /** 231 | * @typedef {Object} SocketArgsType 232 | * @property {string} server 233 | * @property {string[]} corsOrigin 234 | * @property {import("socket.io").ServerOptions} options 235 | * @property {(socket: import("socket.io").Socket) => void} callback 236 | * 237 | * @param {SocketArgsType} args 238 | */ 239 | constructor(args) { 240 | /** 241 | * @typedef {import("socket.io").Server} SocketServer 242 | * 243 | * @type {SocketServer} 244 | */ 245 | // console.log("this.socket:", this.socket); 246 | // if (!(this.socket instanceof socket.Server)) { 247 | // console.error("this.socket is not an instance of socket.Server"); 248 | // return; 249 | // } 250 | // const nsp = this.socket.of(process.env?.LISTEN_PATH); 251 | 252 | // nsp.use(this.Auth) 253 | // .on("connection", (socket) => { 254 | // this.connected = socket.connected; 255 | // args.callback(socket); 256 | // }); 257 | this.socket = new socket.Server(args.server, { 258 | ...args.options, 259 | }); 260 | // this.socket = new Socket(args.server, { 261 | // query: { 262 | // api_key: this.api_key, 263 | // }, 264 | // retries: 60, 265 | // }); 266 | 267 | // console.log("this.socket:", this.socket); 268 | 269 | // if (!(this.socket instanceof socket.Server)) { 270 | // console.error("this.socket is not an instance of socket.Server"); 271 | // return; 272 | // } 273 | 274 | // const nsp = this.socket.of(process.env?.LISTEN_PATH); 275 | 276 | // nsp.use(this.Auth).on("connection", (socket) => { 277 | // this.connected = socket.connected; 278 | // args.callback(socket); 279 | // }); 280 | 281 | this.socket 282 | .of(process.env?.LISTEN_PATH) 283 | .use(this.Auth) 284 | .on("connection", (socket) => { 285 | this.connected = socket.connected; 286 | 287 | args.callback(socket); 288 | }); 289 | } 290 | 291 | Auth(socket, next) { 292 | // console.log(`REQUESTS`); 293 | // console.log('Received headers:', socket.handshake.headers); 294 | // console.log('Received handshake:', socket.handshake); 295 | const apiKey = socket.handshake.headers.api_key; 296 | 297 | if (!apiKey) { 298 | console.error('API Key is empty or undefined'); 299 | return next(new Error('Authentication error')); 300 | } 301 | 302 | let decryptedKey; 303 | try { 304 | decryptedKey = crypto.AES.decrypt( 305 | apiKey, 306 | process.env.API_SECRET, 307 | ).toString(crypto.enc.Utf8); 308 | } catch (error) { 309 | console.error('Error during decryption:', error); 310 | return next(new Error('Authentication error')); 311 | } 312 | 313 | console.log('Decrypted Key:', decryptedKey); 314 | 315 | if (!decryptedKey) { 316 | console.error('Decrypted Key is empty or undefined'); 317 | return next(new Error('Authentication error')); 318 | } 319 | 320 | try { 321 | const parseKey = JSON.parse(decryptedKey); 322 | 323 | if (Date.now() > +parseKey.expireAt) 324 | return next(new Error('Authentication error')); 325 | 326 | next(); 327 | } catch (error) { 328 | console.error('Error parsing JSON:', error); 329 | return next(new Error('Authentication error')); 330 | } 331 | } 332 | 333 | /** 334 | * @typedef {Object} BanIPArgsType 335 | * @property {string} ip 336 | * @property {string} expireAt 337 | * 338 | * @param {BanIPArgsType} args 339 | * 340 | * @returns {void} 341 | */ 342 | BanIP(args) { 343 | if (!this.connected) { 344 | console.log(`Failed send ban ip to node, socket not connected`) 345 | return; 346 | } 347 | 348 | this.socket.emit("user:ip:ban", JSON.stringify(args)); 349 | console.log(`Sent ban IP to node`); 350 | } 351 | 352 | /** 353 | * @returns {void} 354 | */ 355 | UnbanIP() { 356 | if (!this.connected) { 357 | console.log(`Failed send unban ip to node, socket not connected`) 358 | return; 359 | } 360 | 361 | this.socket.emit("user:ip:unban", JSON.stringify({})); 362 | console.log(`Sent unban IP to node`); 363 | } 364 | } 365 | 366 | class Connection { 367 | /** 368 | * 369 | * @deprecated 370 | */ 371 | BanDB() { 372 | const dbPath = join(__dirname, "ban.sqlite"); 373 | 374 | new File().ForceExistsFile(dbPath); 375 | 376 | return new sqlite3.Database(dbPath); 377 | } 378 | } 379 | 380 | /** 381 | * @description IP Guard 382 | */ 383 | class IPGuard { 384 | /** 385 | * @param {IpGuardType} params 386 | */ 387 | constructor(params) { 388 | this.banDB = params.banDB; 389 | this.socket = params.socket; 390 | this.api = params.api; 391 | this.db = params.db; 392 | } 393 | 394 | /** 395 | * 396 | * @param {IPSDataType} record A user's record includes email, ips array 397 | * @param {function[]} callback Return function to allow ip usage 398 | * 399 | * @returns {void | Promise} 400 | */ 401 | async use(ip, ...callback) { 402 | let data = null; 403 | 404 | try { 405 | data = await callback[0](); 406 | } catch (e) {} 407 | 408 | if (!data) return await callback[1](); 409 | 410 | const indexOfIp = data.ips.findIndex((item) => item.ip === `${ip}`); 411 | 412 | const users = new File().GetJsonFile( 413 | join(__dirname, "users.json"), 414 | JSON.stringify([]) 415 | ); 416 | 417 | let user = null; 418 | 419 | const userIndex = users.findIndex((item) => item[0] === data.email); 420 | 421 | if (userIndex !== -1) { 422 | user = users[userIndex]; 423 | } 424 | 425 | const maxAllowConnection = user ? +user[1] : +process.env.MAX_ALLOW_USERS; 426 | 427 | const limited = data.ips.length > maxAllowConnection; 428 | 429 | if (indexOfIp !== -1 && limited) { 430 | return callback[2](); 431 | } 432 | 433 | console.log(`Пользователь: ${data.email} | IP: ${ip} | Подключено IP: ${(data.ips.length)} | Максимум IP: ${maxAllowConnection} `); 434 | 435 | if (data.ips.length >= maxAllowConnection && indexOfIp === -1) { 436 | if (process.env?.TARGET === "PROXY") { 437 | await this.deactiveUserProxy(data.email); 438 | 439 | return; 440 | } 441 | 442 | let file = new File() 443 | .GetCsvFile(join(__dirname, "blocked_ips.csv")) 444 | .toString(); 445 | 446 | file = file.split("\r\n").map((item) => item.split(",")); 447 | 448 | if (file.some((item) => item[0] === ip) === true) return; 449 | 450 | const blockedIpMessage = `${ip}`; 451 | 452 | data.ips.push({ ip: ip, date: new Date().toISOString() }); 453 | 454 | 455 | const connectedIpsMessage = data.ips.map((item) => `${item.ip}`).join('\n'); 456 | const connectedIpsMessagelog = data.ips.map((item) => `${item.ip}`).join('\n'); 457 | const databaseInstance = new DBSqlite3(); 458 | const ips = await databaseInstance.getUserIps(data.email); 459 | const connectedIpsMessageBase = ips.map((item) => `${item.ip || item}`).join('\n'); 460 | //const connectedIpsMessageBase = ips.map((ip) => `${ip}`).join('\n'); 461 | 462 | if (process.env?.TESTSCRIPT === "false") { 463 | this.socket.BanIP({ 464 | ip, 465 | expireAt: process.env.BAN_TIME, 466 | }); 467 | 468 | this.ban({ ip, email: data.email }); 469 | 470 | console.log(`[NOT TEST] ${new Date().toLocaleString('ru-RU')}: Заблокирован IP: ${ip} у пользователя ${data.email}\n\nПодключенные IP [Logical]:\n${connectedIpsMessagelog}\n\nПодключенные IP [DataBase]:\n${connectedIpsMessageBase}`); 471 | 472 | if (process.env.TG_ENABLE === "true") 473 | globalThis.bot.api.sendMessage( 474 | process.env.TG_ADMIN, 475 | "[NOT TEST] Пользователь " + data.email + ": IP " + ip + " заблокирован.\nВремя: " + process.env.BAN_TIME + " минут(ы)\n\nПодключено: "+ (data.ips.length) +"\nМаксимум устройств: " + maxAllowConnection +"\n\nПодключенные IP [Logical]:\n"+ connectedIpsMessage +"\n\nПодключенные IP [DataBase]:\n"+ connectedIpsMessageBase +"", 476 | { parse_mode: "HTML" } 477 | ); 478 | } else { 479 | console.log(`[TEST] ${new Date().toLocaleString('ru-RU')}: Заблокирован IP: ${ip} у пользователя ${data.email}`); 480 | 481 | if (process.env.TG_ENABLE === "true") 482 | globalThis.bot.api.sendMessage( 483 | process.env.TG_ADMIN, 484 | "[TEST] Пользователь " + data.email + ": IP " + ip + " заблокирован.\nВремя: " + process.env.BAN_TIME + " минут(ы)\n\nПодключено: "+ (data.ips.length) +"\nМаксимум устройств: " + maxAllowConnection +"\n\nПодключенные IP [Logical]:\n"+ connectedIpsMessage +"\n\nПодключенные IP [DataBase]:\n"+ connectedIpsMessageBase +"", 485 | { parse_mode: "HTML" } 486 | ); 487 | } 488 | 489 | 490 | return; 491 | } 492 | 493 | return await callback[1](); 494 | } 495 | 496 | /** 497 | * @param {BanIpConfigAddType} params 498 | */ 499 | ban(params) { 500 | banIP(`${params.ip}`, params.email); 501 | // console.log("ban", params); 502 | } 503 | 504 | /** 505 | * @param {string} email 506 | */ 507 | async deactiveUserProxy(email) { 508 | const path = join(__dirname, "deactives.json"); 509 | 510 | const deactives = new File().GetJsonFile(path); 511 | 512 | console.log("Deactives:", deactives); 513 | if (deactives.some((item) => item.email === email) === true) return; 514 | 515 | console.log("should to disable"); 516 | await this.api.changeUserProxyStatus(email, "disabled"); 517 | console.log("successfully disabled"); 518 | 519 | const fewMinutesLater = new Date( 520 | Date.now() + 1000 * 60 * process.env?.BAN_TIME, 521 | ).toISOString(); 522 | 523 | deactives.push({ 524 | email, 525 | activationAt: fewMinutesLater, 526 | }); 527 | 528 | console.log(email, fewMinutesLater, deactives, path); 529 | 530 | fs.writeFileSync(path, JSON.stringify(deactives)); 531 | 532 | this.db.deleteUser(email); 533 | 534 | if (process.env.TG_ENABLE === "true") 535 | globalThis.bot.api.sendMessage( 536 | process.env.TG_ADMIN, 537 | `${email} disabled successfully. 538 | Duration: ${process.env.BAN_TIME} minutes 539 | `, 540 | ); 541 | } 542 | 543 | /** 544 | * @param {string} email 545 | */ 546 | async activeUsersProxy() { 547 | const path = join(__dirname, "deactives.json"); 548 | 549 | const deactives = new File().GetJsonFile(path); 550 | 551 | const currentTime = new Date().getTime(); 552 | 553 | let shouldActive = deactives.filter((item) => { 554 | const activeAt = new Date(item.activationAt); 555 | return currentTime > activeAt; 556 | }); 557 | 558 | if (shouldActive.length === 0) return; 559 | 560 | for (let i in shouldActive) { 561 | const data = shouldActive[i]; 562 | 563 | await this.api.changeUserProxyStatus(data.email, "active"); 564 | } 565 | 566 | const _shouldActive = shouldActive.map((item) => item.email); 567 | 568 | const replaceData = deactives.filter( 569 | (item) => !_shouldActive.includes(item.email), 570 | ); 571 | 572 | fs.writeFileSync(path, JSON.stringify(replaceData)); 573 | 574 | if (process.env.TG_ENABLE === "true") 575 | globalThis.bot.api.sendMessage( 576 | process.env.TG_ADMIN, 577 | `${shouldActive 578 | .map((item) => item.email) 579 | .join(", ")} actived successfully. 580 | `, 581 | ); 582 | } 583 | } 584 | 585 | /** 586 | * @deprecated 587 | */ 588 | class BanDBConfig { 589 | constructor() { 590 | this.db = new Connection().BanDB(); 591 | 592 | this.db.serialize(() => { 593 | const sql = 594 | "CREATE TABLE IF NOT EXISTS banned (ip TEXT, cid TEXT, date TEXT)"; 595 | 596 | this.db.run(sql); 597 | }); 598 | } 599 | 600 | /** 601 | * @param {BanIpConfigAddType} params 602 | */ 603 | add(params) { 604 | this.db.serialize(() => { 605 | const sql = "SELECT * FROM banned WHERE cid = ?"; 606 | this.db.get(sql, [params.cid], (err, row) => { 607 | if (err) throw new Error(err); 608 | 609 | if (!row) { 610 | const sql = "INSERT INTO banned (ip, cid, date) VALUES (?, ?, ?) "; 611 | this.db.run( 612 | sql, 613 | [{ ...params, date: new Date().toLocaleString("en-US") }], 614 | (err) => { 615 | if (err) throw new Error(err); 616 | console.log("Ip banned:", cid); 617 | }, 618 | ); 619 | } 620 | 621 | console.log("Ip already banned:", params.cid); 622 | }); 623 | }); 624 | } 625 | 626 | remove(cid) { 627 | this.db.serialize(() => { 628 | const sql = "DELETE FROM banned WHERE cid = ?"; 629 | this.db.run(sql, [cid], (err) => { 630 | if (err) throw new Error(err); 631 | 632 | console.log("Ip unbanned:", cid); 633 | }); 634 | }); 635 | } 636 | } 637 | 638 | module.exports = { Ws, Api, Connection, Socket, IPGuard, BanDBConfig }; 639 | --------------------------------------------------------------------------------