================================================
FILE: tests/test_utils.py
================================================
"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
"""
import unittest
from utils.entrypoints import *
from utils.geoip import GeoIP, countryCodeToEmoji
from utils.proxy import getProxy
from utils.wireguard import generateWireguardKeys
# Change working directory to the root directory
os.chdir(os.path.join(os.path.dirname(__file__), ".."))
class TestUtils(unittest.TestCase):
def test_getProxy(self):
proxy = getProxy()
self.assertTrue(proxy.get("http") or proxy.get("https"))
def test_Wireguard(self):
privkey, pubkey = generateWireguardKeys()
self.assertTrue(privkey)
self.assertTrue(pubkey)
self.assertEqual(len(privkey), 44)
self.assertEqual(len(pubkey), 44)
def test_EntryPoints(self):
reloadEntrypoints()
entrypoints = getEntrypoints()
self.assertTrue(entrypoints)
self.assertTrue(len(entrypoints) > 0)
def test_GeoIP(self):
geoip = GeoIP('./config/geolite/GeoLite2-Country.mmdb')
country = geoip.lookup('8.8.8.8')
self.assertTrue(country)
self.assertEqual(country, "US")
self.assertEqual(countryCodeToEmoji(country), "🇺🇸")
countryNone = geoip.lookup('127.0.0.1')
self.assertFalse(countryNone)
self.assertEqual(countryCodeToEmoji(countryNone), "🌏")
geoip.close()
if __name__ == '__main__':
unittest.main()
================================================
FILE: utils/entrypoints.py
================================================
"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
"""
import copy
import csv
import logging
import os
from flask import current_app
from config import DELAY_THRESHOLD, LOSS_THRESHOLD
from models import Entrypoint
ENTRYPOINT_SCRIPT_PATH = './scripts/get_entrypoints.sh'
RESULT_PATH = './config/result.csv'
RESULT_LAST_MODIFIED = 0
RESULT_PATH_V6 = './config/result_v6.csv'
RESULT_LAST_MODIFIED_V6 = 0
ENTRYPOINTS = []
ENTRYPOINTS_V6 = []
def _getLogger():
"""
Get logger
:return: logger
"""
try:
if hasattr(current_app, 'logger'):
return current_app.logger
else:
return logging.getLogger(__name__)
except RuntimeError:
return logging.getLogger(__name__)
def readCsv(file_path):
"""
Read csv file
:param file_path: file path
:return: generator
"""
with open(file_path, 'r') as f:
reader = csv.reader(f)
for row in reader:
yield row
def reloadEntrypoints(ipv6=False):
"""
Reload entrypoints from csv file
:param ipv6: if load ipv6 entrypoints
:return: list of entrypoints
"""
global ENTRYPOINTS, ENTRYPOINTS_V6, RESULT_LAST_MODIFIED, RESULT_LAST_MODIFIED_V6
# Get logger
logger = _getLogger()
result_file = RESULT_PATH_V6 if ipv6 else RESULT_PATH
logger.info(f"Reload entrypoints from {result_file}")
if ipv6:
RESULT_LAST_MODIFIED_V6 = os.path.getmtime(result_file)
ENTRYPOINTS_V6 = []
else:
RESULT_LAST_MODIFIED = os.path.getmtime(result_file)
ENTRYPOINTS = []
entrypoints = copy.copy(ENTRYPOINTS_V6 if ipv6 else ENTRYPOINTS)
for row in readCsv(result_file):
try:
if row[0].lower() == 'ip:port':
continue
ip, port = row[0].split(':') if not ipv6 else (row[0].split("]:"))
ip = ip.replace('[', '') if ipv6 else ip
loss = float(row[1].replace('%', ''))
delay = float(row[2].replace('ms', ''))
if loss > LOSS_THRESHOLD or delay > DELAY_THRESHOLD:
continue
entrypoint = Entrypoint()
entrypoint.ip = ip
entrypoint.port = int(port)
entrypoint.loss = loss
entrypoint.delay = delay
entrypoints.append(entrypoint)
except Exception as e:
logger.error(f"Error when reading row: {row}, error: {e}")
return entrypoints
def getEntrypoints(ipv6=False):
"""
Get entrypoints
:param ipv6: if get ipv6 entrypoints
:return: list of entrypoints
"""
entrypoints = copy.copy(ENTRYPOINTS_V6 if ipv6 else ENTRYPOINTS)
# Get logger
logger = _getLogger()
if not entrypoints or len(entrypoints) == 0:
return reloadEntrypoints(ipv6)
last_modified = RESULT_LAST_MODIFIED_V6 if ipv6 else RESULT_LAST_MODIFIED
result_file = RESULT_PATH_V6 if ipv6 else RESULT_PATH
# Check if file has been modified
if last_modified != os.path.getmtime(result_file):
logger.info(f"File {last_modified} has been modified, will reload entrypoints.")
return reloadEntrypoints(ipv6)
return entrypoints
def getBestEntrypoints(num=1, ipv6=False):
"""
Get best entrypoints
:param num: number of entrypoints
:param ipv6: if get ipv6 entrypoints
:return: list of entrypoints
"""
# sort by loss and delay
returnEntryPoints = sorted(getEntrypoints(ipv6), key=lambda x: (x.loss, x.delay))[:num]
return returnEntryPoints
def optimizeEntrypoints():
"""
Optimize entrypoints
:return:
"""
# Get logger
logger = _getLogger()
# Check current path
if not os.path.exists(ENTRYPOINT_SCRIPT_PATH):
logger.error(f"File {ENTRYPOINT_SCRIPT_PATH} does not exist.")
return
# Fix ./scripts/get_entrypoint.sh if it has CRLF
file = open(ENTRYPOINT_SCRIPT_PATH, 'r')
data = file.read().replace('\r\n', '\n')
file.close()
file = open(ENTRYPOINT_SCRIPT_PATH, 'w')
file.write(data)
file.close()
# Get ipv4 entrypoints
print("Getting IPv4 entrypoints")
os.system(f"bash {ENTRYPOINT_SCRIPT_PATH} -4")
# Get ipv6 entrypoints
print("Getting IPv6 entrypoints")
os.system(f"bash {ENTRYPOINT_SCRIPT_PATH} -6")
# if __name__ == '__main__':
# reloadEntrypoints()
# print(ENTRYPOINTS)
# print(len(ENTRYPOINTS))
================================================
FILE: utils/geoip.py
================================================
"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
"""
import logging
import maxminddb
def countryCodeToEmoji(country_code):
"""
Convert country code to emoji.
:param country_code: A two-letter country code
:return: Corresponding country flag emoji if valid, otherwise a globe emoji
"""
if not country_code or len(country_code) != 2:
return '🌏'
OFFSET = 127462 - ord('A')
return chr(ord(country_code[0].upper()) + OFFSET) + chr(ord(country_code[1].upper()) + OFFSET)
class GeoIP:
def __init__(self, db_path: str) -> None:
self.db = maxminddb.open_database(db_path)
def lookup(self, ip: str) -> str or None:
"""
Lookup ip to get country code
:param ip:
:return:
"""
# Remove brackets from IPv6 addresses
if ip.startswith('['):
ip = ip.replace('[', '').replace(']', '')
result = self.db.get(ip)
try:
if result:
# Country field shows the accurate country code of the IP
if 'country' in result:
return result['country']['iso_code']
# If no country field, use the registered_country field
return result['registered_country']['iso_code']
else:
return None
except Exception as e:
logging.error(f"Failed to lookup ip: {ip}, error: {e}")
return None
def lookup_emoji(self, ip: str) -> str or None:
"""
Lookup ip to get country emoji
:param ip:
:return:
"""
result = self.lookup(ip)
return countryCodeToEmoji(result)
def close(self) -> None:
"""
Close database
:return:
"""
self.db.close()
================================================
FILE: utils/logger.py
================================================
"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
"""
import logging
import os
from logging.handlers import TimedRotatingFileHandler
def createLogger(filename, level=logging.INFO):
"""
Create logger with TimedRotatingFileHandler
:param filename: filename
:param level: logging level
:return: logger
"""
# Create logs directory
if not os.path.exists("logs"):
os.makedirs("logs")
# Create logger
logger = logging.getLogger(filename)
logger.setLevel(level)
# Create TimedRotatingFileHandler to rotate log file
log_filename = os.path.join("logs", filename + ".log")
file_handler = TimedRotatingFileHandler(log_filename, when="midnight", interval=1, backupCount=7)
# Create formatter
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)
# Create StreamHandler to output to console
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
# Add handlers
logger.addHandler(file_handler)
logger.addHandler(stream_handler)
return logger
================================================
FILE: utils/node_name.py
================================================
"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
"""
import uuid
from faker import Faker
class NodeNameGenerator:
"""
This class is used for generate unique node names
"""
def __init__(self, random_name=True):
self.random_name = random_name
self.fake = Faker()
self.used_names = set()
self.counter = 1 # Used when random_name is False
def generate_unique_name(self, country_emoji, country):
"""
Generate unique node name
:param country_emoji:
:param country:
:return:
"""
name_type = 'color'
cnt = 0 # Used to avoid infinite loop
while True:
if name_type == 'color':
name = f"{country_emoji} {country}-CF-{self.fake.color_name()}"
cnt += 1
if cnt > 100:
name_type = 'alternate' # If color name is not available, use alternate name
cnt = 0 # Reset counter
elif name_type == 'alternate':
name = f"{country_emoji} {country}-CF-{self.fake.city()}" # When color name is not available
cnt += 1
if cnt > 100:
name_type = 'random' # If alternate name is not available, use random name
cnt = 0
else: # When name_type is invalid
name = f"{country_emoji} {country}-CF-{uuid.uuid4()}" # Use UUID to ensure uniqueness
if name not in self.used_names:
self.used_names.add(name)
return name
def next(self, country_emoji, country):
"""
Generate next node name
:param country_emoji: Country emoji
:param country: Country name
:return: Node name
"""
if self.random_name:
# Try to generate a unique name, first try color name, then try alternate name
return self.generate_unique_name(country_emoji, country)
else:
name = f"{country_emoji} {country}-CF-WARP-{self.counter}"
self.counter += 1
return name
================================================
FILE: utils/proxy.py
================================================
"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
"""
import requests
from config import PROXY_POOL_URL
def getProxy():
"""
Get proxy from proxy pool
:return: proxy got from proxy pool
"""
try:
ret = requests.get(PROXY_POOL_URL, timeout=60).json()
proxy = {}
if ret.get('proxy'):
if ret['https']:
proxy = {"https": {ret['proxy']}}
else:
proxy = {"http": {ret['proxy']}}
return proxy
except TimeoutError:
return {}
================================================
FILE: utils/sub_useragent.py
================================================
"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
"""
# common user agent for proxy application,
# format:
# 'user-agent': 'app-name'
USERAGENT_FLAG = {
'clashforwindows': 'clash',
'clashx': 'clash',
'clashforandroid': 'clash',
'clashmetaforandroid': 'meta',
'clash-verge': 'meta',
'clash.meta': 'meta',
'surge': 'surge',
'shadowrocket': 'shadowrocket',
'v2ray': 'shadowrocket',
'sing-box': 'sing-box',
'loon': 'loon',
'nekobox': 'nekobox',
}
def getSubTypeFromUA(ua):
"""
Get subscription type from useragent
:param ua: useragent
:return:
"""
for key in USERAGENT_FLAG:
if ua.find(key) != -1:
return USERAGENT_FLAG[key]
# By default, return Clash
return 'clash'
================================================
FILE: utils/wireguard.py
================================================
"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
"""
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import x25519
from base64 import b64encode
def generateWireguardKeys():
"""
Generate WireGuard keys
:return: Private key and public key in Base64
"""
# Generate private key in X25519 format
private_key = x25519.X25519PrivateKey.generate()
private_bytes = private_key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption()
)
# Generate public key in X25519 format
public_key = private_key.public_key()
public_bytes = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
# Return Base64 encoded keys
return b64encode(private_bytes).decode('utf-8'), b64encode(public_bytes).decode('utf-8')
# if __name__ == '__main__':
# privatekey, publickey = generateWireguardKeys()
#
# print(f"Private Key: {privatekey}")
# print(f"Public Key: {publickey}")