From b55c8a2feca96eb3b67a4a87da64f80a6fa23e01 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sun, 10 Dec 2023 09:49:04 +0330 Subject: [PATCH 001/112] add: auth to api (incomplete) --- hiddifypanel/panel/authentication.py | 21 +++ .../commercial/restapi/v2/user/apps_api.py | 175 ++++++++++-------- .../commercial/restapi/v2/user/configs_api.py | 6 +- .../commercial/restapi/v2/user/info_api.py | 3 + .../commercial/restapi/v2/user/mtproxies.py | 8 +- .../commercial/restapi/v2/user/short_api.py | 2 + 6 files changed, 129 insertions(+), 86 deletions(-) create mode 100644 hiddifypanel/panel/authentication.py diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py new file mode 100644 index 000000000..040c07576 --- /dev/null +++ b/hiddifypanel/panel/authentication.py @@ -0,0 +1,21 @@ +from apiflask import HTTPBasicAuth, HTTPTokenAuth +from hiddifypanel.models import AdminUser, User +api_auth = HTTPTokenAuth("ApiKey") +basic_auth = HTTPBasicAuth() + + +@api_auth.verify_token +def verify_api_auth_token(token): + # for now, token is the same as uuid + user = User.query.filter(User.uuid == token).first() + if user: + return user + else: + return AdminUser.query.filter(AdminUser.uuid == token).first() + + +# @basic_auth.verify_password +# def verify_basic_auth_password(username, password): +# user = User.query.filter(User.username == username).first() +# if user and user.check_password(password): +# return user diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py index 406b08e93..2c6e1428e 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py @@ -4,18 +4,21 @@ from flask import current_app as app from flask import g, request from apiflask import Schema, abort -from apiflask.fields import String,URL,Enum,List,Nested +from apiflask.fields import String, URL, Enum, List, Nested from flask_babelex import lazy_gettext as _ -from urllib.parse import quote_plus,urlparse +from urllib.parse import quote_plus, urlparse import user_agents from strenum import StrEnum from enum import auto from hiddifypanel.panel.user.user import get_common_data -from hiddifypanel.hutils.utils import get_latest_release_url,do_base_64 +from hiddifypanel.hutils.utils import get_latest_release_url, do_base_64 +from hiddifypanel.panel.authentication import api_auth + +# region App Api DTOs + -#region App Api DTOs class AppInstallType(StrEnum): google_play = auto() app_store = auto() @@ -28,20 +31,24 @@ class AppInstallType(StrEnum): portable = auto() other = auto() + class AppInstall(Schema): title = String() - type = Enum(AppInstallType,required=True,description='The platform that provides the app to download') - url = URL(required=True,description='The url to download the app') + type = Enum(AppInstallType, required=True, description='The platform that provides the app to download') + url = URL(required=True, description='The url to download the app') + class AppSchema(Schema): - title = String(required=True,description='The title of the app') - description = String(required=True,description='The description of the app') - icon_url = URL(required=True,description='The icon url of the app') + title = String(required=True, description='The title of the app') + description = String(required=True, description='The description of the app') + icon_url = URL(required=True, description='The icon url of the app') guide_url = URL(description='The guide url of the app') - deeplink = URL(required=True,description='The deeplink of the app to imoprt configs') - install = List(Nested(AppInstall()),required=True,description='The install url of the app') + deeplink = URL(required=True, description='The deeplink of the app to imoprt configs') + install = List(Nested(AppInstall()), required=True, description='The install url of the app') # this class is not a Data Transfer Object, It's just an enum + + class Platform(StrEnum): all = auto() android = auto() @@ -51,10 +58,12 @@ class Platform(StrEnum): mac = auto() auto = auto() -class AppInSchema(Schema): - platform = Enum(Platform,load_default=Platform.auto,required=False,description='The platform (Operating System) to know what clients should be send. Possible values are: all, android, ios, windows, linux, mac, auto.') -#endregion +class AppInSchema(Schema): + platform = Enum( + Platform, load_default=Platform.auto, required=False, + description='The platform (Operating System) to know what clients should be send. Possible values are: all, android, ios, windows, linux, mac, auto.') +# endregion class AppAPI(MethodView): @@ -77,22 +86,23 @@ def __init__(self) -> None: # self.clash_meta_all_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/meta/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mall_{domain}-{c['mode']}" # self.clash_meta_foreign_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/meta/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mnormal_{domain}-{c['mode']}" self.clash_meta_blocked_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/meta/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mlite_{domain}-{c['mode']}" - + @app.input(AppInSchema, arg_name='data', location="query") @app.output(AppSchema(many=True)) - def get(self,data): + @app.auth_required(api_auth) + def get(self, data): # parse user agent if data['platform'] == Platform.auto: platfrom = self.__get_ua_platform() if not platfrom: - abort(400,'Your selected platform is invalid!') + abort(400, 'Your selected platform is invalid!') self.platform = platfrom else: self.platform = data['platform'] # output data apps_data = [] - + match self.platform: case Platform.all: apps_data = self.__get_all_apps_dto() @@ -102,27 +112,27 @@ def get(self,data): v2rayng_dto = self.__get_v2rayng_app_dto() hiddify_clash_android_dto = self.__get_hiddify_clash_android_app_dto() nekobox_dto = self.__get_nekobox_app_dto() - apps_data += ([hiddify_next_dto,hiddifyng_dto,v2rayng_dto,hiddify_clash_android_dto,nekobox_dto]) + apps_data += ([hiddify_next_dto, hiddifyng_dto, v2rayng_dto, hiddify_clash_android_dto, nekobox_dto]) case Platform.windows: hiddify_next_dto = self.__get_hiddify_next_app_dto() hiddify_clash_dto = self.__get_hiddify_clash_desktop_app_dto() hiddifyn_dto = self.__get_hiddifyn_app_dto() - apps_data += ([hiddify_next_dto,hiddifyng_dto,hiddify_clash_dto,hiddifyn_dto]) + apps_data += ([hiddify_next_dto, hiddifyng_dto, hiddify_clash_dto, hiddifyn_dto]) case Platform.ios: stash_dto = self.__get_stash_app_dto() shadowrocket_dto = self.__get_shadowrocket_app_dto() foxray_dto = self.__get_foxray_app_dto() streisand_dto = self.__get_streisand_app_dto() loon_dto = self.__get_loon_app_dto() - apps_data += ([streisand_dto,stash_dto,shadowrocket_dto,foxray_dto,loon_dto]) + apps_data += ([streisand_dto, stash_dto, shadowrocket_dto, foxray_dto, loon_dto]) case Platform.linux: hiddify_next_dto = self.__get_hiddify_next_app_dto() hiddify_clash_dto = self.__get_hiddify_clash_desktop_app_dto() - apps_data += ([hiddify_next_dto,hiddify_clash_dto,]) + apps_data += ([hiddify_next_dto, hiddify_clash_dto,]) case Platform.mac: hiddify_clash_dto = self.__get_hiddify_clash_desktop_app_dto() hiddify_next_dto = self.__get_hiddify_next_app_dto() - apps_data += ([hiddify_next_dto,hiddify_clash_dto]) + apps_data += ([hiddify_next_dto, hiddify_clash_dto]) return apps_data @@ -140,6 +150,7 @@ def __get_ua_platform(self): return Platform.linux return None + def __get_all_apps_dto(self): hiddifyn_app_dto = self.__get_hiddifyn_app_dto() v2rayng_app_dto = self.__get_v2rayng_app_dto() @@ -153,47 +164,48 @@ def __get_all_apps_dto(self): hiddify_clash_app_dto = self.__get_hiddify_clash_desktop_app_dto() hiddify_next_app_dto = self.__get_hiddify_next_app_dto() return [ - hiddifyn_app_dto,v2rayng_app_dto,hiddifyng_app_dto,hiddify_android_app_dto, - foxray_app_dto,shadowrocket_app_dto,streisand_app_dto, - loon_app_dto,stash_app_dto,hiddify_clash_app_dto,hiddify_next_app_dto - ] - def __get_app_icon_url(self,app_name): + hiddifyn_app_dto, v2rayng_app_dto, hiddifyng_app_dto, hiddify_android_app_dto, + foxray_app_dto, shadowrocket_app_dto, streisand_app_dto, + loon_app_dto, stash_app_dto, hiddify_clash_app_dto, hiddify_next_app_dto + ] + + def __get_app_icon_url(self, app_name): base = f'https://{urlparse(request.base_url).hostname}' url = '' if app_name == _('app.hiddify.next.title'): - url = base + url_for('static',filename='apps-icon/hiddify_next.ico') + url = base + url_for('static', filename='apps-icon/hiddify_next.ico') elif app_name == _('app.hiddifyn.title'): - url = base + url_for('static',filename='apps-icon/hiddifyn.ico') + url = base + url_for('static', filename='apps-icon/hiddifyn.ico') elif app_name == _('app.v2rayng.title'): - url = base + url_for('static',filename='apps-icon/v2rayng.ico') + url = base + url_for('static', filename='apps-icon/v2rayng.ico') elif app_name == _('app.hiddifyng.title'): - url = base + url_for('static',filename='apps-icon/hiddifyng.ico') + url = base + url_for('static', filename='apps-icon/hiddifyng.ico') elif app_name == _('app.hiddify-clash-android.title'): - url = base + url_for('static',filename='apps-icon/hiddify_android.ico') + url = base + url_for('static', filename='apps-icon/hiddify_android.ico') elif app_name == _('app.foxray.title'): - url = base + url_for('static',filename='apps-icon/foxray.ico') + url = base + url_for('static', filename='apps-icon/foxray.ico') elif app_name == _('app.shadowrocket.title'): - url = base + url_for('static',filename='apps-icon/shadowrocket.ico') + url = base + url_for('static', filename='apps-icon/shadowrocket.ico') elif app_name == _('app.streisand.title'): - url = base + url_for('static',filename='apps-icon/streisand.ico') + url = base + url_for('static', filename='apps-icon/streisand.ico') elif app_name == _('app.loon.title'): - url = base + url_for('static',filename='apps-icon/loon.ico') + url = base + url_for('static', filename='apps-icon/loon.ico') elif app_name == _('app.stash.title'): - url = base + url_for('static',filename='apps-icon/stash.ico') + url = base + url_for('static', filename='apps-icon/stash.ico') elif app_name == _('app.hiddify-clash-desktop.title'): - url = base + url_for('static',filename='apps-icon/hiddify_clash.ico') + url = base + url_for('static', filename='apps-icon/hiddify_clash.ico') elif app_name == _('app.nekobox.title'): - url = base + url_for(endpoint='static',filename='apps-icon/nekobox.ico') + url = base + url_for(endpoint='static', filename='apps-icon/nekobox.ico') return url - def __get_app_install_dto(self,install_type:AppInstallType,url,title=''): - install_dto = AppInstall() - install_dto.title = title - install_dto.type = install_type - install_dto.url = url - return install_dto - + def __get_app_install_dto(self, install_type: AppInstallType, url, title=''): + install_dto = AppInstall() + install_dto.title = title + install_dto.type = install_type + install_dto.url = url + return install_dto + def __get_hiddifyn_app_dto(self): dto = AppSchema() dto.title = _('app.hiddifyn.title') @@ -203,7 +215,7 @@ def __get_hiddifyn_app_dto(self): dto.deeplink = f'hiddify://install-sub?url={self.subscription_link_url}' ins_url = f'{self.hiddify_github_repo}/HiddifyN/releases/latest/download/HiddifyN.zip' - dto.install = [self.__get_app_install_dto(AppInstallType.portable,ins_url)] + dto.install = [self.__get_app_install_dto(AppInstallType.portable, ins_url)] return dto def __get_nekobox_app_dto(self): @@ -216,8 +228,9 @@ def __get_nekobox_app_dto(self): latest_url, version = get_latest_release_url(f'https://github.com/MatsuriDayo/NekoBoxForAndroid') ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/NB4A-{version}-x86_64.apk' - dto.install = [self.__get_app_install_dto(AppInstallType.apk,ins_url)] + dto.install = [self.__get_app_install_dto(AppInstallType.apk, ins_url)] return dto + def __get_v2rayng_app_dto(self): dto = AppSchema() dto.title = _('app.v2rayng.title') @@ -225,12 +238,12 @@ def __get_v2rayng_app_dto(self): dto.icon_url = self.__get_app_icon_url(_('app.v2rayng.title')) dto.guide_url = 'https://www.youtube.com/watch?v=6HncctDHXVs' dto.deeplink = f'v2rayng://install-sub/?url={self.user_panel_encoded_url}' - + # make v2rayng latest version url download latest_url, version = get_latest_release_url(f'https://github.com/2dust/v2rayNG/') github_ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/v2rayNG_{version}.apk' google_play_ins_url = 'https://play.google.com/store/apps/details?id=com.v2ray.ang' - dto.install = [self.__get_app_install_dto(AppInstallType.apk,github_ins_url),self.__get_app_install_dto(AppInstallType.google_play, google_play_ins_url)] + dto.install = [self.__get_app_install_dto(AppInstallType.apk, github_ins_url), self.__get_app_install_dto(AppInstallType.google_play, google_play_ins_url)] return dto def __get_hiddifyng_app_dto(self): @@ -240,12 +253,11 @@ def __get_hiddifyng_app_dto(self): dto.icon_url = self.__get_app_icon_url(_('app.hiddifyng.title')) dto.guide_url = 'https://www.youtube.com/watch?v=qDbI72J-INM' dto.deeplink = f'hiddify://install-sub/?url={self.user_panel_encoded_url}' - - latest_url,version = get_latest_release_url(f'{self.hiddify_github_repo}/HiddifyNG/') + latest_url, version = get_latest_release_url(f'{self.hiddify_github_repo}/HiddifyNG/') github_ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/HiddifyNG.apk' google_play_ins_url = 'https://play.google.com/store/apps/details?id=ang.hiddify.com' - dto.install = [self.__get_app_install_dto(AppInstallType.apk,github_ins_url),self.__get_app_install_dto(AppInstallType.google_play, google_play_ins_url)] + dto.install = [self.__get_app_install_dto(AppInstallType.apk, github_ins_url), self.__get_app_install_dto(AppInstallType.google_play, google_play_ins_url)] return dto def __get_hiddify_clash_android_app_dto(self): @@ -255,9 +267,9 @@ def __get_hiddify_clash_android_app_dto(self): dto.icon_url = self.__get_app_icon_url(_('app.hiddify-clash-android.title')) dto.guide_url = 'https://www.youtube.com/watch?v=mUTfYd1_UCM' dto.deeplink = f'clash://install-config/?url={self.user_panel_encoded_url}' - latest_url,version = get_latest_release_url(f'{self.hiddify_github_repo}/HiddifyClashAndroid/') + latest_url, version = get_latest_release_url(f'{self.hiddify_github_repo}/HiddifyClashAndroid/') ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/hiddify-{version}-meta-alpha-universal-release.apk' - dto.install = [self.__get_app_install_dto(AppInstallType.apk,ins_url),] + dto.install = [self.__get_app_install_dto(AppInstallType.apk, ins_url),] return dto def __get_foxray_app_dto(self): @@ -269,7 +281,7 @@ def __get_foxray_app_dto(self): dto.deeplink = f'https://yiguo.dev/sub/add/?url={do_base_64(self.subscription_link_encoded_url)}#{self.profile_title}' ins_url = 'https://apps.apple.com/us/app/foxray/id6448898396' - dto.install = [self.__get_app_install_dto(AppInstallType.app_store,ins_url),] + dto.install = [self.__get_app_install_dto(AppInstallType.app_store, ins_url),] return dto def __get_shadowrocket_app_dto(self): @@ -281,8 +293,9 @@ def __get_shadowrocket_app_dto(self): dto.deeplink = f'sub://{do_base_64(self.user_panel_url)}' ins_url = 'https://apps.apple.com/us/app/shadowrocket/id932747118' - dto.install = [self.__get_app_install_dto(AppInstallType.app_store,ins_url),] + dto.install = [self.__get_app_install_dto(AppInstallType.app_store, ins_url),] return dto + def __get_streisand_app_dto(self): dto = AppSchema() dto.title = _('app.streisand.title') @@ -291,8 +304,9 @@ def __get_streisand_app_dto(self): dto.guide_url = 'https://www.youtube.com/watch?v=jaMkZTLH2QY' dto.deeplink = f'streisand://import/{self.user_panel_url}#{self.profile_title}' ins_url = 'https://apps.apple.com/app/id6450534064' - dto.install = [self.__get_app_install_dto(AppInstallType.app_store,ins_url),] + dto.install = [self.__get_app_install_dto(AppInstallType.app_store, ins_url),] return dto + def __get_loon_app_dto(self): dto = AppSchema() dto.title = _('app.loon.title') @@ -301,7 +315,8 @@ def __get_loon_app_dto(self): dto.guide_url = '' dto.deeplink = f'loon://import?nodelist={self.user_panel_encoded_url}' ins_url = 'https://apps.apple.com/app/id1373567447' - dto.install = [self.__get_app_install_dto(AppInstallType.app_store,ins_url)] + dto.install = [self.__get_app_install_dto(AppInstallType.app_store, ins_url)] + def __get_stash_app_dto(self): dto = AppSchema() dto.title = _('app.stash.title') @@ -311,7 +326,7 @@ def __get_stash_app_dto(self): dto.deeplink = f'clash://install-config/?url={self.user_panel_encoded_url}' ins_url = 'https://apps.apple.com/us/app/stash-rule-based-proxy/id1596063349' - dto.install = [self.__get_app_install_dto(AppInstallType.app_store,ins_url),] + dto.install = [self.__get_app_install_dto(AppInstallType.app_store, ins_url),] return dto def __get_hiddify_clash_desktop_app_dto(self): @@ -322,41 +337,40 @@ def __get_hiddify_clash_desktop_app_dto(self): dto.guide_url = 'https://www.youtube.com/watch?v=omGIz97mbzM' dto.deeplink = f'clash://install-config/?url={self.user_panel_encoded_url}' dto.install = [] - + # make hiddify clash latest version url download latest_url, version = get_latest_release_url(f'{self.hiddify_github_repo}/hiddifydesktop') - version = version.replace('v','') + version = version.replace('v', '') platform = self.platform if self.platform == Platform.all: - platform = [Platform.windows,Platform.linux,Platform.mac] - + platform = [Platform.windows, Platform.linux, Platform.mac] + if isinstance(platform, list): for p in platform: match p: case Platform.windows: ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/HiddifyClashDesktop_{version}_x64_en-US.msi' - dto.install.append(self.__get_app_install_dto(AppInstallType.setup,ins_url)) + dto.install.append(self.__get_app_install_dto(AppInstallType.setup, ins_url)) case Platform.linux: ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/hiddify-clash-desktop_{version}_amd64.AppImage' - dto.install.append(self.__get_app_install_dto(AppInstallType.appimage,ins_url)) + dto.install.append(self.__get_app_install_dto(AppInstallType.appimage, ins_url)) case Platform.mac: ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/HiddifyClashDesktop_{version}x64.dmg' - dto.install.append(self.__get_app_install_dto(AppInstallType.dmg,ins_url)) + dto.install.append(self.__get_app_install_dto(AppInstallType.dmg, ins_url)) else: match platform: case Platform.windows: ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/HiddifyClashDesktop_{version}_x64_en-US.msi' - dto.install.append(self.__get_app_install_dto(AppInstallType.setup,ins_url)) + dto.install.append(self.__get_app_install_dto(AppInstallType.setup, ins_url)) case Platform.linux: ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/hiddify-clash-desktop_{version}_amd64.AppImage' - dto.install.append(self.__get_app_install_dto(AppInstallType.appimage,ins_url)) + dto.install.append(self.__get_app_install_dto(AppInstallType.appimage, ins_url)) case Platform.mac: ins_url = latest_url.split('releases/')[0] + f'releases/download/{version}/HiddifyClashDesktop_{version}x64.dmg' - dto.install.append(self.__get_app_install_dto(AppInstallType.dmg,ins_url)) - - + dto.install.append(self.__get_app_install_dto(AppInstallType.dmg, ins_url)) + return dto def __get_hiddify_next_app_dto(self): @@ -370,18 +384,17 @@ def __get_hiddify_next_app_dto(self): # availabe installatoin types installation_types = [] if self.platform == Platform.all: - installation_types = [AppInstallType.apk,AppInstallType.google_play,AppInstallType.setup,AppInstallType.portable,AppInstallType.appimage,AppInstallType.dmg] + installation_types = [AppInstallType.apk, AppInstallType.google_play, AppInstallType.setup, AppInstallType.portable, AppInstallType.appimage, AppInstallType.dmg] else: match self.platform: case Platform.android: - installation_types = [AppInstallType.apk,AppInstallType.google_play] + installation_types = [AppInstallType.apk, AppInstallType.google_play] case Platform.windows: - installation_types = [AppInstallType.setup,AppInstallType.portable] + installation_types = [AppInstallType.setup, AppInstallType.portable] case Platform.linux: installation_types = [AppInstallType.appimage] case Platform.mac: installation_types = [AppInstallType.dmg] - install_dtos = [] for install_type in installation_types: @@ -400,11 +413,9 @@ def __get_hiddify_next_app_dto(self): ins_url = f'{self.hiddify_github_repo}/hiddify-next/releases/latest/download/hiddify-linux-x64.zip' case AppInstallType.dmg: ins_url = f'{self.hiddify_github_repo}/hiddify-next/releases/latest/download/hiddify-macos-universal.zip' - - install_dto = self.__get_app_install_dto(install_type,ins_url) + + install_dto = self.__get_app_install_dto(install_type, ins_url) install_dtos.append(install_dto) - + dto.install = install_dtos return dto - - \ No newline at end of file diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py index c8420d7d1..e44e2d57b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py @@ -2,13 +2,14 @@ from flask import g, request from flask import current_app as app from flask.views import MethodView -from hiddifypanel.models.config import hconfig +from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum from hiddifypanel.panel import hiddify from apiflask import Schema from apiflask.fields import String from hiddifypanel.panel.user.user import get_common_data from hiddifypanel.panel.user import link_maker +from hiddifypanel.panel.authentication import api_auth class ConfigSchema(Schema): @@ -20,10 +21,12 @@ class ConfigSchema(Schema): security = String(required=True) type = String(required=True) + class AllConfigsAPI(MethodView): decorators = [hiddify.user_auth] @app.output(ConfigSchema(many=True)) + @app.auth_required(api_auth) def get(self): def create_item(name, domain, type, protocol, transport, security, link): dto = ConfigSchema() @@ -110,4 +113,3 @@ def create_item(name, domain, type, protocol, transport, security, link): ) return items - diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py index 8fca5f8be..148d4b093 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py @@ -12,6 +12,7 @@ from hiddifypanel.panel import hiddify from hiddifypanel.panel.database import db from hiddifypanel.panel.user.user import get_common_data +from hiddifypanel.panel.authentication import api_auth class ProfileSchema(Schema): @@ -40,6 +41,7 @@ class InfoAPI(MethodView): decorators = [hiddify.user_auth] @app.output(ProfileSchema) + @app.auth_required(api_auth) def get(self): c = get_common_data(g.user_uuid, 'new') @@ -62,6 +64,7 @@ def get(self): return dto @app.input(UserInfoChangableSchema, arg_name='data') + @app.auth_required(api_auth) def patch(self, data): if data['telegram_id']: try: diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py index 9cdc5d7fb..32bb6a64e 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py @@ -10,19 +10,23 @@ from hiddifypanel.panel.user.user import get_common_data from hiddifypanel.panel import hiddify +from hiddifypanel.panel.authentication import api_auth + class MtproxySchema(Schema): link = String(required=True) title = String(required=True) + class MTProxiesAPI(MethodView): decorators = [hiddify.user_auth] @app.output(MtproxySchema(many=True)) + @app.auth_required(api_auth) def get(self): # check mtproxie is enable if not hconfig(ConfigEnum.telegram_enable, g.user_uuid): - abort(status_code=404,message="Telegram mtproxy is not enable") + abort(status_code=404, message="Telegram mtproxy is not enable") # get domains c = get_common_data(g.user_uuid, 'new') dtos = [] @@ -37,4 +41,4 @@ def get(self): dto.title = d.alias or d.domain dto.link = server_link dtos.append(dto) - return dtos \ No newline at end of file + return dtos diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py index 473ee3e8b..e26c0d0dc 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py @@ -9,6 +9,7 @@ from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum from hiddifypanel.panel import hiddify +from hiddifypanel.panel.authentication import api_auth class ShortSchema(Schema): @@ -21,6 +22,7 @@ class ShortAPI(MethodView): decorators = [hiddify.user_auth] @app.output(ShortSchema) + @app.auth_required(api_auth) def get(self): short, expire_in = hiddify.add_short_link("/"+hconfig(ConfigEnum.proxy_path)+"/"+str(g.user_uuid)+"/") full_url = f"https://{urlparse(request.base_url).hostname}/{short}" From e313142d5f8200c2ccdd91717a179341bddbd852 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sun, 10 Dec 2023 13:27:33 +0330 Subject: [PATCH 002/112] add: username & password to user model and admin model - filling the username and password (password saved in database as plaintext) --- hiddifypanel/hutils/utils.py | 29 ++++++++++++++++--- hiddifypanel/models/admin.py | 41 ++++++++++++++++++++++++++ hiddifypanel/models/user.py | 43 +++++++++++++++++++++++++++- hiddifypanel/panel/authentication.py | 12 ++++---- hiddifypanel/panel/init_db.py | 22 ++++++++++++++ 5 files changed, 137 insertions(+), 10 deletions(-) diff --git a/hiddifypanel/hutils/utils.py b/hiddifypanel/hutils/utils.py index 794e3f0d2..57f560e2d 100644 --- a/hiddifypanel/hutils/utils.py +++ b/hiddifypanel/hutils/utils.py @@ -37,13 +37,15 @@ def url_encode(strr): import urllib.parse return urllib.parse.quote(strr) -def is_uuid_valid(uuid,version): + +def is_uuid_valid(uuid, version): try: uuid_obj = UUID(uuid, version=version) except ValueError: return False return str(uuid_obj) == uuid + def error(str): print(str, file=sys.stderr) @@ -53,11 +55,14 @@ def static_url_for(**values): orig = url_for("static", **values) return orig.split("user_secret")[0] + @cache.cache(ttl=60000) def get_latest_release_url(repo): - latest_url = requests.get(f'{repo}/releases/latest').url.strip() - version = latest_url.split('tag/')[1].strip() - return (latest_url,version) + latest_url = requests.get(f'{repo}/releases/latest').url.strip() + version = latest_url.split('tag/')[1].strip() + return (latest_url, version) + + @cache.cache(ttl=600) def get_latest_release_version(repo_name): try: @@ -104,6 +109,22 @@ def get_random_string(min_=10, max_=30): return result_str +def get_random_password(length: int = 16) -> str: + '''Retunrns a random password with fixed length''' + characters = string.ascii_letters + string.digits # + '-' + while True: + passwd = ''.join(random.choice(characters) for i in range(length)) + if (any(c.islower() for c in passwd) and any(c.isupper() for c in passwd) and sum(c.isdigit() for c in passwd) > 1): + return passwd + + +def is_assci_alphanumeric(str): + for c in str: + if c not in string.ascii_letters + string.digits: + return False + return True + + def date_to_json(d): return d.strftime("%Y-%m-%d") if d else None diff --git a/hiddifypanel/models/admin.py b/hiddifypanel/models/admin.py index 9563cd529..5855fd866 100644 --- a/hiddifypanel/models/admin.py +++ b/hiddifypanel/models/admin.py @@ -2,6 +2,7 @@ from enum import auto from flask import g +from hiddifypanel import hutils from hiddifypanel.models.usage import DailyUsage from sqlalchemy_serializer import SerializerMixin from strenum import StrEnum @@ -34,6 +35,8 @@ class AdminUser(db.Model, SerializerMixin): id = db.Column(db.Integer, primary_key=True, autoincrement=True) uuid = db.Column(db.String(36), default=lambda: str(uuid_mod.uuid4()), nullable=False, unique=True) name = db.Column(db.String(512), nullable=False) + username = db.Column(db.String(16), nullable=True, default='') + password = db.Column(db.String(16), nullable=True, default='') mode = db.Column(db.Enum(AdminMode), default=AdminMode.agent, nullable=False) can_add_admin = db.Column(db.Boolean, default=False, nullable=False) max_users = db.Column(db.Integer, default=100, nullable=False) @@ -174,3 +177,41 @@ def current_admin_or_owner(): if g and hasattr(g, 'admin') and g.admin: return g.admin return AdminUser.query.filter(AdminUser.id == 1).first() + + +def fill_username(admin: AdminUser) -> None: + minimum_username_length = 10 + if not admin.username or len(admin.username) < 10: + base_username = '' + rand_str = '' + # if the username chats isn't only string.ascii_letters, it's invalid + # because we can't set non ascii characters in the http header (https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication) + if admin.name and hutils.utils.is_assci_alphanumeric(admin.name): + # user actual name + base_username = admin.name.replace(' ', '_') + if len(base_username) > minimum_username_length - 1: + # check if the name is unique, if it's not we add some random char to it + while AdminUser.query.filter(AdminUser.username == base_username + rand_str).first(): + rand_str = hutils.utils.get_random_string(2, 4) + else: + needed_length = minimum_username_length - len(base_username) + rand_str = hutils.utils.get_random_string(needed_length, needed_length) + while AdminUser.query.filter(AdminUser.username == base_username + rand_str).first(): + rand_str = hutils.utils.get_random_string(needed_length, needed_length) + else: + base_username = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) + while AdminUser.query.filter(AdminUser.username == base_username + rand_str).first(): + rand_str = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) + + admin.username = base_username + rand_str if rand_str else base_username + + +def fill_password(admin: AdminUser) -> None: + # TODO: hash the password + if not admin.password or len(admin.password) < 16: + base_passwd = hutils.utils.get_random_password() + rand_str = '' + # if passwd is duplicated, we create another one + while AdminUser.query.filter(AdminUser.password == base_passwd + rand_str).first(): + rand_str = hutils.utils.get_random_password() + admin.password = base_passwd + rand_str diff --git a/hiddifypanel/models/user.py b/hiddifypanel/models/user.py index 1d90d5246..b918a1d31 100644 --- a/hiddifypanel/models/user.py +++ b/hiddifypanel/models/user.py @@ -3,6 +3,7 @@ from enum import auto from dateutil import relativedelta +from hiddifypanel import hutils from sqlalchemy_serializer import SerializerMixin from strenum import StrEnum @@ -56,6 +57,8 @@ class User(db.Model, SerializerMixin): id = db.Column(db.Integer, primary_key=True, autoincrement=True) uuid = db.Column(db.String(36), default=lambda: str(uuid_mod.uuid4()), nullable=False, unique=True) name = db.Column(db.String(512), nullable=False, default='') + username = db.Column(db.String(16), nullable=True, default='') + password = db.Column(db.String(16), nullable=True, default='') last_online = db.Column(db.DateTime, nullable=False, default=datetime.datetime.min) # removed expiry_time = db.Column(db.Date, default=datetime.date.today() + relativedelta.relativedelta(months=6)) @@ -180,7 +183,7 @@ def to_dict(d, convert_date=True): } -def remove(user: User, commit=True): +def remove(user: User, commit=True) -> None: from hiddifypanel.drivers import user_driver user_driver.remove_client(user) user.delete() @@ -351,3 +354,41 @@ def bulk_register_users(users=[], commit=True, remove=False): db.session.delete(d) if commit: db.session.commit() + + +def fill_username(user: User) -> None: + minimum_username_length = 10 + if not user.username or len(user.username) < 10: + base_username = '' + rand_str = '' + # if the username chats isn't only string.ascii_letters, it's invalid + # because we can't set non ascii characters in the http header (https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication) + if user.name and hutils.utils.is_assci_alphanumeric(user.name): + # user actual name + base_username = user.name.replace(' ', '_') + if len(base_username) > minimum_username_length - 1: + # check if the name is unique, if it's not we add some random char to it + while User.query.filter(User.username == base_username + rand_str).first(): + rand_str = hutils.utils.get_random_string(2, 4) + else: + needed_length = minimum_username_length - len(base_username) + rand_str = hutils.utils.get_random_string(needed_length, needed_length) + while User.query.filter(User.username == base_username + rand_str).first(): + rand_str = hutils.utils.get_random_string(needed_length, needed_length) + else: + base_username = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) + while User.query.filter(User.username == base_username + rand_str).first(): + rand_str = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) + + user.username = base_username + rand_str if rand_str else base_username + + +def fill_password(user: User) -> None: + # TODO: hash the password + if not user.password or len(user.password) < 16: + base_passwd = hutils.utils.get_random_password() + rand_str = '' + # if passwd is duplicated, we create another one + while User.query.filter(User.password == base_passwd + rand_str).first(): + rand_str = hutils.utils.get_random_password() + user.password = base_passwd + rand_str diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index 040c07576..b0f4163c1 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -14,8 +14,10 @@ def verify_api_auth_token(token): return AdminUser.query.filter(AdminUser.uuid == token).first() -# @basic_auth.verify_password -# def verify_basic_auth_password(username, password): -# user = User.query.filter(User.username == username).first() -# if user and user.check_password(password): -# return user +@basic_auth.verify_password +def verify_basic_auth_password(username, password): + user = User.query.filter(User.username == username, User.password == password).first() + if user: + return user + else: + return AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() diff --git a/hiddifypanel/panel/init_db.py b/hiddifypanel/panel/init_db.py index 35196f785..1c6e9f736 100644 --- a/hiddifypanel/panel/init_db.py +++ b/hiddifypanel/panel/init_db.py @@ -2,6 +2,7 @@ import json import os import random +import string import sys import uuid @@ -25,6 +26,11 @@ def init_db(): get_hconfigs.invalidate_all() db_version = int(hconfig(ConfigEnum.db_version) or 0) add_column(User.lang) + add_column(User.username) + add_column(User.password) + add_column(AdminUser.username) + add_column(AdminUser.password) + if db_version == latest_db_version(): return Events.db_prehook.notify() @@ -138,6 +144,22 @@ def init_db(): # add_config_if_not_exist(ConfigEnum.hysteria_enable, True) # add_config_if_not_exist(ConfigEnum.hysteria_port, random.randint(5000, 20000)) +def _v59(): + # set user model username and password + for u in User.query.all(): + user.fill_username(u) + user.fill_password(u) + + # set admin model username and password + for a in AdminUser.query.all(): + admin.fill_username(a) + admin.fill_password(a) + + +def __fix_username_and_password(model_type: AdminUser | User): + + pass + def _v57(): add_config_if_not_exist(ConfigEnum.warp_sites, "") From 38ad0de2f614861cdbd48ba387abd6b51ef1da05 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sun, 10 Dec 2023 22:10:11 +0330 Subject: [PATCH 003/112] add: auth to admin apis --- .../restapi/v2/admin/admin_info_api.py | 12 ++++++--- .../restapi/v2/admin/admin_user_api.py | 24 ++++++++---------- .../restapi/v2/admin/admin_users_api.py | 6 +++-- .../restapi/v2/admin/server_status_api.py | 10 +++++--- .../commercial/restapi/v2/admin/user_api.py | 25 +++++++++---------- .../commercial/restapi/v2/admin/users_api.py | 10 +++++--- 6 files changed, 46 insertions(+), 41 deletions(-) diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py index e39ab1011..a3a6b100b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py @@ -4,14 +4,18 @@ from apiflask import abort from hiddifypanel.models.admin import AdminUser, get_admin_user_db -from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum, Lang +from hiddifypanel.models.config import hconfig from hiddifypanel.panel import hiddify +from hiddifypanel.panel.authentication import api_auth from .admin_user_api import AdminSchema + class AdminInfoApi(MethodView): - decorators = [hiddify.super_admin] + decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] + @app.output(AdminSchema) + # @app.auth_required(api_auth, roles=['super_admin', 'admin']) def get(self): # in this case g.user_uuid is equal to admin uuid admin_uuid = g.user_uuid @@ -25,5 +29,5 @@ def get(self): dto.can_add_admin = admin.can_add_admin dto.parent_admin_uuid = AdminUser.query.filter(AdminUser.id == admin.parent_admin_id).first().uuid or 'None' dto.telegram_id = admin.telegram_id - dto.lang = Lang(hconfig(ConfigEnum.admin_lang)) - return dto \ No newline at end of file + dto.lang = Lang(hconfig(ConfigEnum.admin_lang)) + return dto diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py index c549a1e1e..02dcfe661 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py @@ -1,18 +1,16 @@ -from flask import jsonify, request -from flask_restful import Resource -# from flask_simplelogin import login_required -from hiddifypanel.models import * -from hiddifypanel.panel import hiddify - -from flask.views import MethodView +from apiflask.fields import Integer, String, UUID, Boolean, Enum from flask import current_app as app +from flask.views import MethodView +from apiflask import Schema from apiflask import abort + from hiddifypanel.models import * -from apiflask import Schema -from apiflask.fields import Integer, String, UUID, Boolean, Enum -from hiddifypanel.models import AdminMode,Lang +from hiddifypanel.models import * +from hiddifypanel.panel import hiddify +from hiddifypanel.models import AdminMode, Lang +from hiddifypanel.panel.authentication import api_auth class AdminSchema(Schema): @@ -25,12 +23,11 @@ class AdminSchema(Schema): # validate=OneOf([p.uuid for p in AdminUser.query.all()]) ) telegram_id = Integer(required=True, description='The Telegram ID associated with the admin') - lang = Enum(Lang,required=True) - + lang = Enum(Lang, required=True) class AdminUserApi(MethodView): - decorators = [hiddify.super_admin] + decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] @app.output(AdminSchema) def get(self, uuid): @@ -48,4 +45,3 @@ def delete(self, uuid): admin = get_admin_user_db(uuid) or abort(404, "admin not found") admin.remove() return {'status': 200, 'msg': 'ok'} - diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py index 2efe21e8d..77c551077 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py @@ -4,9 +4,11 @@ from .admin_user_api import AdminSchema from hiddifypanel.models import AdminUser from hiddifypanel.panel import hiddify +from hiddifypanel.panel.authentication import api_auth + class AdminUsersApi(MethodView): - decorators = [hiddify.super_admin] + decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] @app.output(AdminSchema(many=True)) def get(self): @@ -20,4 +22,4 @@ def put(self, data): # uuid = data.get('uuid') or abort(422, "Parameter issue: 'uuid'") dbuser = hiddify.add_or_update_admin(**data) - return dbuser.to_dict() \ No newline at end of file + return dbuser.to_dict() diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py index f1923965a..ea1f6c451 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py @@ -5,14 +5,16 @@ from apiflask import Schema from hiddifypanel.panel import hiddify from hiddifypanel.models import get_daily_usage_stats +from hiddifypanel.panel.authentication import api_auth + class ServerStatus(Schema): - stats = Dict(required=True,description="System stats") - usage_history = Dict(required=True,description="System usage history") + stats = Dict(required=True, description="System stats") + usage_history = Dict(required=True, description="System usage history") class AdminServerStatusApi(MethodView): - decorators = [hiddify.super_admin] + decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] @app.output(ServerStatus) def get(self): @@ -23,4 +25,4 @@ def get(self): } admin_id = request.args.get("admin_id") or g.admin.id dto.usage_history = get_daily_usage_stats(admin_id) - return dto \ No newline at end of file + return dto diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py index 911d2504c..1206de87a 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py @@ -1,15 +1,15 @@ -from flask import jsonify, request -# from flask_simplelogin import login_required +from flask import request +from flask.views import MethodView +from flask import current_app as app +from apiflask import abort, Schema + + from hiddifypanel.models import * from hiddifypanel.panel import hiddify from hiddifypanel.drivers import user_driver from hiddifypanel.panel import hiddify - -from flask.views import MethodView - -from flask import current_app as app -from apiflask import abort,Schema -from apiflask.fields import UUID,String,Float,Enum,Date,Time,Integer +from hiddifypanel.panel.authentication import api_auth +from apiflask.fields import UUID, String, Float, Enum, Date, Time, Integer class UserSchema(Schema): @@ -67,13 +67,12 @@ class UserSchema(Schema): ) - class UserApi(MethodView): - decorators = [hiddify.super_admin] + decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] @app.output(UserSchema) def get(self, uuid): - user = user_by_uuid(uuid) or abort(404, "user not found") + user = get_user_by_uuid(uuid) or abort(404, "user not found") return user.to_dict(False) @app.input(UserSchema, arg_name="data") @@ -81,13 +80,13 @@ def patch(self, uuid, data): data = request.json uuid = data.get('uuid') or abort(422, "Parameter issue: 'uuid'") hiddify.add_or_update_user(**data) - user = user_by_uuid(uuid) or abort(502, "unknown issue! user is not added") + user = get_user_by_uuid(uuid) or abort(502, "unknown issue! user is not added") user_driver.add_client(user) hiddify.quick_apply_users() return {'status': 200, 'msg': 'ok'} def delete(self, uuid): - user = user_by_uuid(uuid) or abort(404, "user not found") + user = get_user_by_uuid(uuid) or abort(404, "user not found") user.remove() hiddify.quick_apply_users() return {'status': 200, 'msg': 'ok'} diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py index 9f8c2a4e4..76e8ddbdc 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py @@ -1,14 +1,16 @@ from flask.views import MethodView from flask import current_app as app from apiflask import abort -from hiddifypanel.models.user import user_by_uuid +from hiddifypanel.models.user import get_user_by_uuid from hiddifypanel.panel import hiddify from hiddifypanel.drivers import user_driver from hiddifypanel.models import User +from hiddifypanel.panel.authentication import api_auth from .user_api import UserSchema + class UsersApi(MethodView): - decorators = [hiddify.super_admin] + decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] @app.output(UserSchema(many=True)) def get(self): @@ -19,7 +21,7 @@ def get(self): @app.output(UserSchema) def put(self, data): hiddify.add_or_update_user(**data) - user = user_by_uuid(data['uuid']) or abort(502, "unknown issue! user is not added") + user = get_user_by_uuid(data['uuid']) or abort(502, "unknown issue! user is not added") user_driver.add_client(user) hiddify.quick_apply_users() - return user.to_dict(False) \ No newline at end of file + return user.to_dict(False) From 878043548bd07eaaa079b529f02a966f13e7c84e Mon Sep 17 00:00:00 2001 From: Sarina Date: Sun, 10 Dec 2023 22:12:08 +0330 Subject: [PATCH 004/112] add: get_user_roles to authentication.py --- hiddifypanel/panel/authentication.py | 45 ++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index b0f4163c1..9f5d3ea66 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -1,23 +1,42 @@ from apiflask import HTTPBasicAuth, HTTPTokenAuth -from hiddifypanel.models import AdminUser, User -api_auth = HTTPTokenAuth("ApiKey") +from hiddifypanel.models.user import User, get_user_by_username, get_user_by_uuid +from hiddifypanel.models.admin import AdminUser, get_admin_by_username, get_admin_by_uuid + basic_auth = HTTPBasicAuth() +api_auth = HTTPTokenAuth("ApiKey") -@api_auth.verify_token -def verify_api_auth_token(token): - # for now, token is the same as uuid - user = User.query.filter(User.uuid == token).first() - if user: - return user - else: - return AdminUser.query.filter(AdminUser.uuid == token).first() +@api_auth.get_user_roles +@basic_auth.get_user_roles +def get_user_role(user) -> str | None: + '''Returns user/admin role + Allowed roles are: + - for user: + - user + - for admin: + - super_admin + - admin + - agent + ''' + if isinstance(user, User): + return 'user' + elif isinstance(user, AdminUser): + return str(user.mode) @basic_auth.verify_password -def verify_basic_auth_password(username, password): +def verify_basic_auth_password(username, password) -> str | None: user = User.query.filter(User.username == username, User.password == password).first() if user: return user - else: - return AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() + return AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() + + +@api_auth.verify_token +def verify_api_auth_token(token) -> User | AdminUser | None: + # for now, token is the same as uuid + user = get_user_by_uuid(token) + return user if user else get_admin_by_uuid(token) + + +# ADD ERROR HANDLING TO AUTHENTICATIONS From f86d9e319442e6ced440a21ee28545bf16ef8965 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sun, 10 Dec 2023 22:16:13 +0330 Subject: [PATCH 005/112] refactor: user apis --- .../panel/commercial/restapi/v2/user/apps_api.py | 3 +-- .../panel/commercial/restapi/v2/user/configs_api.py | 3 +-- .../panel/commercial/restapi/v2/user/info_api.py | 10 ++++------ .../panel/commercial/restapi/v2/user/mtproxies.py | 3 +-- .../panel/commercial/restapi/v2/user/short_api.py | 3 +-- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py index 2c6e1428e..3e1f629f3 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py @@ -67,7 +67,7 @@ class AppInSchema(Schema): class AppAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [app.auth_required(api_auth, roles=['user'])] def __init__(self) -> None: super().__init__() @@ -89,7 +89,6 @@ def __init__(self) -> None: @app.input(AppInSchema, arg_name='data', location="query") @app.output(AppSchema(many=True)) - @app.auth_required(api_auth) def get(self, data): # parse user agent if data['platform'] == Platform.auto: diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py index e44e2d57b..2e5a560cb 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py @@ -23,10 +23,9 @@ class ConfigSchema(Schema): class AllConfigsAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [app.auth_required(api_auth, roles=['user'])] @app.output(ConfigSchema(many=True)) - @app.auth_required(api_auth) def get(self): def create_item(name, domain, type, protocol, transport, security, link): dto = ConfigSchema() diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py index 148d4b093..a46627188 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py @@ -6,7 +6,7 @@ from flask import current_app as app from hiddifypanel.models import Lang -from hiddifypanel.models.user import days_to_reset, user_by_uuid +from hiddifypanel.models.user import days_to_reset, get_user_by_uuid from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum from hiddifypanel.panel import hiddify @@ -38,10 +38,9 @@ class UserInfoChangableSchema(Schema): class InfoAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [app.auth_required(api_auth, roles=['user'])] @app.output(ProfileSchema) - @app.auth_required(api_auth) def get(self): c = get_common_data(g.user_uuid, 'new') @@ -64,7 +63,6 @@ def get(self): return dto @app.input(UserInfoChangableSchema, arg_name='data') - @app.auth_required(api_auth) def patch(self, data): if data['telegram_id']: try: @@ -72,13 +70,13 @@ def patch(self, data): except: return {'message': 'The telegram id field is invalid'} - user = user_by_uuid(g.user_uuid) + user = get_user_by_uuid(g.user_uuid) if user.telegram_id != tg_id: user.telegram_id = tg_id db.session.commit() if data['language']: - user = user_by_uuid(g.user_uuid) + user = get_user_by_uuid(g.user_uuid) if user.lang != data['language']: user.lang = data['language'] db.session.commit() diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py index 32bb6a64e..32341b97f 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py @@ -19,10 +19,9 @@ class MtproxySchema(Schema): class MTProxiesAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [app.auth_required(api_auth, roles=['user'])] @app.output(MtproxySchema(many=True)) - @app.auth_required(api_auth) def get(self): # check mtproxie is enable if not hconfig(ConfigEnum.telegram_enable, g.user_uuid): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py index e26c0d0dc..7acb3a62f 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py @@ -19,10 +19,9 @@ class ShortSchema(Schema): class ShortAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [app.auth_required(api_auth, roles=['user'])] @app.output(ShortSchema) - @app.auth_required(api_auth) def get(self): short, expire_in = hiddify.add_short_link("/"+hconfig(ConfigEnum.proxy_path)+"/"+str(g.user_uuid)+"/") full_url = f"https://{urlparse(request.base_url).hostname}/{short}" From 5a9c06ff546ec16f8ec95eb8bfa0eed03d22f2fe Mon Sep 17 00:00:00 2001 From: Sarina Date: Sun, 10 Dec 2023 22:17:10 +0330 Subject: [PATCH 006/112] fix: rename --- hiddifypanel/panel/commercial/restapi/v1/resources.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hiddifypanel/panel/commercial/restapi/v1/resources.py b/hiddifypanel/panel/commercial/restapi/v1/resources.py index e2f7bdef9..993d6a681 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/resources.py +++ b/hiddifypanel/panel/commercial/restapi/v1/resources.py @@ -1,4 +1,4 @@ -from flask import jsonify, request +from flask import jsonify, request from apiflask import abort from flask_restful import Resource # from flask_simplelogin import login_required @@ -20,7 +20,7 @@ class UserResource(Resource): def get(self): uuid = request.args.get('uuid') if uuid: - user = user_by_uuid(uuid) or abort(404, "user not found") + user = get_user_by_uuid(uuid) or abort(404, "user not found") return jsonify(user.to_dict()) users = User.query.all() or abort(502, "WTF!") @@ -30,14 +30,14 @@ def post(self): data = request.json uuid = data.get('uuid') or abort(422, "Parameter issue: 'uuid'") hiddify.add_or_update_user(**data) - user = user_by_uuid(uuid) or abort(502, "unknown issue! user is not added") + user = get_user_by_uuid(uuid) or abort(502, "unknown issue! user is not added") user_driver.add_client(user) hiddify.quick_apply_users() return jsonify({'status': 200, 'msg': 'ok'}) def delete(self): uuid = request.args.get('uuid') or abort(422, "Parameter issue: 'uuid'") - user = user_by_uuid(uuid) or abort(404, "user not found") + user = get_user_by_uuid(uuid) or abort(404, "user not found") user.remove() hiddify.quick_apply_users() return jsonify({'status': 200, 'msg': 'ok'}) From 10b81d729634293df9ddbc2f06d69c44f385bdce Mon Sep 17 00:00:00 2001 From: Sarina Date: Sun, 10 Dec 2023 22:33:47 +0330 Subject: [PATCH 007/112] add: auth to admin panel endpoints --- hiddifypanel/models/__init__.py | 6 +- hiddifypanel/models/admin.py | 6 +- hiddifypanel/models/user.py | 8 ++- hiddifypanel/panel/admin/Actions.py | 62 ++++++++++--------- hiddifypanel/panel/admin/AdminstratorAdmin.py | 16 +---- hiddifypanel/panel/admin/Backup.py | 30 ++++----- hiddifypanel/panel/admin/ChildAdmin.py | 16 +---- hiddifypanel/panel/admin/Dashboard.py | 9 +-- hiddifypanel/panel/admin/ProxyAdmin.py | 18 ++---- hiddifypanel/panel/admin/SettingAdmin.py | 41 +++++++----- hiddifypanel/panel/admin/__init__.py | 44 ++++++------- hiddifypanel/panel/admin/adminlte.py | 5 +- hiddifypanel/panel/admin/commercial_info.py | 18 +----- 13 files changed, 126 insertions(+), 153 deletions(-) diff --git a/hiddifypanel/models/__init__.py b/hiddifypanel/models/__init__.py index 6d280546f..edbae50fa 100644 --- a/hiddifypanel/models/__init__.py +++ b/hiddifypanel/models/__init__.py @@ -1,10 +1,10 @@ -from .config_enum import ConfigCategory, ConfigEnum,Lang +from .config_enum import ConfigCategory, ConfigEnum, Lang from .config import StrConfig, BoolConfig, get_hconfigs, hconfig, set_hconfig, add_or_update_config, bulk_register_configs from .parent_domain import ParentDomain, add_or_update_parent_domains, bulk_register_parent_domains from .domain import Domain, DomainType, ShowDomain, get_domain, get_current_proxy_domains, get_panel_domains, get_proxy_domains, get_proxy_domains_db, get_hdomains, hdomain, add_or_update_domain, bulk_register_domains from .proxy import Proxy, ProxyL3, ProxyCDN, ProxyProto, ProxyTransport, add_or_update_proxy, bulk_register_proxies -from .user import User, UserMode, is_user_active, remaining_days, days_to_reset, user_by_id, user_by_uuid, add_or_update_user,remove_user, user_should_reset, add_or_update_user, bulk_register_users, UserDetail, ONE_GIG -from .admin import AdminUser, AdminMode, get_super_admin_secret, get_admin_user_db, get_admin_by_uuid, add_or_update_admin, bulk_register_admins, current_admin_or_owner, get_super_admin +from .user import User, UserMode, is_user_active, remaining_days, days_to_reset, user_by_id, get_user_by_uuid, add_or_update_user, remove_user, user_should_reset, add_or_update_user, bulk_register_users, UserDetail, ONE_GIG, get_user_by_username +from .admin import AdminUser, AdminMode, get_super_admin_secret, get_admin_user_db, get_admin_by_uuid, add_or_update_admin, bulk_register_admins, current_admin_or_owner, get_super_admin, get_admin_by_username from .child import Child from .usage import DailyUsage, get_daily_usage_stats diff --git a/hiddifypanel/models/admin.py b/hiddifypanel/models/admin.py index 5855fd866..e2ed809b0 100644 --- a/hiddifypanel/models/admin.py +++ b/hiddifypanel/models/admin.py @@ -152,7 +152,7 @@ def add_or_update_admin(commit=True, **admin): return dbuser -def get_admin_by_uuid(uuid, create=False): +def get_admin_by_uuid(uuid, create=False) -> AdminUser | None: admin = AdminUser.query.filter(AdminUser.uuid == uuid).first() # print(admin) if create and not admin: @@ -166,6 +166,10 @@ def get_admin_by_uuid(uuid, create=False): return admin +def get_admin_by_username(username) -> AdminUser | None: + return AdminUser.query.filter(AdminUser.username == username).first() + + def bulk_register_admins(users=[], commit=True): for u in users: add_or_update_admin(commit=False, **u) diff --git a/hiddifypanel/models/user.py b/hiddifypanel/models/user.py index b918a1d31..a20fdb0a0 100644 --- a/hiddifypanel/models/user.py +++ b/hiddifypanel/models/user.py @@ -272,13 +272,17 @@ def user_should_reset(user): return res -def user_by_uuid(uuid): +def get_user_by_uuid(uuid) -> User | None: return User.query.filter(User.uuid == uuid).first() -def user_by_id(id): +def user_by_id(id) -> User | None: return User.query.filter(User.id == id).first() + +def get_user_by_username(username) -> User | None: + return User.query.filter(User.username == username).first() + # aliz dev diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index 351b55125..636a056fe 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -1,36 +1,33 @@ #!/usr/bin/env python3 -import pprint from flask_babelex import gettext as _ -import pathlib -from datetime import datetime, timedelta, date -from typing import Optional import os -import sys -import json import urllib.request -import subprocess -import re from flask_classful import FlaskView, route -from flask import current_app, render_template, request, Response, Markup, url_for, make_response, redirect +from flask import render_template, request, Markup, url_for, make_response, redirect +from flask import current_app as app from hiddifypanel import hutils from hiddifypanel.models import * from hiddifypanel.panel import hiddify, usage -from hiddifypanel.panel.run_commander import commander,Command +from hiddifypanel.panel.run_commander import commander, Command from hiddifypanel.panel.hiddify import flash +from hiddifypanel.panel.authentication import basic_auth + class Actions(FlaskView): - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def index(self): return render_template('index.html') - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def reverselog(self, logfile): if logfile == None: return self.viewlogs() - config_dir = current_app.config['HIDDIFY_CONFIG_PATH'] + config_dir = app.config['HIDDIFY_CONFIG_PATH'] with open(f'{config_dir}/log/system/{logfile}') as f: lines = [line for line in f] @@ -51,23 +48,27 @@ def reverselog(self, logfile): response.headers['Access-Control-Allow-Headers'] = 'Content-Type' return response - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def viewlogs(self): - config_dir = current_app.config['HIDDIFY_CONFIG_PATH'] + config_dir = app.config['HIDDIFY_CONFIG_PATH'] res = [] for filename in sorted(os.listdir(f'{config_dir}/log/system/')): res.append(f"{filename}") return Markup("
".join(res)) @route('apply_configs', methods=['POST']) + @app.auth_required(basic_auth, roles=['super_admin']) def apply_configs(self): return self.reinstall(False) @route('reset', methods=['POST']) + @app.auth_required(basic_auth, roles=['super_admin']) def reset(self): return self.reset2() - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def reset2(self): status = self.status() # flash(_("rebooting system may takes time please wait"),'info') @@ -84,7 +85,7 @@ def reset2(self): # file = "restart.sh" # config = current_app.config # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/{file} --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) - + # run restart.sh commander(Command.restart_services) @@ -93,10 +94,12 @@ def reset2(self): return resp @route('reinstall', methods=['POST']) + @app.auth_required(basic_auth, roles=['super_admin']) def reinstall(self, complete_install=True, domain_changed=False): return self.reinstall2(complete_install, domain_changed) - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def reinstall2(self, complete_install=True, domain_changed=False): if int(hconfig(ConfigEnum.db_version)) < 9: return ("Please update your panel before this action.") @@ -142,17 +145,17 @@ def reinstall2(self, complete_install=True, domain_changed=False): domains=get_domains(), ) - - #subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/{file} --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) + # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/{file} --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) # run install.sh or apply_configs.sh commander(Command.install if complete_install else Command.apply) - + import time time.sleep(1) return resp - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def change_reality_keys(self): key = hiddify.generate_x25519_keys() set_hconfig(ConfigEnum.reality_private_key, key['private_key']) @@ -160,7 +163,8 @@ def change_reality_keys(self): hiddify.flash_config_success(restart_mode='apply', domain_changed=False) return redirect(url_for('admin.SettingAdmin:index')) - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def status(self): # hiddify.add_temporary_access() # configs=read_configs() @@ -168,8 +172,8 @@ def status(self): # os.system(f'cd {config_dir};{env} ./install.sh &') # rc = subprocess.call(f"cd {config_dir};./{file} & disown",shell=True) - #subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/status.sh --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) - + # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/status.sh --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) + # run status.sh commander(Command.status) @@ -187,10 +191,11 @@ def status(self): # @hiddify.super_admin @route('update', methods=['POST']) + @app.auth_required(basic_auth, roles=['super_admin']) def update(self): return self.update2() - @hiddify.super_admin + # @hiddify.super_admin def update2(self): hiddify.add_temporary_access() @@ -199,7 +204,7 @@ def update2(self): # rc = subprocess.call(f"cd {config_dir};./update.sh & disown",shell=True) # os.system(f'cd {config_dir};./update.sh &') - #subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/update.sh --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) + # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/update.sh --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) # run update.sh commander(Command.update) @@ -247,7 +252,8 @@ def get_some_random_reality_friendly_domain(self): return res+"" - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def update_usage(self): import json diff --git a/hiddifypanel/panel/admin/AdminstratorAdmin.py b/hiddifypanel/panel/admin/AdminstratorAdmin.py index 99ac1cc5e..e4108322b 100644 --- a/hiddifypanel/panel/admin/AdminstratorAdmin.py +++ b/hiddifypanel/panel/admin/AdminstratorAdmin.py @@ -1,25 +1,13 @@ -from flask_admin.contrib import sqla - -from hiddifypanel.panel.database import db from wtforms.validators import Regexp from hiddifypanel.models import * from wtforms.validators import Regexp, ValidationError from .adminlte import AdminLTEModelView from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ -from hiddifypanel.panel import hiddify, cf_api +from hiddifypanel.panel import hiddify from flask import Markup -from flask import Flask, g, url_for -from flask_sqlalchemy import SQLAlchemy -from flask_admin import Admin -from flask_admin.contrib.sqla import ModelView -from wtforms import SelectMultipleField +from flask import g import datetime - -from wtforms.widgets import ListWidget, CheckboxInput -from sqlalchemy.orm import backref -# Define a custom field type for the related domains -from flask_admin.form.fields import Select2TagsField, Select2Field from wtforms import SelectField diff --git a/hiddifypanel/panel/admin/Backup.py b/hiddifypanel/panel/admin/Backup.py index 940d705f8..b9920bea7 100644 --- a/hiddifypanel/panel/admin/Backup.py +++ b/hiddifypanel/panel/admin/Backup.py @@ -1,40 +1,33 @@ #!/usr/bin/env python3 from urllib.parse import urlparse - -from hiddifypanel.panel.database import db -import uuid from flask_babelex import gettext as _ from flask_bootstrap import SwitchField # from flask_babelex import gettext as _ import wtforms as wtf from flask_wtf import FlaskForm -import pathlib -from hiddifypanel.models import * -from hiddifypanel.panel import hiddify -from datetime import datetime, timedelta, date -import os -import sys +from datetime import datetime import json -import urllib.request -import subprocess -import re -from hiddifypanel.panel import hiddify -from flask import current_app, render_template, request, Response, Markup, url_for, jsonify, redirect, g +import json +from flask import render_template, request, jsonify, redirect, g +from flask import current_app as app from hiddifypanel.panel.hiddify import flash from flask_wtf.file import FileField, FileRequired -import json - from flask_classful import FlaskView, route +from hiddifypanel.panel.database import db +from hiddifypanel.panel import hiddify +from hiddifypanel.models import * +from hiddifypanel.panel import hiddify +from hiddifypanel.panel.authentication import basic_auth + class Backup(FlaskView): + decorators = [app.auth_required(basic_auth, roles=['super_admin'])] - @hiddify.super_admin def index(self): return render_template('backup.html', restore_form=get_restore_form()) # @route("/backupfile") - @hiddify.super_admin def backupfile(self): response = jsonify( hiddify.dump_db_to_dict() @@ -45,7 +38,6 @@ def backupfile(self): return response - @hiddify.super_admin def post(self): restore_form = get_restore_form() diff --git a/hiddifypanel/panel/admin/ChildAdmin.py b/hiddifypanel/panel/admin/ChildAdmin.py index 5adefcd27..b1db91516 100644 --- a/hiddifypanel/panel/admin/ChildAdmin.py +++ b/hiddifypanel/panel/admin/ChildAdmin.py @@ -1,6 +1,4 @@ -from flask_admin.contrib import sqla from hiddifypanel import hutils -from hiddifypanel.panel.database import db from wtforms.validators import Regexp from hiddifypanel.models import * from wtforms.validators import Regexp, ValidationError @@ -9,17 +7,6 @@ from flask_babelex import lazy_gettext as _ from hiddifypanel.panel import hiddify from flask import Markup -from flask import Flask -from flask_sqlalchemy import SQLAlchemy -from flask_admin import Admin -from flask_admin.contrib.sqla import ModelView -from wtforms import SelectMultipleField - - -from wtforms.widgets import ListWidget, CheckboxInput -from sqlalchemy.orm import backref -# Define a custom field type for the related domains -from flask_admin.form.fields import Select2TagsField, Select2Field class ChildAdmin(AdminLTEModelView): @@ -62,7 +49,8 @@ class ChildAdmin(AdminLTEModelView): def _domain_admin_link(view, context, model, name): admin_link = f'https://{model.domain}{hiddify.get_admin_path()}' - return Markup(f'') + return Markup(f'') def _domain_ip(view, context, model, name): dip = hutils.ip.get_domain_ip(model.domain) diff --git a/hiddifypanel/panel/admin/Dashboard.py b/hiddifypanel/panel/admin/Dashboard.py index ee005ff15..c1e6998c2 100644 --- a/hiddifypanel/panel/admin/Dashboard.py +++ b/hiddifypanel/panel/admin/Dashboard.py @@ -1,8 +1,8 @@ import datetime -import os -import re + from flask import render_template, url_for, request, jsonify, g, redirect +from flask import current_app as app from apiflask import abort from flask_babelex import lazy_gettext as _ from flask_classful import FlaskView, route @@ -12,6 +12,7 @@ from hiddifypanel.panel import hiddify from hiddifypanel.panel.database import db from hiddifypanel.panel.hiddify import flash +from hiddifypanel.panel.authentication import basic_auth class Dashboard(FlaskView): @@ -25,8 +26,9 @@ def get_data(self): usage_history=get_daily_usage_stats(admin_id) )) + @app.auth_required(basic_auth, roles=['super_admin', 'admin']) def index(self): - + if hconfig(ConfigEnum.first_setup): return redirect(url_for("admin.QuickSetup:index")) if hiddifypanel.__release_date__ + datetime.timedelta(days=20) < datetime.datetime.now(): @@ -90,4 +92,3 @@ def remove_child(self): db.session.commit() flash(_("child has been removed!"), "success") return self.index() - diff --git a/hiddifypanel/panel/admin/ProxyAdmin.py b/hiddifypanel/panel/admin/ProxyAdmin.py index 902247064..ad949c696 100644 --- a/hiddifypanel/panel/admin/ProxyAdmin.py +++ b/hiddifypanel/panel/admin/ProxyAdmin.py @@ -1,34 +1,28 @@ -from hiddifypanel.models import BoolConfig, StrConfig, ConfigEnum, hconfig, Proxy -# from flask_babelex import lazy_gettext as __ from flask_babelex import gettext as _ import wtforms as wtf from flask_wtf import FlaskForm from flask_bootstrap import SwitchField - -from flask_admin.base import expose -# from gettext import gettext as _ -from hiddifypanel.panel import hiddify +from flask import render_template +from flask import current_app as app -import re -from flask import render_template, current_app, Markup, url_for -from apiflask import abort +from hiddifypanel.models import ConfigEnum, get_hconfigs, BoolConfig, ConfigEnum, hconfig, Proxy from hiddifypanel.panel.hiddify import flash -from hiddifypanel.models import User, Domain, DomainType, StrConfig, ConfigEnum, get_hconfigs from hiddifypanel.panel.database import db from wtforms.fields import * from hiddifypanel.panel import hiddify from flask_classful import FlaskView from hiddifypanel.panel.hiddify import flash +from hiddifypanel.panel import hiddify +from hiddifypanel.panel.authentication import basic_auth class ProxyAdmin(FlaskView): + decorators = [app.auth_required(basic_auth, roles=['super_admin', 'admin'])] - @hiddify.admin def index(self): return render_template('proxy.html', global_config_form=get_global_config_form(), detailed_config_form=get_all_proxy_form()) - @hiddify.admin def post(self): global_config_form = get_global_config_form() all_proxy_form = get_all_proxy_form() diff --git a/hiddifypanel/panel/admin/SettingAdmin.py b/hiddifypanel/panel/admin/SettingAdmin.py index 2ff715223..7b6acbb8e 100644 --- a/hiddifypanel/panel/admin/SettingAdmin.py +++ b/hiddifypanel/panel/admin/SettingAdmin.py @@ -1,36 +1,38 @@ import flask_babel import flask_babelex -from hiddifypanel.models import BoolConfig, StrConfig, ConfigEnum, hconfig, ConfigCategory -from wtforms.validators import Regexp, ValidationError from flask_babelex import lazy_gettext as _ # from flask_babelex import gettext as _ +from flask import render_template, Markup, url_for, g +from flask import current_app as app import wtforms as wtf -from flask_wtf import FlaskForm, CSRFProtect from flask_bootstrap import SwitchField -from flask_admin.base import expose # from gettext import gettext as _ -from hiddifypanel.panel.hiddify import get_random_domains +from flask_classful import FlaskView +from wtforms.fields import * +from flask_wtf import FlaskForm import re -from flask import render_template, current_app, Markup, url_for, g -from apiflask import abort -from hiddifypanel.panel.hiddify import flash + +from hiddifypanel.models import BoolConfig, StrConfig, ConfigEnum, hconfig, ConfigCategory from hiddifypanel.models import * from hiddifypanel.panel.database import db -from wtforms.fields import * -from flask_classful import FlaskView +from hiddifypanel.panel.hiddify import flash +from hiddifypanel.panel.hiddify import get_random_domains from hiddifypanel.panel import hiddify, custom_widgets +from hiddifypanel.panel.authentication import basic_auth class SettingAdmin(FlaskView): - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def index(self): form = get_config_form() return render_template('config.html', form=form) - @hiddify.super_admin + # @hiddify.super_admin + @app.auth_required(basic_auth, roles=['super_admin']) def post(self): set_hconfig(ConfigEnum.first_setup, False) form = get_config_form() @@ -188,8 +190,19 @@ class CategoryForm(FlaskForm): field = wtf.fields.SelectField(_(f"config.{c.key}.label"), choices=[("release", _("Release")), ("beta", _("Beta")), ("develop", _("Develop"))], description=_(f"config.{c.key}.description"), default=hconfig(c.key)) elif c.key == ConfigEnum.utls: - field = wtf.fields.SelectField(_(f"config.{c.key}.label"), choices=[("none", "None"), ("chrome", "Chrome"), ("edge", "Edge"), ("ios", "iOS"), ("android", "Android"), ( - "safari", "Safari"), ("firefox", "Firefox"), ('random', 'random'), ('randomized', 'randomized')], description=_(f"config.{c.key}.description"), default=hconfig(c.key)) + field = wtf.fields.SelectField( + _(f"config.{c.key}.label"), + choices=[("none", "None"), + ("chrome", "Chrome"), + ("edge", "Edge"), + ("ios", "iOS"), + ("android", "Android"), + ("safari", "Safari"), + ("firefox", "Firefox"), + ('random', 'random'), + ('randomized', 'randomized')], + description=_(f"config.{c.key}.description"), + default=hconfig(c.key)) elif c.key == ConfigEnum.telegram_lib: # if hconfig(ConfigEnum.telegram_lib)=='python': # continue6 diff --git a/hiddifypanel/panel/admin/__init__.py b/hiddifypanel/panel/admin/__init__.py index ab8671534..6e78f1b06 100644 --- a/hiddifypanel/panel/admin/__init__.py +++ b/hiddifypanel/panel/admin/__init__.py @@ -1,31 +1,18 @@ -from flask import Blueprint, url_for, request, jsonify, g, redirect +from flask import request, redirect from flask_admin import Admin from hiddifypanel import Events -# from flask_sockets import Sockets - -from .SettingAdmin import SettingAdmin from .DomainAdmin import DomainAdmin -from .ConfigAdmin import ConfigAdmin -from .commercial_info import CommercialInfo -from .ProxyAdmin import ProxyAdmin from .AdminstratorAdmin import AdminstratorAdmin -from .Actions import Actions -from .Backup import Backup -from .QuickSetup import QuickSetup -from hiddifypanel.models import StrConfig, ConfigEnum -# from .resources import ProductItemResource, ProductResource + from hiddifypanel.panel.database import db from hiddifypanel.models import * -from .Dashboard import Dashboard - -from flask_admin.menu import MenuLink from apiflask import APIBlueprint -import uuid from flask_adminlte3 import AdminLTE3 + + flask_bp = APIBlueprint("flask", __name__, url_prefix=f"///", template_folder="templates", enable_openapi=False) admin_bp = APIBlueprint("admin", __name__, url_prefix=f"///admin/", template_folder="templates", enable_openapi=False) flaskadmin = Admin(endpoint="admin", base_template='flaskadmin-layout.html') -# from extensions import socketio def init_app(app): @@ -53,14 +40,21 @@ def auto_route(): flaskadmin.add_view(DomainAdmin(Domain, db.session)) flaskadmin.add_view(AdminstratorAdmin(AdminUser, db.session)) - SettingAdmin.register(admin_bp) - ProxyAdmin.register(admin_bp) - Actions.register(admin_bp) - CommercialInfo.register(admin_bp) - - QuickSetup.register(admin_bp) - Backup.register(admin_bp) - Dashboard.register(admin_bp, route_base="/") + with app.app_context(): + from .Dashboard import Dashboard + from .SettingAdmin import SettingAdmin + from .commercial_info import CommercialInfo + from .ProxyAdmin import ProxyAdmin + from .Actions import Actions + from .Backup import Backup + from .QuickSetup import QuickSetup + Dashboard.register(admin_bp, route_base="/") + SettingAdmin.register(admin_bp) + ProxyAdmin.register(admin_bp) + Actions.register(admin_bp) + CommercialInfo.register(admin_bp) + QuickSetup.register(admin_bp) + Backup.register(admin_bp) # admin_bp.add_url_rule('/admin/quicksetup/',endpoint="quicksetup",view_func=QuickSetup.index,methods=["GET"]) # admin_bp.add_url_rule('/admin/quicksetup/',endpoint="quicksetup-save", view_func=QuickSetup.save,methods=["POST"]) diff --git a/hiddifypanel/panel/admin/adminlte.py b/hiddifypanel/panel/admin/adminlte.py index 026065dbf..90c724291 100644 --- a/hiddifypanel/panel/admin/adminlte.py +++ b/hiddifypanel/panel/admin/adminlte.py @@ -1,5 +1,6 @@ from flask_admin.contrib.sqla import ModelView -from flask_admin.form import SecureForm + + class AdminLTEModelView(ModelView): list_template = 'hiddify-flask-admin/list.html' create_template = 'flask-admin/model/create.html' @@ -10,4 +11,4 @@ class AdminLTEModelView(ModelView): edit_modal_template = 'flask-admin/model/modals/edit.html' details_modal_template = 'flask-admin/model/modals/details.html' - # form_base_class = SecureForm \ No newline at end of file + # form_base_class = SecureForm diff --git a/hiddifypanel/panel/admin/commercial_info.py b/hiddifypanel/panel/admin/commercial_info.py index 6a5d22906..8f734f002 100644 --- a/hiddifypanel/panel/admin/commercial_info.py +++ b/hiddifypanel/panel/admin/commercial_info.py @@ -1,21 +1,9 @@ #!/usr/bin/env python3 -import pprint from flask_babelex import gettext as _ -import pathlib -from hiddifypanel.models import * - -from datetime import datetime, timedelta, date -import os -import sys -import json -import urllib.request -import subprocess -import re -from hiddifypanel.panel import hiddify, usage -from flask import current_app, render_template, request, Response, Markup, url_for -from hiddifypanel.panel.hiddify import flash +from flask import render_template +from flask_classful import FlaskView -from flask_classful import FlaskView, route +from hiddifypanel.models import * class CommercialInfo(FlaskView): From 1587cf121a0b5398334420e4fe6b59e741a016fb Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 11 Dec 2023 11:51:33 +0330 Subject: [PATCH 008/112] using non-assci name in building username because we send username and password with base64 encoding, so we're not limited to assci --- hiddifypanel/models/admin.py | 2 +- hiddifypanel/models/user.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hiddifypanel/models/admin.py b/hiddifypanel/models/admin.py index e2ed809b0..6ef70ae01 100644 --- a/hiddifypanel/models/admin.py +++ b/hiddifypanel/models/admin.py @@ -190,7 +190,7 @@ def fill_username(admin: AdminUser) -> None: rand_str = '' # if the username chats isn't only string.ascii_letters, it's invalid # because we can't set non ascii characters in the http header (https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication) - if admin.name and hutils.utils.is_assci_alphanumeric(admin.name): + if admin.name: # user actual name base_username = admin.name.replace(' ', '_') if len(base_username) > minimum_username_length - 1: diff --git a/hiddifypanel/models/user.py b/hiddifypanel/models/user.py index a20fdb0a0..305551b38 100644 --- a/hiddifypanel/models/user.py +++ b/hiddifypanel/models/user.py @@ -367,7 +367,7 @@ def fill_username(user: User) -> None: rand_str = '' # if the username chats isn't only string.ascii_letters, it's invalid # because we can't set non ascii characters in the http header (https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication) - if user.name and hutils.utils.is_assci_alphanumeric(user.name): + if user.name: # user actual name base_username = user.name.replace(' ', '_') if len(base_username) > minimum_username_length - 1: From f14f51bad99c688f21d44bfb534ad7691a2a7997 Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 11 Dec 2023 11:53:56 +0330 Subject: [PATCH 009/112] fix: basic authentication verify password function --- hiddifypanel/panel/authentication.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index 9f5d3ea66..f48bdbc0f 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -26,6 +26,8 @@ def get_user_role(user) -> str | None: @basic_auth.verify_password def verify_basic_auth_password(username, password) -> str | None: + username = username.strip() + password = password.strip() user = User.query.filter(User.username == username, User.password == password).first() if user: return user From 0a177000914d0a95e906ee7e496f1dea95c15d76 Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 11 Dec 2023 12:09:40 +0330 Subject: [PATCH 010/112] add: athentication with session --- hiddifypanel/panel/authentication.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index f48bdbc0f..b8e5e5543 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -1,6 +1,7 @@ from apiflask import HTTPBasicAuth, HTTPTokenAuth -from hiddifypanel.models.user import User, get_user_by_username, get_user_by_uuid -from hiddifypanel.models.admin import AdminUser, get_admin_by_username, get_admin_by_uuid +from hiddifypanel.models.user import User, get_user_by_uuid +from hiddifypanel.models.admin import AdminUser, get_admin_by_uuid +from flask import session basic_auth = HTTPBasicAuth() api_auth = HTTPTokenAuth("ApiKey") @@ -26,12 +27,25 @@ def get_user_role(user) -> str | None: @basic_auth.verify_password def verify_basic_auth_password(username, password) -> str | None: + if session.get('account'): + if session['account']['role'] == 'user': + return get_user_by_uuid(session['account']['uuid']) + else: + return get_admin_by_uuid(session['account']['uuid']) + username = username.strip() password = password.strip() - user = User.query.filter(User.username == username, User.password == password).first() - if user: - return user - return AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() + res = User.query.filter(User.username == username, User.password == password).first() + if not res: + res = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() + + if res: + session['account'] = { + 'uuid': res.uuid, + 'role': get_user_role(res), + # 'username': res.username, + } + return res @api_auth.verify_token From 53f639759fb80d98fd436ee0a0025a017d396cef Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 11 Dec 2023 16:47:38 +0330 Subject: [PATCH 011/112] fix: add authentication(role-based) for ModelView classes --- hiddifypanel/panel/admin/ConfigAdmin.py | 7 +- hiddifypanel/panel/admin/DomainAdmin.py | 6 +- hiddifypanel/panel/admin/UserAdmin.py | 29 ++++---- hiddifypanel/panel/authentication.py | 96 ++++++++++++++++++++----- 4 files changed, 97 insertions(+), 41 deletions(-) diff --git a/hiddifypanel/panel/admin/ConfigAdmin.py b/hiddifypanel/panel/admin/ConfigAdmin.py index e4c09e9d3..be6583b1a 100644 --- a/hiddifypanel/panel/admin/ConfigAdmin.py +++ b/hiddifypanel/panel/admin/ConfigAdmin.py @@ -1,9 +1,9 @@ -from hiddifypanel.models import ConfigEnum, Domain import re import uuid from wtforms.validators import ValidationError +from hiddifypanel.panel.authentication import AccountRole, standalone_verify from hiddifypanel.models import ConfigEnum, Domain from .adminlte import AdminLTEModelView @@ -22,6 +22,9 @@ class ConfigAdmin(AdminLTEModelView): }, } + def is_accessible(self): + return standalone_verify({AccountRole.super_admin, AccountRole.admin}) + @staticmethod def _is_valid_uuid(val: str, version: int | None = None): try: @@ -41,7 +44,7 @@ def on_model_change(self, form, model, is_created): if not self._is_valid_uuid(model.value): raise ValidationError('Invalid UUID e.g.,' + str(uuid.uuid4())) - if model.key in [ConfigEnum.telegram_secret]: + if model.key in [ConfigEnum.telegratelem_secret]: if not re.match("^[0-9a-fA-F]{32}$", model.value): raise ValidationError('Invalid UUID e.g.,' + uuid.uuid4().hex) diff --git a/hiddifypanel/panel/admin/DomainAdmin.py b/hiddifypanel/panel/admin/DomainAdmin.py index 27dccbb58..2d0342dab 100644 --- a/hiddifypanel/panel/admin/DomainAdmin.py +++ b/hiddifypanel/panel/admin/DomainAdmin.py @@ -1,9 +1,9 @@ import ipaddress from hiddifypanel.models import * import re -from hiddifypanel.panel.database import db +from hiddifypanel.panel.authentication import AccountRole, standalone_verify from flask import Markup -from flask import g, flash +from flask import flash from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ from hiddifypanel.panel.run_commander import Command, commander @@ -284,7 +284,7 @@ def after_model_change(self, form, model, is_created): commander(Command.get_cert, domain=model.domain) def is_accessible(self): - return g.admin.mode in [AdminMode.admin, AdminMode.super_admin] + return standalone_verify([AccountRole.super_admin, AccountRole.admin]) # def form_choices(self, field, *args, **kwargs): # if field.type == "Enum": diff --git a/hiddifypanel/panel/admin/UserAdmin.py b/hiddifypanel/panel/admin/UserAdmin.py index eb03cd4f2..60c5c8a9c 100644 --- a/hiddifypanel/panel/admin/UserAdmin.py +++ b/hiddifypanel/panel/admin/UserAdmin.py @@ -1,25 +1,22 @@ -from flask_admin.contrib import sqla -from hiddifypanel.panel.database import db import datetime -from hiddifypanel.models import * -from flask import Markup, g, request, url_for -from apiflask import abort -from wtforms.validators import Regexp, ValidationError -import re import uuid -from hiddifypanel.drivers import user_driver -from .adminlte import AdminLTEModelView -# from gettext import gettext as _ -from flask_babelex import gettext as __ -from flask_babelex import lazy_gettext as _ +import re +from wtforms.validators import Regexp, ValidationError +from flask import Markup, g, request, url_for +from flask_babelex import lazy_gettext as _ from wtforms.validators import NumberRange +from .adminlte import AdminLTEModelView +from flask_babelex import gettext as __ +from apiflask import abort +from hiddifypanel.panel.authentication import standalone_verify, AccountRole from hiddifypanel.panel import hiddify, custom_widgets from hiddifypanel.panel.hiddify import flash -from wtforms.fields import StringField +from hiddifypanel.drivers import user_driver from flask_bootstrap import SwitchField +from hiddifypanel.models import * class UserAdmin(AdminLTEModelView): @@ -208,8 +205,8 @@ def on_model_delete(self, model): user_driver.remove_client(model) # hiddify.flash_config_success() - # def is_accessible(self): - # return g.is_admin + def is_accessible(self): + return standalone_verify({AccountRole.super_admin, AccountRole.admin}) def on_form_prefill(self, form, id=None): # print("================",form._obj.start_date) @@ -273,8 +270,6 @@ def on_model_change(self, form, model, is_created): # else: # xray_api.remove_client(model.uuid) # hiddify.flash_config_success() - # def is_accessible(self): - # return is_valid() def after_model_change(self, form, model, is_created): if hconfig(ConfigEnum.first_setup): diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index b8e5e5543..1bd6a34bc 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -1,15 +1,48 @@ +from typing import Set from apiflask import HTTPBasicAuth, HTTPTokenAuth from hiddifypanel.models.user import User, get_user_by_uuid from hiddifypanel.models.admin import AdminUser, get_admin_by_uuid from flask import session +from enum import auto +from strenum import StrEnum basic_auth = HTTPBasicAuth() api_auth = HTTPTokenAuth("ApiKey") +class AccountRole(StrEnum): + user = 'user' + admin = 'admin' + super_admin = 'super_admin' + agent = 'agent' + + +def set_authentication_in_session(account: User | AdminUser) -> None: + session['account'] = { + 'uuid': account.uuid, + 'role': get_user_role(account), + # 'username': res.username, + } + + +def verify_from_session(roles: Set[AccountRole] = set()) -> User | AdminUser | None: + if session.get('account'): + if roles: + if session['account']['role'] in roles: + if AccountRole.user in roles: + return get_user_by_uuid(session['account']['uuid']) + else: + return get_admin_by_uuid(session['account']['uuid']) + else: + if session['account']['role'] == AccountRole.user: + return get_user_by_uuid(session['account']['uuid']) + else: + return get_admin_by_uuid(session['account']['uuid']) + + @api_auth.get_user_roles @basic_auth.get_user_roles -def get_user_role(user) -> str | None: +def get_user_role(user) -> AccountRole | None: '''Returns user/admin role Allowed roles are: - for user: @@ -20,32 +53,45 @@ def get_user_role(user) -> str | None: - agent ''' if isinstance(user, User): - return 'user' + return AccountRole.user elif isinstance(user, AdminUser): - return str(user.mode) + match user.mode: + case 'super_admin': + return AccountRole.super_admin + case 'admin': + return AccountRole.admin + case 'agent': + return AccountRole.agent @basic_auth.verify_password -def verify_basic_auth_password(username, password) -> str | None: - if session.get('account'): - if session['account']['role'] == 'user': - return get_user_by_uuid(session['account']['uuid']) - else: - return get_admin_by_uuid(session['account']['uuid']) +def verify_basic_auth_password(username, password, check_session=True, roles: Set[AccountRole] = set()) -> AdminUser | User | None: + if check_session: + account = verify_from_session(roles) + if account: + return account username = username.strip() password = password.strip() - res = User.query.filter(User.username == username, User.password == password).first() - if not res: - res = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() + account = None + if roles: + if AccountRole.user in roles: + account = User.query.filter(User.username == username, User.password == password).first() + else: + account = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() + else: + account = User.query.filter(User.username == username, User.password == password).first() + if not account: + account = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() - if res: - session['account'] = { - 'uuid': res.uuid, - 'role': get_user_role(res), - # 'username': res.username, - } - return res + if account: + if roles: + if account.mode in roles: + set_authentication_in_session(account) + return account + else: + set_authentication_in_session(account) + return account @api_auth.verify_token @@ -55,4 +101,16 @@ def verify_api_auth_token(token) -> User | AdminUser | None: return user if user else get_admin_by_uuid(token) +def standalone_verify(roles: Set[AccountRole]) -> bool: + '''This funciton is for ModelView-based views authentication''' + if verify_from_session(roles): + return True + + auth = basic_auth.get_auth() + if auth: + account = verify_basic_auth_password(auth.username, auth.password, check_session=False, roles=roles) + if account: + return True + return False + # ADD ERROR HANDLING TO AUTHENTICATIONS From 27d451885379e0c28fc2a7d97280f63860e416cb Mon Sep 17 00:00:00 2001 From: Sarina Date: Wed, 13 Dec 2023 12:49:31 +0330 Subject: [PATCH 012/112] add: admin route backward compatibility --- hiddifypanel/panel/admin/__init__.py | 12 ++++++++++-- .../panel/admin/templates/redirect_to_admin.html | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 hiddifypanel/panel/admin/templates/redirect_to_admin.html diff --git a/hiddifypanel/panel/admin/__init__.py b/hiddifypanel/panel/admin/__init__.py index 6e78f1b06..9b1927cc0 100644 --- a/hiddifypanel/panel/admin/__init__.py +++ b/hiddifypanel/panel/admin/__init__.py @@ -1,4 +1,4 @@ -from flask import request, redirect +from flask import render_template, request, redirect, g from flask_admin import Admin from hiddifypanel import Events from .DomainAdmin import DomainAdmin @@ -9,7 +9,6 @@ from apiflask import APIBlueprint from flask_adminlte3 import AdminLTE3 - flask_bp = APIBlueprint("flask", __name__, url_prefix=f"///", template_folder="templates", enable_openapi=False) admin_bp = APIBlueprint("admin", __name__, url_prefix=f"///admin/", template_folder="templates", enable_openapi=False) flaskadmin = Admin(endpoint="admin", base_template='flaskadmin-layout.html') @@ -34,6 +33,15 @@ def init_app(app): def auto_route(): return redirect(request.url.replace("http://", "https://")+"/") + @app.route('///admin/') + @app.doc(hide=True) + def backward_compatibality(): + # TODO: show a page that redirects to the new admin page + # TODO: handle none -browser requests + # return redirect(request.url_root.rstrip('/') + f"/{g.proxy_path}/admin/") + + return render_template('redirect_to_admin.html', admin_link=request.url_root.replace('http://', 'https://').rstrip('/') + f"/{g.proxy_path}/admin/") + # flaskadmin.init_app(app) flaskadmin.add_view(UserAdmin(User, db.session)) diff --git a/hiddifypanel/panel/admin/templates/redirect_to_admin.html b/hiddifypanel/panel/admin/templates/redirect_to_admin.html new file mode 100644 index 000000000..9993a8a9d --- /dev/null +++ b/hiddifypanel/panel/admin/templates/redirect_to_admin.html @@ -0,0 +1,4 @@ +

+ Admin page route is redirected to: {{admin_link}} +
+

\ No newline at end of file From 9c3dd7e97b2e4023db34fff370dd5848c9488906 Mon Sep 17 00:00:00 2001 From: Sarina Date: Wed, 13 Dec 2023 14:18:38 +0330 Subject: [PATCH 013/112] chg: if client sent user/pass we try to authenticate with the user/pass, THEN we check session cookies --- hiddifypanel/panel/authentication.py | 45 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index 1bd6a34bc..1ad906d87 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -3,7 +3,6 @@ from hiddifypanel.models.user import User, get_user_by_uuid from hiddifypanel.models.admin import AdminUser, get_admin_by_uuid from flask import session -from enum import auto from strenum import StrEnum basic_auth = HTTPBasicAuth() @@ -66,31 +65,32 @@ def get_user_role(user) -> AccountRole | None: @basic_auth.verify_password def verify_basic_auth_password(username, password, check_session=True, roles: Set[AccountRole] = set()) -> AdminUser | User | None: - if check_session: - account = verify_from_session(roles) - if account: - return account - username = username.strip() password = password.strip() - account = None - if roles: - if AccountRole.user in roles: - account = User.query.filter(User.username == username, User.password == password).first() + if username and password: + account = None + if roles: + if AccountRole.user in roles: + account = User.query.filter(User.username == username, User.password == password).first() + else: + account = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() else: - account = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() - else: - account = User.query.filter(User.username == username, User.password == password).first() - if not account: - account = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() + account = User.query.filter(User.username == username, User.password == password).first() + if not account: + account = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() - if account: - if roles: - if account.mode in roles: + if account: + if roles: + if account.mode in roles: + set_authentication_in_session(account) + return account + else: set_authentication_in_session(account) return account - else: - set_authentication_in_session(account) + + if check_session: + account = verify_from_session(roles) + if account: return account @@ -103,14 +103,15 @@ def verify_api_auth_token(token) -> User | AdminUser | None: def standalone_verify(roles: Set[AccountRole]) -> bool: '''This funciton is for ModelView-based views authentication''' - if verify_from_session(roles): - return True auth = basic_auth.get_auth() if auth: account = verify_basic_auth_password(auth.username, auth.password, check_session=False, roles=roles) if account: return True + + if verify_from_session(roles): + return True return False # ADD ERROR HANDLING TO AUTHENTICATIONS From 2a1febfb8d745e6cdd66fea012bf93b9c8ef6801 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 16 Dec 2023 20:42:51 +0330 Subject: [PATCH 014/112] chg: authentication --- hiddifypanel/panel/authentication.py | 126 ++++++++++++++------------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index 1ad906d87..aec9d75ae 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -16,32 +16,56 @@ class AccountRole(StrEnum): agent = 'agent' -def set_authentication_in_session(account: User | AdminUser) -> None: +@basic_auth.verify_password +def verify_basic_auth_password(username, password) -> AdminUser | User | None: + username = username.strip() + password = password.strip() + if username and password: + return User.query.filter( + User.username == username, User.password == password).first() or AdminUser.query.filter( + AdminUser.username == username, AdminUser.password == password).first() + + +@api_auth.verify_token +def verify_api_auth_token(token) -> User | AdminUser | None: + # for now, token is the same as uuid + token = token.strip() + if token: + return get_user_by_uuid(token) or get_admin_by_uuid(token) + + # we dont' set session for api auth + # if check_session:Admin + # account = verify_from_session(roles) + # if account: + # return account + + +def set_admin_authentication_in_session(admin: AdminUser) -> None: session['account'] = { - 'uuid': account.uuid, - 'role': get_user_role(account), + 'uuid': admin.uuid, + 'role': get_account_role(admin), # 'username': res.username, } -def verify_from_session(roles: Set[AccountRole] = set()) -> User | AdminUser | None: +def set_user_authentication_in_session(user: User) -> None: + session['user_sign'] = {'uuid': user.uuid} + + +def verify_admin_authentication_from_session() -> User | AdminUser | None: if session.get('account'): - if roles: - if session['account']['role'] in roles: - if AccountRole.user in roles: - return get_user_by_uuid(session['account']['uuid']) - else: - return get_admin_by_uuid(session['account']['uuid']) - else: - if session['account']['role'] == AccountRole.user: - return get_user_by_uuid(session['account']['uuid']) - else: - return get_admin_by_uuid(session['account']['uuid']) + return get_user_by_uuid(session['account']['uuid']) or get_admin_by_uuid(session['account']['uuid']) + +def verify_user_authentication_from_session() -> User | AdminUser | None: + if session.get('user_sign'): + return get_user_by_uuid(session['user_sign']['uuid']) + +# actually this is not used, we authenticate the client and set in the flask.g object, then in views we re-authenticate with their roles to see if they have access to the view or not @api_auth.get_user_roles @basic_auth.get_user_roles -def get_user_role(user) -> AccountRole | None: +def get_account_role(account) -> AccountRole | None: '''Returns user/admin role Allowed roles are: - for user: @@ -51,10 +75,10 @@ def get_user_role(user) -> AccountRole | None: - admin - agent ''' - if isinstance(user, User): + if isinstance(account, User): return AccountRole.user - elif isinstance(user, AdminUser): - match user.mode: + elif isinstance(account, AdminUser): + match account.mode: case 'super_admin': return AccountRole.super_admin case 'admin': @@ -63,55 +87,37 @@ def get_user_role(user) -> AccountRole | None: return AccountRole.agent -@basic_auth.verify_password -def verify_basic_auth_password(username, password, check_session=True, roles: Set[AccountRole] = set()) -> AdminUser | User | None: - username = username.strip() - password = password.strip() - if username and password: - account = None - if roles: - if AccountRole.user in roles: - account = User.query.filter(User.username == username, User.password == password).first() - else: - account = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() - else: - account = User.query.filter(User.username == username, User.password == password).first() - if not account: - account = AdminUser.query.filter(AdminUser.username == username, AdminUser.password == password).first() - - if account: - if roles: - if account.mode in roles: - set_authentication_in_session(account) - return account - else: - set_authentication_in_session(account) - return account +def standalone_admin_basic_auth_verification() -> AdminUser | None: + auth = basic_auth.get_auth() - if check_session: - account = verify_from_session(roles) + if auth: + account = verify_basic_auth_password(auth.username, auth.password) if account: return account + return verify_admin_authentication_from_session() -@api_auth.verify_token -def verify_api_auth_token(token) -> User | AdminUser | None: - # for now, token is the same as uuid - user = get_user_by_uuid(token) - return user if user else get_admin_by_uuid(token) +def standalone_user_basic_auth_verification() -> User | None: + auth = basic_auth.get_auth() -def standalone_verify(roles: Set[AccountRole]) -> bool: - '''This funciton is for ModelView-based views authentication''' + if auth and auth.username and auth.password: + user = verify_basic_auth_password(auth.username, auth.password) + if user: + set_user_authentication_in_session(user) + return user + return verify_user_authentication_from_session() - auth = basic_auth.get_auth() - if auth: - account = verify_basic_auth_password(auth.username, auth.password, check_session=False, roles=roles) - if account: - return True - if verify_from_session(roles): - return True - return False +def standalone_api_auth_verify(): + auth = api_auth.get_auth() + try: + if hasattr(auth, 'token'): + account = verify_api_auth_token(auth.token, check_session=False) + if account: + return account + except AttributeError: + return None + # ADD ERROR HANDLING TO AUTHENTICATIONS From 07a2d36f809f9b2e7696a0888065335275a0fdd7 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 16 Dec 2023 20:48:07 +0330 Subject: [PATCH 015/112] chg: remove from routing in blueprint --- hiddifypanel/base.py | 2 +- hiddifypanel/panel/admin/__init__.py | 21 +++++-------------- .../panel/commercial/restapi/v1/__init__.py | 2 +- .../commercial/restapi/v2/admin/__init__.py | 6 +++--- .../commercial/restapi/v2/hello/__init__.py | 2 +- .../commercial/restapi/v2/user/__init__.py | 6 +++--- hiddifypanel/panel/user/__init__.py | 12 ++++++++++- 7 files changed, 25 insertions(+), 26 deletions(-) diff --git a/hiddifypanel/base.py b/hiddifypanel/base.py index 86be3d910..b3014f2a5 100644 --- a/hiddifypanel/base.py +++ b/hiddifypanel/base.py @@ -16,7 +16,7 @@ def create_app(cli=False, **config): app = APIFlask(__name__, static_url_path="//static/", instance_relative_config=True, version='2.0.0', title="Hiddify API", - openapi_blueprint_url_prefix="///api", docs_ui='elements', json_errors=False, enable_openapi=True) + openapi_blueprint_url_prefix="//api", docs_ui='elements', json_errors=False, enable_openapi=True) # app = Flask(__name__, static_url_path="//static/", instance_relative_config=True) app.wsgi_app = ProxyFix( app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 diff --git a/hiddifypanel/panel/admin/__init__.py b/hiddifypanel/panel/admin/__init__.py index 9b1927cc0..cf46ed70d 100644 --- a/hiddifypanel/panel/admin/__init__.py +++ b/hiddifypanel/panel/admin/__init__.py @@ -9,8 +9,8 @@ from apiflask import APIBlueprint from flask_adminlte3 import AdminLTE3 -flask_bp = APIBlueprint("flask", __name__, url_prefix=f"///", template_folder="templates", enable_openapi=False) -admin_bp = APIBlueprint("admin", __name__, url_prefix=f"///admin/", template_folder="templates", enable_openapi=False) +flask_bp = APIBlueprint("flask", __name__, url_prefix=f"//", template_folder="templates", enable_openapi=False) +admin_bp = APIBlueprint("admin", __name__, url_prefix=f"//admin/", template_folder="templates", enable_openapi=False) flaskadmin = Admin(endpoint="admin", base_template='flaskadmin-layout.html') @@ -28,22 +28,11 @@ def init_app(app): Events.admin_prehook.notify(flaskadmin=flaskadmin, admin_bp=admin_bp) - @app.route('///admin') + @app.route('//admin') @app.doc(hide=True) - def auto_route(): + def auto_route(proxy_path=None, user_secret=None): return redirect(request.url.replace("http://", "https://")+"/") - @app.route('///admin/') - @app.doc(hide=True) - def backward_compatibality(): - # TODO: show a page that redirects to the new admin page - # TODO: handle none -browser requests - # return redirect(request.url_root.rstrip('/') + f"/{g.proxy_path}/admin/") - - return render_template('redirect_to_admin.html', admin_link=request.url_root.replace('http://', 'https://').rstrip('/') + f"/{g.proxy_path}/admin/") - - # flaskadmin.init_app(app) - flaskadmin.add_view(UserAdmin(User, db.session)) flaskadmin.add_view(DomainAdmin(Domain, db.session)) flaskadmin.add_view(AdminstratorAdmin(AdminUser, db.session)) @@ -67,7 +56,7 @@ def backward_compatibality(): # admin_bp.add_url_rule('/admin/quicksetup/',endpoint="quicksetup",view_func=QuickSetup.index,methods=["GET"]) # admin_bp.add_url_rule('/admin/quicksetup/',endpoint="quicksetup-save", view_func=QuickSetup.save,methods=["POST"]) - app.add_url_rule("///admin/static//", endpoint="admin.static") # fix bug in admin with blueprint + app.add_url_rule("//admin/static//", endpoint="admin.static") # fix bug in admin with blueprint flask_bp.debug = True app.register_blueprint(admin_bp) diff --git a/hiddifypanel/panel/commercial/restapi/v1/__init__.py b/hiddifypanel/panel/commercial/restapi/v1/__init__.py index 53d76443a..671c3593e 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/__init__.py +++ b/hiddifypanel/panel/commercial/restapi/v1/__init__.py @@ -9,7 +9,7 @@ from . import tgbot from .tgmsg import SendMsgResource from .resources import * -bp = APIBlueprint("api", __name__, url_prefix="///api/v1", tag="api_v1", enable_openapi=False) +bp = APIBlueprint("api", __name__, url_prefix="//api/v1", tag="api_v1", enable_openapi=False) api = Api(bp) diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py b/hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py index 606dfdd02..56556b086 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py @@ -1,6 +1,6 @@ from apiflask import APIBlueprint -bp = APIBlueprint("api_admin", __name__, url_prefix="///api/v2/admin/", enable_openapi=True) +bp = APIBlueprint("api_admin", __name__, url_prefix="//api/v2/admin/", enable_openapi=True) def init_app(app): @@ -10,8 +10,8 @@ def init_app(app): from .server_status_api import AdminServerStatusApi from .admin_user_api import AdminUserApi from .admin_users_api import AdminUsersApi - bp.add_url_rule('/me/',view_func=AdminInfoApi) - bp.add_url_rule('/server_status/',view_func=AdminServerStatusApi) + bp.add_url_rule('/me/', view_func=AdminInfoApi) + bp.add_url_rule('/server_status/', view_func=AdminServerStatusApi) bp.add_url_rule('/admin_user/', view_func=AdminUserApi) bp.add_url_rule('/admin_user/', view_func=AdminUsersApi) diff --git a/hiddifypanel/panel/commercial/restapi/v2/hello/__init__.py b/hiddifypanel/panel/commercial/restapi/v2/hello/__init__.py index 175fad1e8..49db7e390 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/hello/__init__.py +++ b/hiddifypanel/panel/commercial/restapi/v2/hello/__init__.py @@ -5,7 +5,7 @@ from flask import Blueprint from apiflask import APIBlueprint -bp = APIBlueprint("api_hello", __name__, url_prefix="///api/v2/hello/", enable_openapi=True) +bp = APIBlueprint("api_hello", __name__, url_prefix="//api/v2/hello/", enable_openapi=True) def init_app(app): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/__init__.py b/hiddifypanel/panel/commercial/restapi/v2/user/__init__.py index 45203091f..f30eb0eeb 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/__init__.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/__init__.py @@ -1,6 +1,6 @@ from apiflask import APIBlueprint -bp = APIBlueprint("api_user", __name__, url_prefix="///api/v2/user/", enable_openapi=True) +bp = APIBlueprint("api_user", __name__, url_prefix="//api/v2/user/", enable_openapi=True) def init_app(app): @@ -11,11 +11,11 @@ def init_app(app): from .configs_api import AllConfigsAPI from .short_api import ShortAPI from .apps_api import AppAPI - + bp.add_url_rule("/me/", view_func=InfoAPI) bp.add_url_rule("/mtproxies/", view_func=MTProxiesAPI) bp.add_url_rule("/all-configs/", view_func=AllConfigsAPI) bp.add_url_rule("/short/", view_func=ShortAPI) - bp.add_url_rule('/apps/',view_func=AppAPI) + bp.add_url_rule('/apps/', view_func=AppAPI) app.register_blueprint(bp) diff --git a/hiddifypanel/panel/user/__init__.py b/hiddifypanel/panel/user/__init__.py index c5bbf0dfa..c2eabe08c 100644 --- a/hiddifypanel/panel/user/__init__.py +++ b/hiddifypanel/panel/user/__init__.py @@ -7,7 +7,7 @@ # from .resources import ProductItemResource, ProductResource from .user import * from apiflask import APIBlueprint -bp = APIBlueprint("user2", __name__, url_prefix="///", template_folder="templates", enable_openapi=False) +bp = APIBlueprint("user2", __name__, url_prefix="//", template_folder="templates", enable_openapi=False) def send_static(path): @@ -15,7 +15,17 @@ def send_static(path): def init_app(app): + # @app.route('//') + # @bp.route('///') + # @app.doc(hide=True) + # def backward_compatibality(): + # # TODO: handle none -browser requests + # # return redirect(request.url_root.rstrip('/') + f"/{g.proxy_path}/admin/") + + # return render_template('redirect_to_user.html', user_link=request.url_root.replace('http://', 'https://').rstrip('/') + f"/{g.proxy_path}/#{g.account.name}") + UserView.register(bp, route_base="/") + # bp.add_url_rule("/", view_func=index) # bp.add_url_rule("/", view_func=index) # bp.add_url_rule("/clash//.yml", view_func=clash_config) From d9c63e0a2503d351c474a043c2be6cd42e357536 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 16 Dec 2023 20:50:19 +0330 Subject: [PATCH 016/112] add: some functions in utils --- hiddifypanel/hutils/utils.py | 47 +++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/hiddifypanel/hutils/utils.py b/hiddifypanel/hutils/utils.py index 57f560e2d..e4993dbf2 100644 --- a/hiddifypanel/hutils/utils.py +++ b/hiddifypanel/hutils/utils.py @@ -1,32 +1,15 @@ -import socket +from urllib.parse import urlparse from uuid import UUID -import user_agents -from sqlalchemy.orm import Load -import glob -import json -from babel.dates import format_timedelta as babel_format_timedelta -from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ -import datetime -from flask import jsonify, g, url_for, Markup, abort, current_app, request +from datetime import datetime +from flask import url_for, Markup from flask import flash as flask_flash import re -from wtforms.validators import ValidationError import requests import string import random -from babel.dates import format_timedelta as babel_format_timedelta -import urllib -import time import os -import psutil -from urllib.parse import urlparse -import ssl -import h2.connection -import subprocess -import netifaces -import time import sys from hiddifypanel.cache import cache @@ -131,7 +114,7 @@ def date_to_json(d): def json_to_date(date_str): try: - return datetime.datetime.strptime(date_str, '%Y-%m-%d') + return datetime.strptime(date_str, '%Y-%m-%d') except: return date_str @@ -143,7 +126,7 @@ def time_to_json(d): def json_to_time(time_str): try: - return datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S") + return datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S") except: return time_str @@ -151,3 +134,23 @@ def json_to_time(time_str): def flash(message, category): print(message) return flask_flash(Markup(message), category) + + +def get_proxy_path_from_url(url: str) -> str | None: + url_path = urlparse(url).path + proxy_path = url_path.lstrip('/').split('/')[0] or None + return proxy_path + + +def is_uuid_in_url_path(path: str) -> bool: + for section in path.split('/'): + if is_uuid_valid(section, 4): + return True + return False + + +def get_uuid_from_url_path(path: str) -> str | None: + for section in path.split('/'): + if is_uuid_valid(section, 4): + return section + return None From b9e03bb9eee57f816a62a1286d8c1abc19c0a526 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 16 Dec 2023 21:26:18 +0330 Subject: [PATCH 017/112] add: api auth, basic auth, old url backward compatibility middlewares chg: g.user and g.admin to g.account --- hiddifypanel/panel/common.py | 223 ++++++++++++++++++++++++++--------- 1 file changed, 170 insertions(+), 53 deletions(-) diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index efb499780..241e55930 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -1,17 +1,15 @@ import traceback -import uuid - import user_agents -from flask import render_template, request, jsonify +from flask import render_template, request, jsonify, redirect from flask import g, send_from_directory, session, Markup -from jinja2 import Environment, FileSystemLoader from flask_babelex import gettext as _ import hiddifypanel from hiddifypanel.models import * from hiddifypanel.panel import hiddify, github_issue_generator from sys import version as python_version from platform import platform -from werkzeug.exceptions import HTTPException as WerkzeugHTTPException +import hiddifypanel.panel.authentication as auth +import hiddifypanel.hutils as hutils from apiflask import APIFlask, HTTPError, abort @@ -92,14 +90,14 @@ def set_default_path_values(spec): if parameter['name'] == 'proxy_path': parameter['schema'] = {'type': 'string', 'default': g.proxy_path} # elif parameter['name'] == 'user_secret': - # parameter['schema'] = {'type': 'string', 'default': g.user_uuid} + # parameter['schema'] = {'type': 'string', 'default': g.account_uuid} return spec @app.url_defaults def add_proxy_path_user(endpoint, values): if 'user_secret' not in values and hasattr(g, 'user_uuid'): - values['user_secret'] = f'{g.user_uuid}' + values['user_secret'] = f'{g.account_uuid}' if 'proxy_path' not in values: # values['proxy_path']=f'{g.proxy_path}' values['proxy_path'] = hconfig(ConfigEnum.proxy_path) @@ -115,61 +113,40 @@ def videos(file): # diff=datetime.datetime.now()-value # return format_timedelta(diff, add_direction=True, locale=hconfig(ConfigEnum.lang)) - @app.url_value_preprocessor - def pull_secret_code(endpoint, values): - # print("Y",endpoint, values)6 - # if values is None: - # return - # if hiddifypanel.__release_date__ + datetime.timedelta(days=40) < datetime.datetime.now() or hiddifypanel.__release_date__ > datetime.datetime.now(): - # abort(400, _('This version of hiddify panel is outdated. Please update it from admin area.')) - g.user = None - g.user_uuid = None - g.is_admin = False - - if request.args.get('darkmode') != None: - session['darkmode'] = request.args.get( - 'darkmode', '').lower() == 'true' - g.darkmode = session.get('darkmode', False) - import random - g.install_pwa = random.random() <= 0.05 - if request.args.get('pwa') != None: - session['pwa'] = request.args.get('pwa', '').lower() == 'true' - g.pwa = session.get('pwa', False) + @app.before_request + def base_middleware(): + if request.endpoint == 'static' or request.endpoint == "videos": + return + # validate request made by human (just check user agent, there's no capcha) g.user_agent = user_agents.parse(request.user_agent.string) - if g.user_agent.is_bot: abort(400, "invalid") - g.proxy_path = values.pop('proxy_path', None) if values else None + # validate proxy path + g.proxy_path = hutils.utils.get_proxy_path_from_url(request.url) + if not g.proxy_path: + abort(400, "invalid") if g.proxy_path != hconfig(ConfigEnum.proxy_path): if app.config['DEBUG']: abort(400, Markup( f"Invalid Proxy Path admin")) abort(400, "Invalid Proxy Path") - if endpoint == 'static' or endpoint == "videos": - return - tmp_secret = values.pop('user_secret', None) if values else None - try: - if tmp_secret: - g.user_uuid = uuid.UUID(tmp_secret) - except: - # raise PermissionError("Invalid secret") - abort(400, 'invalid user') - g.admin = get_admin_user_db(tmp_secret) - g.is_admin = g.admin is not None - bare_path = request.path.replace( - g.proxy_path, "").replace(tmp_secret, "").lower() - if not g.is_admin: - g.user = User.query.filter(User.uuid == f'{g.user_uuid}').first() - if not g.user: - abort(401, 'invalid user') - if endpoint and ("admin" in endpoint or "api/v1" in endpoint): - # raise PermissionError("Access Denied") - abort(403, 'Access Denied') - if "admin" in bare_path or "api/v1" in bare_path: - abort(403, 'Access Denied') + # setup dark mode + if request.args.get('darkmode') != None: + session['darkmode'] = request.args.get( + 'darkmode', '').lower() == 'true' + g.darkmode = session.get('darkmode', False) + + # setup pwa + import random + g.install_pwa = random.random() <= 0.05 + if request.args.get('pwa') != None: + session['pwa'] = request.args.get('pwa', '').lower() == 'true' + g.pwa = session.get('pwa', False) + + # setup telegram bot if hconfig(ConfigEnum.telegram_bot_token): import hiddifypanel.panel.commercial.telegrambot as telegrambot if (not telegrambot.bot) or (not telegrambot.bot.username): @@ -178,6 +155,146 @@ def pull_secret_code(endpoint, values): else: g.bot = None + @app.before_request + def api_auth_middleware(): + '''In every api request(whether is for the admin or the user) the client should provide api key and we check it''' + if 'api' not in request.path: # type: ignore + return + + # get authenticated account + account: AdminUser | User | None = auth.standalone_api_auth_verify() + if not account: + return abort(401) + # get account role + role = auth.get_account_role(account) + if not role: + return abort(401) + # setup authenticated account things (uuid, is_admin, etc.) + g.account = account + g.account_uuid = account.uuid + g.is_admin = False if role == auth.AccountRole.user else True + + @app.before_request + def backward_compatibile_middleware(): + if hutils.utils.is_uuid_in_url_path(request.path): + if g.proxy_path != hconfig(ConfigEnum.proxy_path): + # this will make a fingerprint for panel. we should redirect the request to decoy website. + # abort(400, "invalid proxy path") + redirect(hconfig(ConfigEnum.decoy_domain)) + + if '/admin/' in request.path: + # we check if there is such uuid or not, because we don't want to redirect to admin panel if there is no such uuid + # otherwise anyone can provide any secret to get access to admin panel + uuid = hutils.utils.get_uuid_from_url_path(request.path) or abort(400, 'invalid request') + if get_admin_by_uuid(uuid): + return render_template('redirect_to_admin.html', admin_link=f'{request.url_root.rstrip("/").replace("http", "https")}/{g.proxy_path}/admin/') + else: + abort(400, 'invalid request') + else: + uuid = hutils.utils.get_uuid_from_url_path(request.path) or abort(400, 'invalid request') + if user := get_user_by_uuid(uuid): + return render_template('redirect_to_user.html', user_link=f'{request.url_root.rstrip("/").replace("http", "https")}/{g.proxy_path}/#{user.name}') + else: + abort(400, 'invalid request') + + @app.before_request + def basic_auth_middleware(): + '''if the request is for user panel(user page), we try to authenticate the user with basic auth or the client session data, we do that for admin panel too''' + if 'api' in request.path: # type: ignore + return + + account: AdminUser | User | None = None + + # if we don't have endpoint, we can't detect the request is for admin panel or user panel, so we can't authenticate + if not request.endpoint: + abort(400, "invalid request") + + if request.endpoint and 'UserView' in request.endpoint: + account = auth.standalone_user_basic_auth_verification() + else: + account = auth.standalone_admin_basic_auth_verification() + # get authenticated account + if not account: + return abort(401) + # get account role + role = auth.get_account_role(account) + if not role: + return abort(401) + # setup authenticated account things (uuid, is_admin, etc.) + g.account = account + g.account_uuid = account.uuid + g.is_admin = False if role == auth.AccountRole.user else True + + # @app.auth_required(basic_auth, roles=['super_admin', 'admin', 'agent', 'user']) + + @app.url_value_preprocessor + def pull_secret_code(endpoint, values): + # just remove proxy_path + # by doing that we don't need to get it in every view function, we have it in g.proxy_path. it's done in base_middleware function + if values: + values.pop('proxy_path', None) + values.pop('user_secret', None) + # print("Y",endpoint, values)6 + # if values is None: + # return + # if hiddifypanel.__release_date__ + datetime.timedelta(days=40) < datetime.datetime.now() or hiddifypanel.__release_date__ > datetime.datetime.now(): + # abort(400, _('This version of hiddify panel is outdated. Please update it from admin area.')) + # g.user = None + # g.account_uuid = None + # g.is_admin = False + + # if request.args.get('darkmode') != None: + # session['darkmode'] = request.args.get( + # 'darkmode', '').lower() == 'true' + # g.darkmode = session.get('darkmode', False) + # import random + # g.install_pwa = random.random() <= 0.05 + # if request.args.get('pwa') != None: + # session['pwa'] = request.args.get('pwa', '').lower() == 'true' + # g.pwa = session.get('pwa', False) + + # g.user_agent = user_agents.parse(request.user_agent.string) + + # if g.user_agent.is_bot: + # abort(400, "invalid") + # g.proxy_path = values.pop('proxy_path', None) if values else None + + # if g.proxy_path != hconfig(ConfigEnum.proxy_path): + # if app.config['DEBUG']: + # abort(400, Markup( + # f"Invalid Proxy Path admin")) + # abort(400, "Invalid Proxy Path") + # if endpoint == 'static' or endpoint == "videos": + # return + # tmp_secret = values.pop('user_secret', None) if values else None + # try: + # if tmp_secret: + # g.account_uuid = uuid.UUID(tmp_secret) + # except: + # # raise PermissionError("Invalid secret") + # abort(400, 'invalid user') + # g.account = get_admin_user_db(tmp_secret) + # g.is_admin = g.account is not None + # bare_path = request.path.replace( + # g.proxy_path, "").replace(tmp_secret, "").lower() + # if not g.is_admin: + # g.user = User.query.filter(User.uuid == f'{g.account_uuid}').first() + # if not g.user: + # abort(401, 'invalid user') + # if endpoint and ("admin" in endpoint or "api/v1" in endpoint): + # # raise PermissionError("Access Denied") + # abort(403, 'Access Denied') + # if "admin" in bare_path or "api/v1" in bare_path: + # abort(403, 'Access Denied') + + # if hconfig(ConfigEnum.telegram_bot_token): + # import hiddifypanel.panel.commercial.telegrambot as telegrambot + # if (not telegrambot.bot) or (not telegrambot.bot.username): + # telegrambot.register_bot() + # g.bot = telegrambot.bot + # else: + # g.bot = None + # print(g.user) def github_issue_details(): @@ -194,8 +311,8 @@ def github_issue_details(): def generate_github_issue_link_for_500_error(error, traceback, remove_sensetive_data=True, remove_unrelated_traceback_datails=True): def remove_sensetive_data_from_github_issue_link(issue_link): - if hasattr(g, 'user_uuid') and g.user_uuid: - issue_link.replace(f'{g.user_uuid}', '*******************') + if hasattr(g, 'user_uuid') and g.account_uuid: + issue_link.replace(f'{g.account_uuid}', '*******************') if hconfig(ConfigEnum.proxy_path) and hconfig(ConfigEnum.proxy_path): issue_link.replace(hconfig(ConfigEnum.proxy_path), '**********') From f736c1a44c9c96ed35623d6722043321ed0ed66f Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 16 Dec 2023 21:41:15 +0330 Subject: [PATCH 018/112] fix: using g.account instead of g.user/g.admin --- hiddifypanel/models/admin.py | 10 ++--- hiddifypanel/models/usage.py | 3 +- hiddifypanel/panel/admin/AdminstratorAdmin.py | 31 +++++++------- hiddifypanel/panel/admin/Backup.py | 4 +- hiddifypanel/panel/admin/Dashboard.py | 12 +++--- hiddifypanel/panel/admin/SettingAdmin.py | 7 ++-- hiddifypanel/panel/admin/UserAdmin.py | 22 +++++----- .../panel/commercial/ProxyDetailsAdmin.py | 18 +------- .../panel/commercial/restapi/v1/tgbot.py | 4 +- .../restapi/v2/admin/admin_info_api.py | 7 ++-- .../restapi/v2/admin/server_status_api.py | 4 +- .../commercial/restapi/v2/admin/user_api.py | 2 +- .../commercial/restapi/v2/user/apps_api.py | 18 ++++---- .../commercial/restapi/v2/user/configs_api.py | 6 +-- .../commercial/restapi/v2/user/info_api.py | 20 ++++----- .../commercial/restapi/v2/user/mtproxies.py | 6 +-- .../commercial/restapi/v2/user/short_api.py | 4 +- hiddifypanel/panel/hiddify.py | 35 +++++++++------- hiddifypanel/panel/user/link_maker.py | 4 +- .../user/templates/clash_config copy.yml | 2 +- hiddifypanel/panel/user/user.py | 42 +++++++++---------- hiddifypanel/templates/lte-master.html | 2 +- 22 files changed, 127 insertions(+), 136 deletions(-) diff --git a/hiddifypanel/models/admin.py b/hiddifypanel/models/admin.py index 6ef70ae01..d0ce9b0b9 100644 --- a/hiddifypanel/models/admin.py +++ b/hiddifypanel/models/admin.py @@ -78,14 +78,14 @@ def recursive_sub_admins_ids(self, depth=20, seen=None): return sub_admin_ids def remove(model): - if model.id == 1 or model.id == g.admin.id: + if model.id == 1 or model.id == g.account.id: # raise ValidationError(_("Owner can not be deleted!")) abort(422, __("Owner can not be deleted!")) users = model.recursive_users_query().all() for u in users: - u.added_by = g.admin.id + u.added_by = g.account.id - DailyUsage.query.filter(DailyUsage.admin_id.in_(model.recursive_sub_admins_ids())).update({'admin_id': g.admin.id}) + DailyUsage.query.filter(DailyUsage.admin_id.in_(model.recursive_sub_admins_ids())).update({'admin_id': g.account.id}) AdminUser.query.filter(AdminUser.id.in_(model.recursive_sub_admins_ids())).delete() db.session.commit() @@ -178,8 +178,8 @@ def bulk_register_admins(users=[], commit=True): def current_admin_or_owner(): - if g and hasattr(g, 'admin') and g.admin: - return g.admin + if g and hasattr(g, 'account') and g.account and isinstance(g.account, AdminUser): + return g.account return AdminUser.query.filter(AdminUser.id == 1).first() diff --git a/hiddifypanel/models/usage.py b/hiddifypanel/models/usage.py index 8b828d622..660a30b41 100644 --- a/hiddifypanel/models/usage.py +++ b/hiddifypanel/models/usage.py @@ -8,7 +8,6 @@ from hiddifypanel.panel.database import db - class DailyUsage(db.Model, SerializerMixin): id = db.Column(db.Integer, primary_key=True, autoincrement=True) date = db.Column(db.Date, default=datetime.date.today()) @@ -24,7 +23,7 @@ class DailyUsage(db.Model, SerializerMixin): def get_daily_usage_stats(admin_id=None, child_id=None): from .admin import AdminUser if not admin_id: - admin_id = g.admin.id + admin_id = g.account.id sub_admins = AdminUser.query.filter(AdminUser.id == admin_id).first().recursive_sub_admins_ids() print(sub_admins) diff --git a/hiddifypanel/panel/admin/AdminstratorAdmin.py b/hiddifypanel/panel/admin/AdminstratorAdmin.py index e4108322b..6049144d2 100644 --- a/hiddifypanel/panel/admin/AdminstratorAdmin.py +++ b/hiddifypanel/panel/admin/AdminstratorAdmin.py @@ -14,11 +14,11 @@ class AdminModeField(SelectField): def __init__(self, label=None, validators=None, **kwargs): super(AdminModeField, self).__init__(label, validators, **kwargs) - if g.admin.mode in [AdminMode.agent, AdminMode.admin]: + if g.account.mode in [AdminMode.agent, AdminMode.admin]: self.choices = [(AdminMode.agent.value, 'agent')] - elif g.admin.mode == AdminMode.admin: + elif g.account.mode == AdminMode.admin: self.choices = [(AdminMode.agent.value, 'agent'), (AdminMode.admin.value, 'Admin'),] - elif g.admin.mode == AdminMode.super_admin: + elif g.account.mode == AdminMode.super_admin: self.choices = [(AdminMode.agent.value, 'agent'), (AdminMode.admin.value, 'Admin'), (AdminMode.super_admin.value, 'Super Admin')] @@ -26,8 +26,8 @@ class SubAdminsField(SelectField): def __init__(self, label=None, validators=None, *args, **kwargs): kwargs.pop("allow_blank") super().__init__(label, validators, *args, **kwargs) - self.choices = [(admin.id, admin.name) for admin in g.admin.sub_admins] - self.choices += [(g.admin.id, g.admin.name)] + self.choices = [(admin.id, admin.name) for admin in g.account.sub_admins] + self.choices += [(g.account.id, g.account.name)] class AdminstratorAdmin(AdminLTEModelView): @@ -83,7 +83,7 @@ def _ul_formatter(view, context, model, name): @property def can_create(self): - return g.admin.can_add_admin or g.admin.mode == AdminMode.super_admin + return g.account.can_add_admin or g.account.mode == AdminMode.super_admin def _name_formatter(view, context, model, name): proxy_path = hconfig(ConfigEnum.proxy_path) @@ -161,14 +161,15 @@ def _max_active_users_formatter(view, context, model, name): def search_placeholder(self): return f"{_('search')} {_('user.UUID')} {_('user.name')}" - # def is_accessible(self): - # return g.admin.mode==AdminMode.super_admin + @hiddify.admin + def is_accessible(self): + return True def get_query(self): # Get the base query query = super().get_query() - admin_ids = g.admin.recursive_sub_admins_ids() + admin_ids = g.account.recursive_sub_admins_ids() query = query.filter(AdminUser.id.in_(admin_ids)) return query @@ -178,7 +179,7 @@ def get_count_query(self): # Get the base count query query = super().get_count_query() - admin_ids = g.admin.recursive_sub_admins_ids() + admin_ids = g.account.recursive_sub_admins_ids() query = query.filter(AdminUser.id.in_(admin_ids)) return query @@ -192,10 +193,10 @@ def on_model_change(self, form, model, is_created): # model.parent_admin_id=1 # model.parent_admin=AdminUser.query.filter(AdminUser.id==1).first() if model.id != 1 and model.parent_admin == None: - model.parent_admin_id = g.admin.id - model.parent_admin = g.admin + model.parent_admin_id = g.account.id + model.parent_admin = g.account - if g.admin.mode != AdminMode.super_admin and model.mode == AdminMode.super_admin: + if g.account.mode != AdminMode.super_admin and model.mode == AdminMode.super_admin: raise ValidationError("Sub-Admin can not have more power!!!!") if model.mode == AdminMode.agent and model.mode != AdminMode.agent: raise ValidationError("Sub-Admin can not have more power!!!!") @@ -210,11 +211,11 @@ def get_query_for_parent_admin(self): def on_form_prefill(self, form, id=None): - if g.admin.mode != AdminMode.super_admin: + if g.account.mode != AdminMode.super_admin: del form.mode del form.can_add_admin - if g.admin.id == form._obj.id: + if g.account.id == form._obj.id: del form.max_users del form.max_active_users del form.comment diff --git a/hiddifypanel/panel/admin/Backup.py b/hiddifypanel/panel/admin/Backup.py index b9920bea7..88ccc58bf 100644 --- a/hiddifypanel/panel/admin/Backup.py +++ b/hiddifypanel/panel/admin/Backup.py @@ -22,7 +22,7 @@ class Backup(FlaskView): - decorators = [app.auth_required(basic_auth, roles=['super_admin'])] + decorators = [hiddify.super_admin] def index(self): return render_template('backup.html', restore_form=get_restore_form()) @@ -61,7 +61,7 @@ def post(self): from flask_babel import refresh refresh() - return redirect(f'/{hconfig(ConfigEnum.proxy_path)}/{g.admin.uuid}/admin/actions/reinstall2/', code=302) + return redirect(f'/{hconfig(ConfigEnum.proxy_path)}/{g.account.uuid}/admin/actions/reinstall2/', code=302) from . import Actions action = Actions() return action.reinstall(complete_install=True, domain_changed=True) diff --git a/hiddifypanel/panel/admin/Dashboard.py b/hiddifypanel/panel/admin/Dashboard.py index c1e6998c2..f191b6067 100644 --- a/hiddifypanel/panel/admin/Dashboard.py +++ b/hiddifypanel/panel/admin/Dashboard.py @@ -16,9 +16,10 @@ class Dashboard(FlaskView): + @hiddify.admin def get_data(self): - admin_id = request.args.get("admin_id") or g.admin.id - if admin_id not in g.admin.recursive_sub_admins_ids(): + admin_id = request.args.get("admin_id") or g.account.id + if admin_id not in g.account.recursive_sub_admins_ids(): abort(403, _("Access Denied!")) return jsonify(dict( @@ -26,7 +27,7 @@ def get_data(self): usage_history=get_daily_usage_stats(admin_id) )) - @app.auth_required(basic_auth, roles=['super_admin', 'admin']) + @hiddify.admin def index(self): if hconfig(ConfigEnum.first_setup): @@ -36,8 +37,8 @@ def index(self): bot = None # if hconfig(ConfigEnum.license): childs = None - admin_id = request.args.get("admin_id") or g.admin.id - if admin_id not in g.admin.recursive_sub_admins_ids(): + admin_id = request.args.get("admin_id") or g.account.id + if admin_id not in g.account.recursive_sub_admins_ids(): abort(403, _("Access Denied!")) child_id = request.args.get("child_id") or None @@ -84,6 +85,7 @@ def index(self): stats = {'system': hiddify.system_stats(), 'top5': hiddify.top_processes()} return render_template('index.html', stats=stats, usage_history=get_daily_usage_stats(admin_id, child_id), childs=childs) + @hiddify.super_admin @route('remove_child', methods=['POST']) def remove_child(self): child_id = request.form['child_id'] diff --git a/hiddifypanel/panel/admin/SettingAdmin.py b/hiddifypanel/panel/admin/SettingAdmin.py index 7b6acbb8e..fb536d1d9 100644 --- a/hiddifypanel/panel/admin/SettingAdmin.py +++ b/hiddifypanel/panel/admin/SettingAdmin.py @@ -25,14 +25,13 @@ class SettingAdmin(FlaskView): - # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def index(self): form = get_config_form() return render_template('config.html', form=form) # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def post(self): set_hconfig(ConfigEnum.first_setup, False) form = get_config_form() @@ -270,7 +269,7 @@ class CategoryForm(FlaskForm): if hasattr(val, "regex"): render_kw['pattern'] = val.regex.pattern render_kw['title'] = val.message - if c.key == ConfigEnum.reality_public_key and g.admin.mode in [AdminMode.super_admin]: + if c.key == ConfigEnum.reality_public_key and g.account.mode in [AdminMode.super_admin]: extra_info = f" {_('Change')}" field = wtf.fields.StringField(_(f'config.{c.key}.label'), validators, default=c.value, description=_(f'config.{c.key}.description')+extra_info, render_kw=render_kw) diff --git a/hiddifypanel/panel/admin/UserAdmin.py b/hiddifypanel/panel/admin/UserAdmin.py index 60c5c8a9c..a7c42eab7 100644 --- a/hiddifypanel/panel/admin/UserAdmin.py +++ b/hiddifypanel/panel/admin/UserAdmin.py @@ -11,7 +11,6 @@ from apiflask import abort -from hiddifypanel.panel.authentication import standalone_verify, AccountRole from hiddifypanel.panel import hiddify, custom_widgets from hiddifypanel.panel.hiddify import flash from hiddifypanel.drivers import user_driver @@ -205,8 +204,9 @@ def on_model_delete(self, model): user_driver.remove_client(model) # hiddify.flash_config_success() + @hiddify.admin def is_accessible(self): - return standalone_verify({AccountRole.super_admin, AccountRole.admin}) + return True def on_form_prefill(self, form, id=None): # print("================",form._obj.start_date) @@ -253,10 +253,10 @@ def on_model_change(self, form, model, is_created): model.package_days = min(model.package_days, 10000) old_user = user_by_id(model.id) if not model.added_by or model.added_by == 1: - model.added_by = g.admin.id - if not g.admin.can_have_more_users(): + model.added_by = g.account.id + if not g.account.can_have_more_users(): raise ValidationError(_('You have too much users! You can have only %(active)s active users and %(total)s users', - active=g.admin.max_active_users, total=g.admin.max_users)) + active=g.account.max_active_users, total=g.account.max_users)) if old_user and old_user.uuid != model.uuid: user_driver.remove_client(old_user) if not model.ed25519_private_key: @@ -337,8 +337,8 @@ def get_query(self): # Get the base query query = super().get_query() - admin_id = int(request.args.get("admin_id") or g.admin.id) - if admin_id not in g.admin.recursive_sub_admins_ids(): + admin_id = int(request.args.get("admin_id") or g.account.id) + if admin_id not in g.account.recursive_sub_admins_ids(): abort(403) admin = AdminUser.query.filter(AdminUser.id == admin_id).first() if not admin: @@ -356,8 +356,8 @@ def get_count_query(self): # query = query.session.query(func.count(User.id)) query = super().get_count_query() - admin_id = int(request.args.get("admin_id") or g.admin.id) - if admin_id not in g.admin.recursive_sub_admins_ids(): + admin_id = int(request.args.get("admin_id") or g.account.id) + if admin_id not in g.account.recursive_sub_admins_ids(): abort(403) admin = AdminUser.query.filter(AdminUser.id == admin_id).first() if not admin: @@ -365,8 +365,8 @@ def get_count_query(self): query = query.filter(User.added_by.in_(admin.recursive_sub_admins_ids())) - # admin_id=int(request.args.get("admin_id") or g.admin.id) - # if admin_id not in g.admin.recursive_sub_admins_ids(): + # admin_id=int(request.args.get("admin_id") or g.account.id) + # if admin_id not in g.account.recursive_sub_admins_ids(): # abort(403) # admin=AdminUser.query.filter(AdminUser.id==admin_id).first() # if not admin: diff --git a/hiddifypanel/panel/commercial/ProxyDetailsAdmin.py b/hiddifypanel/panel/commercial/ProxyDetailsAdmin.py index baa6c309c..3a49a64ec 100644 --- a/hiddifypanel/panel/commercial/ProxyDetailsAdmin.py +++ b/hiddifypanel/panel/commercial/ProxyDetailsAdmin.py @@ -1,24 +1,10 @@ -from flask_admin.contrib import sqla -from hiddifypanel.panel.database import db -from wtforms.validators import Regexp from hiddifypanel.models import * -from wtforms.validators import Regexp, ValidationError from hiddifypanel.panel.admin.adminlte import AdminLTEModelView from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ from hiddifypanel.panel import hiddify -from flask import Markup, g -from flask import Flask -from flask_sqlalchemy import SQLAlchemy -from flask_admin import Admin -from flask_admin.contrib.sqla import ModelView -from wtforms import SelectMultipleField - - -from wtforms.widgets import ListWidget, CheckboxInput -from sqlalchemy.orm import backref +from flask import g # Define a custom field type for the related domains -from flask_admin.form.fields import Select2TagsField, Select2Field class ProxyDetailsAdmin(AdminLTEModelView): @@ -42,4 +28,4 @@ def after_model_delete(self, model): pass def is_accessible(self): - return g.admin.mode in [AdminMode.admin, AdminMode.super_admin] + return g.account.mode in [AdminMode.admin, AdminMode.super_admin] diff --git a/hiddifypanel/panel/commercial/restapi/v1/tgbot.py b/hiddifypanel/panel/commercial/restapi/v1/tgbot.py index a5c8cc964..383292a53 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/tgbot.py +++ b/hiddifypanel/panel/commercial/restapi/v1/tgbot.py @@ -1,6 +1,6 @@ import time import telebot -from flask import request +from flask import request from apiflask import abort from flask_restful import Resource @@ -34,7 +34,7 @@ def register_bot(set_hook=False): proxy_path = hconfig(ConfigEnum.proxy_path) user_secret = get_super_admin_secret() - bot.set_webhook(url=f"https://{domain}/{proxy_path}/{user_secret}/api/v1/tgbot/") + bot.set_webhook(url=f"https://{domain}/{proxy_path}/api/v1/tgbot/") except Exception as e: print(e) import traceback diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py index a3a6b100b..e3263bc81 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py @@ -12,13 +12,12 @@ class AdminInfoApi(MethodView): - decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] + decorators = [hiddify.admin] @app.output(AdminSchema) - # @app.auth_required(api_auth, roles=['super_admin', 'admin']) def get(self): - # in this case g.user_uuid is equal to admin uuid - admin_uuid = g.user_uuid + # in this case g.account_uuid is equal to admin uuid + admin_uuid = g.account_uuid admin = get_admin_user_db(admin_uuid) or abort(404, "user not found") dto = AdminSchema() diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py index ea1f6c451..2cd20af6a 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py @@ -14,7 +14,7 @@ class ServerStatus(Schema): class AdminServerStatusApi(MethodView): - decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] + decorators = [hiddify.admin] @app.output(ServerStatus) def get(self): @@ -23,6 +23,6 @@ def get(self): 'system': hiddify.system_stats(), 'top5': hiddify.top_processes() } - admin_id = request.args.get("admin_id") or g.admin.id + admin_id = request.args.get("admin_id") or g.account.id dto.usage_history = get_daily_usage_stats(admin_id) return dto diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py index 1206de87a..72009037f 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py @@ -68,7 +68,7 @@ class UserSchema(Schema): class UserApi(MethodView): - decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] + decorators = [hiddify.admin] @app.output(UserSchema) def get(self, uuid): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py index 3e1f629f3..2078541fb 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py @@ -67,25 +67,25 @@ class AppInSchema(Schema): class AppAPI(MethodView): - decorators = [app.auth_required(api_auth, roles=['user'])] + decorators = [hiddify.user_auth] def __init__(self) -> None: super().__init__() self.hiddify_github_repo = 'https://github.com/hiddify' - self.user_panel_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.user_uuid}/" + self.user_panel_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/" self.user_panel_encoded_url = quote_plus(self.user_panel_url) - c = get_common_data(g.user_uuid, 'new') + c = get_common_data(g.account_uuid, 'new') self.subscription_link_url = f"{self.user_panel_url}all.txt?name={c['db_domain'].alias or c['db_domain'].domain}-{c['asn']}&asn={c['asn']}&mode={c['mode']}" self.subscription_link_encoded_url = do_base_64(self.subscription_link_url) domain = c['db_domain'].alias or c['db_domain'].domain self.profile_title = c['profile_title'] - # self.clash_all_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_all_{domain}-{c['mode']}" - # self.clash_foreign_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_normal_{domain}-{c['mode']}" - # self.clash_blocked_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_lite_{c['db_domain'].alias or c['db_domain'].domain}-{c['mode']}" - # self.clash_meta_all_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/meta/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mall_{domain}-{c['mode']}" - # self.clash_meta_foreign_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/meta/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mnormal_{domain}-{c['mode']}" - self.clash_meta_blocked_sites = f"https://{domain}/{g.proxy_path}/{g.user_uuid}/clash/meta/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mlite_{domain}-{c['mode']}" + # self.clash_all_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_all_{domain}-{c['mode']}" + # self.clash_foreign_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_normal_{domain}-{c['mode']}" + # self.clash_blocked_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_lite_{c['db_domain'].alias or c['db_domain'].domain}-{c['mode']}" + # self.clash_meta_all_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/meta/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mall_{domain}-{c['mode']}" + # self.clash_meta_foreign_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/meta/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mnormal_{domain}-{c['mode']}" + self.clash_meta_blocked_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/meta/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mlite_{domain}-{c['mode']}" @app.input(AppInSchema, arg_name='data', location="query") @app.output(AppSchema(many=True)) diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py index 2e5a560cb..1347f5a68 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py @@ -23,7 +23,7 @@ class ConfigSchema(Schema): class AllConfigsAPI(MethodView): - decorators = [app.auth_required(api_auth, roles=['user'])] + decorators = [hiddify.user_auth] @app.output(ConfigSchema(many=True)) def get(self): @@ -39,8 +39,8 @@ def create_item(name, domain, type, protocol, transport, security, link): return dto items = [] - base_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.user_uuid}/" - c = get_common_data(g.user_uuid, 'new') + base_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/" + c = get_common_data(g.account_uuid, 'new') # Add Auto items.append( diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py index a46627188..09630cf75 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py @@ -38,21 +38,21 @@ class UserInfoChangableSchema(Schema): class InfoAPI(MethodView): - decorators = [app.auth_required(api_auth, roles=['user'])] + decorators = [hiddify.user_auth] @app.output(ProfileSchema) def get(self): - c = get_common_data(g.user_uuid, 'new') + c = get_common_data(g.account_uuid, 'new') dto = ProfileSchema() # user is exist for sure dto.profile_title = c['user'].name - dto.profile_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.user_uuid}/#{g.user.name}" - dto.profile_usage_current = g.user.current_usage_GB - dto.profile_usage_total = g.user.usage_limit_GB - dto.profile_remaining_days = g.user.remaining_days - dto.profile_reset_days = days_to_reset(g.user) - dto.telegram_bot_url = f"https://t.me/{c['bot'].username}?start={g.user_uuid}" if c['bot'] else "" + dto.profile_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/#{g.account.name}" + dto.profile_usage_current = g.account.current_usage_GB + dto.profile_usage_total = g.account.usage_limit_GB + dto.profile_remaining_days = g.account.remaining_days + dto.profile_reset_days = days_to_reset(g.account) + dto.telegram_bot_url = f"https://t.me/{c['bot'].username}?start={g.account_uuid}" if c['bot'] else "" dto.telegram_id = c['user'].telegram_id dto.admin_message_html = hconfig(ConfigEnum.branding_freetext) dto.admin_message_url = hconfig(ConfigEnum.branding_site) @@ -70,13 +70,13 @@ def patch(self, data): except: return {'message': 'The telegram id field is invalid'} - user = get_user_by_uuid(g.user_uuid) + user = get_user_by_uuid(g.account_uuid) if user.telegram_id != tg_id: user.telegram_id = tg_id db.session.commit() if data['language']: - user = get_user_by_uuid(g.user_uuid) + user = get_user_by_uuid(g.account_uuid) if user.lang != data['language']: user.lang = data['language'] db.session.commit() diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py index 32341b97f..e23a84f12 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py @@ -19,15 +19,15 @@ class MtproxySchema(Schema): class MTProxiesAPI(MethodView): - decorators = [app.auth_required(api_auth, roles=['user'])] + decorators = [hiddify.user_auth] @app.output(MtproxySchema(many=True)) def get(self): # check mtproxie is enable - if not hconfig(ConfigEnum.telegram_enable, g.user_uuid): + if not hconfig(ConfigEnum.telegram_enable, g.account_uuid): abort(status_code=404, message="Telegram mtproxy is not enable") # get domains - c = get_common_data(g.user_uuid, 'new') + c = get_common_data(g.account_uuid, 'new') dtos = [] # TODO: Remove duplicated domains mapped to a same ipv4 and v6 for d in c['domains']: diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py index 7acb3a62f..bff7df960 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py @@ -19,11 +19,11 @@ class ShortSchema(Schema): class ShortAPI(MethodView): - decorators = [app.auth_required(api_auth, roles=['user'])] + decorators = [hiddify.user_auth] @app.output(ShortSchema) def get(self): - short, expire_in = hiddify.add_short_link("/"+hconfig(ConfigEnum.proxy_path)+"/"+str(g.user_uuid)+"/") + short, expire_in = hiddify.add_short_link("/"+hconfig(ConfigEnum.proxy_path)+"/"+str(g.account_uuid)+"/") full_url = f"https://{urlparse(request.base_url).hostname}/{short}" dto = ShortSchema() dto.full_url = full_url diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 512fdcffd..344a28531 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -1,16 +1,21 @@ -from typing import Tuple, Union +import subprocess +import psutil +from typing import Tuple from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import x25519 +from flask import abort, g, jsonify from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ +from wtforms.validators import ValidationError + +from datetime import timedelta +from babel.dates import format_timedelta as babel_format_timedelta from hiddifypanel.cache import cache from hiddifypanel.models import * from hiddifypanel.panel.database import db from hiddifypanel.hutils.utils import * from hiddifypanel.Events import domain_changed -from wtforms.validators import Regexp, ValidationError -from datetime import datetime, timedelta from hiddifypanel import hutils from hiddifypanel.panel.run_commander import commander, Command @@ -56,8 +61,8 @@ def add_short_link_imp(link: str, period_min: int = 5) -> Tuple[str, datetime]: def get_admin_path(): proxy_path = hconfig(ConfigEnum.proxy_path) - admin_secret = g.admin.uuid or get_super_admin_secret() - return (f"/{proxy_path}/{admin_secret}/admin/") + # admin_secret = g.account.uuid or get_super_admin_secret() + return (f"/{proxy_path}/admin/") def exec_command(cmd, cwd=None): @@ -69,11 +74,11 @@ def exec_command(cmd, cwd=None): def user_auth(function): def wrapper(*args, **kwargs): - if g.user_uuid == None: + if g.account_uuid == None: return jsonify({"error": "auth failed"}) - if not g.user: + if not g.account: return jsonify({"error": "user not found"}) - if g.admin and g.is_admin: + if g.account and g.is_admin: return jsonify({"error": "admin can not access user page. add /admin/ to your url"}) return function() @@ -82,9 +87,9 @@ def wrapper(*args, **kwargs): def super_admin(function): def wrapper(*args, **kwargs): - if g.admin.mode not in [AdminMode.super_admin]: + if g.account.mode not in [AdminMode.super_admin]: abort(403, __("Access Denied")) - return jsonify({"error": "auth failed"}) + # return jsonify({"error": "auth failed"}) return function(*args, **kwargs) return wrapper @@ -92,16 +97,16 @@ def wrapper(*args, **kwargs): def admin(function): def wrapper(*args, **kwargs): - if g.admin.mode not in [AdminMode.admin, AdminMode.super_admin]: - abort(_("Access Denied"), 403) - return jsonify({"error": "auth failed"}) + if g.account.mode not in [AdminMode.admin, AdminMode.super_admin]: + abort(403, __("Access Denied")) + # return jsonify({"error": "auth failed"}) return function(*args, **kwargs) return wrapper def abs_url(path): - return f"/{g.proxy_path}/{g.user_uuid}/{path}" + return f"/{g.proxy_path}/{g.account_uuid}/{path}" def asset_url(path): @@ -402,7 +407,7 @@ def set_db_from_json(json_data, override_child_id=None, set_users=True, set_doma u.parent_admin_id = owner.id # for u in User.query.all(): # if u.added_by in uuids_without_parent: - # u.added_by = g.admin.id + # u.added_by = g.account.id db.session.commit() diff --git a/hiddifypanel/panel/user/link_maker.py b/hiddifypanel/panel/user/link_maker.py index eabebe6a3..35bf9cd33 100644 --- a/hiddifypanel/panel/user/link_maker.py +++ b/hiddifypanel/panel/user/link_maker.py @@ -110,7 +110,7 @@ def is_tls(): 'port': port, 'server': cdn_forced_host, 'sni': domain_db.servernames if is_cdn and domain_db.servernames else domain, - 'uuid': str(g.user_uuid), + 'uuid': str(g.account_uuid), 'proto': proxy.proto, 'transport': proxy.transport, 'proxy_path': hconfigs[ConfigEnum.proxy_path], @@ -223,7 +223,7 @@ def is_tls(): base['alpn'] = 'http/1.1' return base if ProxyProto.ssh == proxy.proto: - base['private_key'] = g.user.ed25519_private_key + base['private_key'] = g.account.ed25519_private_key base['host_key'] = hiddify.get_hostkeys(False) # base['ssh_port'] = hconfig(ConfigEnum.ssh_server_port) return base diff --git a/hiddifypanel/panel/user/templates/clash_config copy.yml b/hiddifypanel/panel/user/templates/clash_config copy.yml index 4d98149fe..ff96e9b26 100644 --- a/hiddifypanel/panel/user/templates/clash_config copy.yml +++ b/hiddifypanel/panel/user/templates/clash_config copy.yml @@ -209,7 +209,7 @@ proxies: server: 127.0.0.1 port: 3005 cipher: chacha20-ietf-poly1305 - password: {{g.user_uuid}} + password: {{g.account_uuid}} udp_over_tcp: true % for d in domains % if hconfig(ConfigEnum.tuic_enable) and d.mode !="cdn" and meta_or_normal=='meta' diff --git a/hiddifypanel/panel/user/user.py b/hiddifypanel/panel/user/user.py index 78e22f75d..feb3055aa 100644 --- a/hiddifypanel/panel/user/user.py +++ b/hiddifypanel/panel/user/user.py @@ -25,22 +25,22 @@ class UserView(FlaskView): @route('/short') def short_link(self): short = hiddify.add_short_link( - "/"+hconfig(ConfigEnum.proxy_path)+"/"+g.user.uuid+"/") + "/"+hconfig(ConfigEnum.proxy_path)+"/"+g.account_uuid+"/") return f"
https://{urlparse(request.base_url).hostname}/{short}/

"+_("This link will expire in 5 minutes") @route('/info/') @route('/info') def info(self): - c = get_common_data(g.user_uuid, 'new') + c = get_common_data(g.account_uuid, 'new') data = { 'profile_title': c['profile_title'], - 'profile_url': f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.user_uuid}/#{g.user.name}", - 'profile_usage_current': g.user.current_usage_GB, - 'profile_usage_total': g.user.usage_limit_GB, - 'profile_remaining_days': g.user.remaining_days, - 'profile_reset_days': days_to_reset(g.user), - 'telegram_bot_url': f"https://t.me/{c['bot'].username}?start={g.user_uuid}" if c['bot'] else "", + 'profile_url': f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/#{g.account.name}", + 'profile_usage_current': g.account.current_usage_GB, + 'profile_usage_total': g.account.usage_limit_GB, + 'profile_remaining_days': g.account.remaining_days, + 'profile_reset_days': days_to_reset(g.account), + 'telegram_bot_url': f"https://t.me/{c['bot'].username}?start={g.account_uuid}" if c['bot'] else "", 'admin_message_html': hconfig(ConfigEnum.branding_freetext), 'admin_message_url': hconfig(ConfigEnum.branding_site), 'brand_title': hconfig(ConfigEnum.branding_title), @@ -55,7 +55,7 @@ def info(self): @route('/mtproxies') def mtproxies(self): # get domains - c = get_common_data(g.user_uuid, 'new') + c = get_common_data(g.account_uuid, 'new') mtproxies = [] # TODO: Remove duplicated domains mapped to a same ipv4 and v6 for d in c['domains']: @@ -84,8 +84,8 @@ def create_item(name, type, domain, protocol, transport, security, link): } items = [] - base_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.user_uuid}/" - c = get_common_data(g.user_uuid, 'new') + base_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/" + c = get_common_data(g.account_uuid, 'new') # Add Auto items.append( @@ -184,7 +184,7 @@ def test(self): # @route('/old/') # def index(self): - # c=get_common_data(g.user_uuid,mode="") + # c=get_common_data(g.account_uuid,mode="") # user_agent = user_agents.parse(request.user_agent.string) # return render_template('home/index.html',**c,ua=user_agent) @@ -192,7 +192,7 @@ def test(self): # @route('/multi') # def multi(self): - # c=get_common_data(g.user_uuid,mode="multi") + # c=get_common_data(g.account_uuid,mode="multi") # user_agent = user_agents.parse(request.user_agent.string) @@ -229,7 +229,7 @@ def get_proper_config(self): @ route('/auto') def auto_select(self): - c = get_common_data(g.user_uuid, mode="new") + c = get_common_data(g.account_uuid, mode="new") user_agent = user_agents.parse(request.user_agent.string) # return render_template('home/handle_smart.html', **c) return render_template('home/auto_page.html', **c, ua=user_agent) @@ -242,7 +242,7 @@ def new(self): if conf: return conf - c = get_common_data(g.user_uuid, mode="new") + c = get_common_data(g.account_uuid, mode="new") user_agent = user_agents.parse(request.user_agent.string) # return render_template('home/multi.html', **c, ua=user_agent) return render_template('new.html', **c, ua=user_agent) @@ -253,7 +253,7 @@ def clash_proxies(self, meta_or_normal="normal"): mode = request.args.get("mode") domain = request.args.get("domain", None) - c = get_common_data(g.user_uuid, mode, filter_domain=domain) + c = get_common_data(g.account_uuid, mode, filter_domain=domain) resp = Response(render_template('clash_proxies.yml', meta_or_normal=meta_or_normal, **c)) resp.mimetype = "text/plain" @@ -299,7 +299,7 @@ def report(self): def clash_config(self, meta_or_normal="normal", typ="all.yml"): mode = request.args.get("mode") - c = get_common_data(g.user_uuid, mode) + c = get_common_data(g.account_uuid, mode) hash_rnd = random.randint(0, 1000000) # hash(f'{c}') if request.method == 'HEAD': @@ -313,7 +313,7 @@ def clash_config(self, meta_or_normal="normal", typ="all.yml"): @ route('/full-singbox.json', methods=["GET", "HEAD"]) def full_singbox(self): mode = "new" # request.args.get("mode") - c = get_common_data(g.user_uuid, mode) + c = get_common_data(g.account_uuid, mode) # response.content_type = 'text/plain'; if request.method == 'HEAD': resp = "" @@ -328,7 +328,7 @@ def singbox(self): if not hconfig(ConfigEnum.ssh_server_enable): return "SSH server is disable in settings" mode = "new" # request.args.get("mode") - c = get_common_data(g.user_uuid, mode) + c = get_common_data(g.account_uuid, mode) # response.content_type = 'text/plain'; if request.method == 'HEAD': resp = "" @@ -341,7 +341,7 @@ def singbox(self): def all_configs(self, base64=False): mode = "new" # request.args.get("mode") base64 = base64 or request.args.get("base64", "").lower() == "true" - c = get_common_data(g.user_uuid, mode) + c = get_common_data(g.account_uuid, mode) # response.content_type = 'text/plain'; if request.method == 'HEAD': resp = "" @@ -385,7 +385,7 @@ def create_pwa_manifest(self): @ route("/offline.html") def offline(): - return f"Not Connected click for reload" + return f"Not Connected click for reload" def do_base_64(str): diff --git a/hiddifypanel/templates/lte-master.html b/hiddifypanel/templates/lte-master.html index 1a11c2c75..dc32c5890 100644 --- a/hiddifypanel/templates/lte-master.html +++ b/hiddifypanel/templates/lte-master.html @@ -36,7 +36,7 @@ {% block user_panel %} -Apply Changes +Apply Changes {% endblock %} {% block footer %}
From 5d42c91c4aef1619c3f2ebe6668a92c45b97f0a4 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 16 Dec 2023 21:43:34 +0330 Subject: [PATCH 019/112] add: role authentication for views --- hiddifypanel/panel/admin/Actions.py | 32 +++++++------------ hiddifypanel/panel/admin/ChildAdmin.py | 4 +++ hiddifypanel/panel/admin/ConfigAdmin.py | 4 ++- hiddifypanel/panel/admin/DomainAdmin.py | 4 +-- hiddifypanel/panel/admin/ProxyAdmin.py | 3 +- hiddifypanel/panel/admin/QuickSetup.py | 24 ++++++++++++-- .../panel/commercial/ParentDomainAdmin.py | 5 +-- .../restapi/v2/admin/admin_user_api.py | 2 +- .../restapi/v2/admin/admin_users_api.py | 2 +- .../commercial/restapi/v2/admin/users_api.py | 2 +- 10 files changed, 48 insertions(+), 34 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index 636a056fe..960e29453 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -17,13 +17,11 @@ class Actions(FlaskView): - # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def index(self): return render_template('index.html') # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) def reverselog(self, logfile): if logfile == None: return self.viewlogs() @@ -48,8 +46,7 @@ def reverselog(self, logfile): response.headers['Access-Control-Allow-Headers'] = 'Content-Type' return response - # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.admin def viewlogs(self): config_dir = app.config['HIDDIFY_CONFIG_PATH'] res = [] @@ -57,18 +54,17 @@ def viewlogs(self): res.append(f"{filename}") return Markup("
".join(res)) + @hiddify.super_admin @route('apply_configs', methods=['POST']) - @app.auth_required(basic_auth, roles=['super_admin']) def apply_configs(self): return self.reinstall(False) + @hiddify.super_admin @route('reset', methods=['POST']) - @app.auth_required(basic_auth, roles=['super_admin']) def reset(self): return self.reset2() - # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def reset2(self): status = self.status() # flash(_("rebooting system may takes time please wait"),'info') @@ -93,13 +89,13 @@ def reset2(self): time.sleep(1) return resp + @hiddify.super_admin @route('reinstall', methods=['POST']) - @app.auth_required(basic_auth, roles=['super_admin']) def reinstall(self, complete_install=True, domain_changed=False): return self.reinstall2(complete_install, domain_changed) # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def reinstall2(self, complete_install=True, domain_changed=False): if int(hconfig(ConfigEnum.db_version)) < 9: return ("Please update your panel before this action.") @@ -154,8 +150,7 @@ def reinstall2(self, complete_install=True, domain_changed=False): time.sleep(1) return resp - # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def change_reality_keys(self): key = hiddify.generate_x25519_keys() set_hconfig(ConfigEnum.reality_private_key, key['private_key']) @@ -163,8 +158,7 @@ def change_reality_keys(self): hiddify.flash_config_success(restart_mode='apply', domain_changed=False) return redirect(url_for('admin.SettingAdmin:index')) - # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def status(self): # hiddify.add_temporary_access() # configs=read_configs() @@ -188,14 +182,11 @@ def status(self): domains=get_domains() ) - # @hiddify.super_admin - @route('update', methods=['POST']) - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def update(self): return self.update2() - # @hiddify.super_admin def update2(self): hiddify.add_temporary_access() @@ -252,8 +243,7 @@ def get_some_random_reality_friendly_domain(self): return res+"" - # @hiddify.super_admin - @app.auth_required(basic_auth, roles=['super_admin']) + @hiddify.super_admin def update_usage(self): import json diff --git a/hiddifypanel/panel/admin/ChildAdmin.py b/hiddifypanel/panel/admin/ChildAdmin.py index b1db91516..4029e403c 100644 --- a/hiddifypanel/panel/admin/ChildAdmin.py +++ b/hiddifypanel/panel/admin/ChildAdmin.py @@ -106,3 +106,7 @@ def on_model_delete(self, model): if len(ParentDomain.query.all()) <= 1: raise ValidationError(f"at least one domain should exist") hiddify.flash_config_success(restart_mode='apply', domain_changed=True) + + @hiddify.admin + def is_accessible(self): + return True diff --git a/hiddifypanel/panel/admin/ConfigAdmin.py b/hiddifypanel/panel/admin/ConfigAdmin.py index be6583b1a..dcccf8faa 100644 --- a/hiddifypanel/panel/admin/ConfigAdmin.py +++ b/hiddifypanel/panel/admin/ConfigAdmin.py @@ -1,5 +1,6 @@ import re import uuid +from hiddifypanel.panel import hiddify from wtforms.validators import ValidationError @@ -22,8 +23,9 @@ class ConfigAdmin(AdminLTEModelView): }, } + @hiddify.admin def is_accessible(self): - return standalone_verify({AccountRole.super_admin, AccountRole.admin}) + return True @staticmethod def _is_valid_uuid(val: str, version: int | None = None): diff --git a/hiddifypanel/panel/admin/DomainAdmin.py b/hiddifypanel/panel/admin/DomainAdmin.py index 2d0342dab..4661d1cab 100644 --- a/hiddifypanel/panel/admin/DomainAdmin.py +++ b/hiddifypanel/panel/admin/DomainAdmin.py @@ -1,7 +1,6 @@ import ipaddress from hiddifypanel.models import * import re -from hiddifypanel.panel.authentication import AccountRole, standalone_verify from flask import Markup from flask import flash from flask_babelex import gettext as __ @@ -283,8 +282,9 @@ def after_model_change(self, form, model, is_created): # run get_cert.sh commander(Command.get_cert, domain=model.domain) + @hiddify.admin def is_accessible(self): - return standalone_verify([AccountRole.super_admin, AccountRole.admin]) + return True # def form_choices(self, field, *args, **kwargs): # if field.type == "Enum": diff --git a/hiddifypanel/panel/admin/ProxyAdmin.py b/hiddifypanel/panel/admin/ProxyAdmin.py index ad949c696..300edc7c9 100644 --- a/hiddifypanel/panel/admin/ProxyAdmin.py +++ b/hiddifypanel/panel/admin/ProxyAdmin.py @@ -14,11 +14,10 @@ from flask_classful import FlaskView from hiddifypanel.panel.hiddify import flash from hiddifypanel.panel import hiddify -from hiddifypanel.panel.authentication import basic_auth class ProxyAdmin(FlaskView): - decorators = [app.auth_required(basic_auth, roles=['super_admin', 'admin'])] + decorators = [hiddify.super_admin] def index(self): return render_template('proxy.html', global_config_form=get_global_config_form(), detailed_config_form=get_all_proxy_form()) diff --git a/hiddifypanel/panel/admin/QuickSetup.py b/hiddifypanel/panel/admin/QuickSetup.py index 3458df54d..0b3255e8c 100644 --- a/hiddifypanel/panel/admin/QuickSetup.py +++ b/hiddifypanel/panel/admin/QuickSetup.py @@ -24,9 +24,16 @@ class QuickSetup(FlaskView): + decorators = [hiddify.admin] def index(self): - return render_template('quick_setup.html', lang_form=get_lang_form(), form=get_quick_setup_form(), ipv4=hutils.ip.get_ip(4), ipv6=hutils.ip.get_ip(6), admin_link=admin_link(), show_domain_info=True) + return render_template( + 'quick_setup.html', lang_form=get_lang_form(), + form=get_quick_setup_form(), + ipv4=hutils.ip.get_ip(4), + ipv6=hutils.ip.get_ip(6), + admin_link=admin_link(), + show_domain_info=True) def post(self): set_hconfig(ConfigEnum.first_setup, False) @@ -48,7 +55,13 @@ def post(self): else: flash((_('quicksetup.setlang.error')), 'danger') - return render_template('quick_setup.html', form=get_quick_setup_form(True), lang_form=get_lang_form(), admin_link=admin_link(), ipv4=hutils.ip.get_ip(4), ipv6=hutils.ip.get_ip(6), show_domain_info=False) + return render_template( + 'quick_setup.html', form=get_quick_setup_form(True), + lang_form=get_lang_form(), + admin_link=admin_link(), + ipv4=hutils.ip.get_ip(4), + ipv6=hutils.ip.get_ip(6), + show_domain_info=False) if quick_form.validate_on_submit(): sslip_dm = Domain.query.filter(Domain.domain == f'{hutils.ip.get_ip(4)}.sslip.io').delete() @@ -73,7 +86,12 @@ def post(self): return action.reinstall(domain_changed=True) else: flash(_('config.validation-error'), 'danger') - return render_template('quick_setup.html', form=quick_form, lang_form=get_lang_form(True), ipv4=hutils.ip.get_ip(4), ipv6=hutils.ip.get_ip(6), admin_link=admin_link(), show_domain_info=False) + return render_template( + 'quick_setup.html', form=quick_form, lang_form=get_lang_form(True), + ipv4=hutils.ip.get_ip(4), + ipv6=hutils.ip.get_ip(6), + admin_link=admin_link(), + show_domain_info=False) def get_lang_form(empty=False): diff --git a/hiddifypanel/panel/commercial/ParentDomainAdmin.py b/hiddifypanel/panel/commercial/ParentDomainAdmin.py index f776b1e3c..bec1cae39 100644 --- a/hiddifypanel/panel/commercial/ParentDomainAdmin.py +++ b/hiddifypanel/panel/commercial/ParentDomainAdmin.py @@ -63,7 +63,8 @@ class ParentDomainAdmin(AdminLTEModelView): def _domain_admin_link(view, context, model, name): admin_link = f'https://{model.domain}{hiddify.get_admin_path()}' - return Markup(f'') + return Markup(f'') def _domain_ip(view, context, model, name): dip = hutils.ip.get_domain_ip(model.domain) @@ -121,4 +122,4 @@ def on_model_delete(self, model): hiddify.flash_config_success(restart_mode='apply', domain_changed=True) def is_accessible(self): - return g.admin.mode in [AdminMode.admin, AdminMode.super_admin] + return g.account.mode in [AdminMode.admin, AdminMode.super_admin] diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py index 02dcfe661..0fcdc6248 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py @@ -27,7 +27,7 @@ class AdminSchema(Schema): class AdminUserApi(MethodView): - decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] + decorators = [hiddify.admin] @app.output(AdminSchema) def get(self, uuid): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py index 77c551077..a61df7fec 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py @@ -8,7 +8,7 @@ class AdminUsersApi(MethodView): - decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] + decorators = [hiddify.admin] @app.output(AdminSchema(many=True)) def get(self): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py index 76e8ddbdc..1087d4b7b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py @@ -10,7 +10,7 @@ class UsersApi(MethodView): - decorators = [app.auth_required(api_auth, roles=['super_admin', 'admin'])] + decorators = [hiddify.admin] @app.output(UserSchema(many=True)) def get(self): From 47e47936992ed41e338ad2661d4f42d41431f7fa Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 16 Dec 2023 21:44:14 +0330 Subject: [PATCH 020/112] add: redirect_to_user.html --- .../panel/user/templates/home/home.html | 239 +++++++++-------- .../panel/user/templates/home/multi.html | 243 +++++++++--------- .../user/templates/redirect_to_user.html | 4 + hiddifypanel/templates/admin-layout.html | 32 ++- 4 files changed, 253 insertions(+), 265 deletions(-) create mode 100644 hiddifypanel/panel/user/templates/redirect_to_user.html diff --git a/hiddifypanel/panel/user/templates/home/home.html b/hiddifypanel/panel/user/templates/home/home.html index 86837106b..fb24267ac 100644 --- a/hiddifypanel/panel/user/templates/home/home.html +++ b/hiddifypanel/panel/user/templates/home/home.html @@ -7,24 +7,24 @@
- - -

- %if g.darkmode - - - - %else - - - - %endif - {{hconfig(ConfigEnum.branding_title) or _('user.home.title')}}:
{{_('Welcome %(user)s',user=user.name if user.name!="default" else "")}}
-

- - + + +

+ %if g.darkmode + + + + %else + + + + %endif + {{hconfig(ConfigEnum.branding_title) or _('user.home.title')}}:
{{_('Welcome %(user)s',user=user.name if user.name!="default" else "")}}
+

+ +
-
+
{%endblock%} {% block body %} @@ -47,35 +47,32 @@

-
+
{{_('user.home.select os')}}
{% include 'home/telegram.html' %}
-
+
{% include 'home/android.html' %}
-
+
{% include 'home/ios.html' %}
- - + -
-
-
-
-

{{_('QR code')}}

-

{{_('user.home.qr-code.description')}}

-
-
- -
-
- +
+
+
+
+

{{_('QR code')}}

+

{{_('user.home.qr-code.description')}}

+
+
+ +
+
+ +
-
-
+
@@ -252,7 +245,7 @@

{{_('QR code')}}

{{ super() }} -%if has_auto_cdn and hconfig(ConfigEnum.country)!="other" and country.lower()!=hconfig(ConfigEnum.country) +%if has_auto_cdn and hconfig(ConfigEnum.country)!="other" and country.lower()!=hconfig(ConfigEnum.country) {% macro reload() -%} - {%- endmacro -%} @@ -398,4 +391,4 @@

{{_('QR code')}}

%endif -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/hiddifypanel/panel/user/templates/home/multi.html b/hiddifypanel/panel/user/templates/home/multi.html index bc3ec4c7b..2cc2c5a90 100644 --- a/hiddifypanel/panel/user/templates/home/multi.html +++ b/hiddifypanel/panel/user/templates/home/multi.html @@ -7,24 +7,24 @@
- - -

- %if g.darkmode - - - - %else - - - - %endif - {{hconfig(ConfigEnum.branding_title) or _('user.home.title')}}:
{{_('Welcome %(user)s',user=user.name if user.name!="default" else "")}}
-

- - + + +

+ %if g.darkmode + + + + %else + + + + %endif + {{hconfig(ConfigEnum.branding_title) or _('user.home.title')}}:
{{_('Welcome %(user)s',user=user.name if user.name!="default" else "")}}
+

+ +
-
+
{%endblock%} {% block body %} @@ -47,37 +47,33 @@

-
+
{{_('user.home.select os')}}
{% include 'home/telegram.html' %}
-
+
{% include 'home/android.html' %}
-
+
{% include 'home/ios.html' %}
-
+
{% include "home/windows.html" %}
@@ -101,11 +97,9 @@

@@ -113,7 +107,7 @@

{{_('user.home.select tool')}}
- +
{% include 'home/speedtest.html' %}
@@ -128,30 +122,29 @@

- + -
-
-
-
-

{{_('QR code')}}

-

{{_('user.home.qr-code.description')}}

-
-
- -
-
- +
+
+
+
+

{{_('QR code')}}

+

{{_('user.home.qr-code.description')}}

+
+
+ +
+
+ +
-
-
+
@@ -254,7 +247,7 @@

{{_('QR code')}}

{{ super() }} -%if has_auto_cdn and hconfig(ConfigEnum.country)!="other" and country.lower()!=hconfig(ConfigEnum.country) +%if has_auto_cdn and hconfig(ConfigEnum.country)!="other" and country.lower()!=hconfig(ConfigEnum.country) {% macro reload() -%} - {%- endmacro -%} @@ -398,4 +391,4 @@

{{_('QR code')}}

%endif -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/hiddifypanel/panel/user/templates/redirect_to_user.html b/hiddifypanel/panel/user/templates/redirect_to_user.html new file mode 100644 index 000000000..90067b0a4 --- /dev/null +++ b/hiddifypanel/panel/user/templates/redirect_to_user.html @@ -0,0 +1,4 @@ +

+ User page route is redirected to: {{user_link}} +
+

\ No newline at end of file diff --git a/hiddifypanel/templates/admin-layout.html b/hiddifypanel/templates/admin-layout.html index 38812afc5..d69ebba3e 100644 --- a/hiddifypanel/templates/admin-layout.html +++ b/hiddifypanel/templates/admin-layout.html @@ -24,7 +24,7 @@ GitHub Repo stars - +
@@ -62,11 +62,11 @@ % if False and hconfig(ConfigEnum.is_parent)
- + %endif - + @@ -93,7 +93,7 @@ - % if g.admin.mode!="agent" + % if g.account.mode!="agent" {% set settings_active = False if request.endpoint and ("user" in request.endpoint or "Actions:" in request.endpoint or "Dashboard" in request.endpoint) else True %} %endif +
-
+
\ No newline at end of file diff --git a/hiddifypanel/panel/cli.py b/hiddifypanel/panel/cli.py index da6dc24a9..f2f20ce6f 100644 --- a/hiddifypanel/panel/cli.py +++ b/hiddifypanel/panel/cli.py @@ -65,7 +65,7 @@ def all_configs(): configs['hconfigs']['first_setup'] = def_user != None and len(sslip_domains) > 0 - path = f'/{hconfig(ConfigEnum.proxy_path)}/{get_super_admin_secret()}/admin/' + path = f'/{hconfig(ConfigEnum.proxy_path)}/admin/' server_ip = hutils.ip.get_ip(4) configs['admin_path'] = path @@ -93,18 +93,17 @@ def test(): def admin_links(): proxy_path = hconfig(ConfigEnum.proxy_path) - admin_secret = get_super_admin_secret() server_ip = hutils.ip.get_ip(4) - admin_links = f"Not Secure (do not use it- only if others not work):\n http://{server_ip}/{proxy_path}/{admin_secret}/admin/\n" + admin_links = f"Not Secure (do not use it- only if others not work):\n http://{server_ip}/{proxy_path}/admin/\n" domains = get_panel_domains() admin_links += f"Secure:\n" if not any([d for d in domains if 'sslip.io' not in d.domain]): - admin_links += f" (not signed) https://{server_ip}/{proxy_path}/{admin_secret}/admin/\n" + admin_links += f" (not signed) https://{server_ip}/{proxy_path}/admin/\n" # domains=[*domains,f'{server_ip}.sslip.io'] for d in domains: - admin_links += f" https://{d.domain}/{proxy_path}/{admin_secret}/admin/\n" + admin_links += f" https://{d.domain}/{proxy_path}/admin/\n" print(admin_links) return admin_links @@ -118,8 +117,7 @@ def admin_path(): db.session.commit() admin = Admin.query.filter(Admin.mode == AdminMode.super_admin).first() - admin_secret = admin.uuid - print(f"/{proxy_path}/{admin_secret}/admin/") + print(f"/{proxy_path}/admin/") def hysteria_domain_port(): diff --git a/hiddifypanel/panel/commercial/templates/parent_dash.html b/hiddifypanel/panel/commercial/templates/parent_dash.html index 448d4abf0..b8ad2ad91 100644 --- a/hiddifypanel/panel/commercial/templates/parent_dash.html +++ b/hiddifypanel/panel/commercial/templates/parent_dash.html @@ -7,9 +7,8 @@ {% macro admin_btn(child,domain) -%} {%- endmacro -%} @@ -23,29 +22,36 @@

{{_('Childs')}}

{{_("No children!")}} %else - - - + + + + + + + + %for child in childs - - + + - + + + %endfor
{{_('Status')}}{{_("Domain")}}
{{_('Status')}}{{_("Domain")}}
{{child.id}} +
{{child.id}} % if not child.is_active
- - + +
 {{icon('solid','check') if child.is_active else icon('solid','triangle-exclamation')}} {{icon('solid','check') if child.is_active else icon('solid','triangle-exclamation')}} %for d in child.domains % if d.mode !="fake" - {{admin_btn(child,d)}} + {{admin_btn(child,d)}} %endif %endfor -
%endif diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 344a28531..ac7f8c7bb 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -231,9 +231,9 @@ def get_user_link(uuid, domain, mode='', username=''): if "*" in d: d = d.replace("*", get_random_string(5, 15)) - link = f"https://{d}/{proxy_path}/{uuid}/#{username}" + link = f"https://{d}/{proxy_path}/#{username}" if mode == "admin": - link = f"https://{d}/{proxy_path}/{uuid}/admin/#{username}" + link = f"https://{d}/{proxy_path}/admin/#{username}" link_multi = f"{link}multi" # if mode == 'new': # link = f"{link}new" From fb453fbb3b93d69f4c56f2f1b668c65c82054a67 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sun, 17 Dec 2023 15:35:28 +0330 Subject: [PATCH 027/112] add: auth for apps api --- hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py index a375e5342..4e41b8946 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py @@ -88,6 +88,7 @@ def __init__(self) -> None: @app.input(AppInSchema, arg_name='data', location="query") @app.output(AppSchema(many=True)) + @hiddify.user_auth def get(self, data): # parse user agent if data['platform'] == Platform.auto: From b840da4a1769dcc0074a758e1219207e1be67a27 Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 18 Dec 2023 13:23:13 +0330 Subject: [PATCH 028/112] fix: creating empty session(in redis) for every request --- hiddifypanel/base.py | 3 +++ hiddifypanel/panel/authentication.py | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/hiddifypanel/base.py b/hiddifypanel/base.py index 58135c961..4b9e2569d 100644 --- a/hiddifypanel/base.py +++ b/hiddifypanel/base.py @@ -19,6 +19,7 @@ def create_app(cli=False, **config): app = APIFlask(__name__, static_url_path="//static/", instance_relative_config=True, version='2.0.0', title="Hiddify API", openapi_blueprint_url_prefix="//api", docs_ui='elements', json_errors=False, enable_openapi=True) + # app = Flask(__name__, static_url_path="//static/", instance_relative_config=True) app.wsgi_app = ProxyFix( app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 @@ -52,6 +53,8 @@ def create_app(cli=False, **config): # setup flask server-side session app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_REDIS'] = redis_client + app.config['SESSION_PERMANENT'] = False + app.config['SESSION_COOKIE_DOMAIN'] = False app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=7) Session(app) diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index 7edf111a9..2e4d5f968 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -41,11 +41,12 @@ def verify_api_auth_token(token) -> User | AdminUser | None: def set_admin_authentication_in_session(admin: AdminUser) -> None: - session['account'] = { - 'uuid': admin.uuid, - 'role': get_account_role(admin), - # 'username': res.username, - } + if not session.get('account'): + session['account'] = { + 'uuid': admin.uuid, + 'role': get_account_role(admin), + # 'username': res.username, + } def set_user_authentication_in_session(user: User) -> None: From 2867a1a441f1d9e9e6637fbac8e479243a09275c Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 18 Dec 2023 20:22:01 +0330 Subject: [PATCH 029/112] fix: bug --- hiddifypanel/panel/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index 2e4d5f968..acb69f90d 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -115,7 +115,7 @@ def standalone_api_auth_verify(): auth = api_auth.get_auth() try: if hasattr(auth, 'token'): - account = verify_api_auth_token(auth.token, check_session=False) + account = verify_api_auth_token(auth.token) if account: return account except AttributeError: From 6833842027a03035414585b9497f2806ae4d8617 Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 18 Dec 2023 20:22:23 +0330 Subject: [PATCH 030/112] add: admin log api --- .../commercial/restapi/v2/admin/__init__.py | 2 ++ .../restapi/v2/admin/admin_log_api.py | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py b/hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py index 56556b086..4a5c9aa32 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/__init__.py @@ -10,10 +10,12 @@ def init_app(app): from .server_status_api import AdminServerStatusApi from .admin_user_api import AdminUserApi from .admin_users_api import AdminUsersApi + from .admin_log_api import AdminLogApi bp.add_url_rule('/me/', view_func=AdminInfoApi) bp.add_url_rule('/server_status/', view_func=AdminServerStatusApi) bp.add_url_rule('/admin_user/', view_func=AdminUserApi) bp.add_url_rule('/admin_user/', view_func=AdminUsersApi) + bp.add_url_rule('/log/', view_func=AdminLogApi) from .user_api import UserApi from .users_api import UsersApi diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py new file mode 100644 index 000000000..4ab22f15d --- /dev/null +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py @@ -0,0 +1,33 @@ +from apiflask import Schema, fields, abort +from flask.views import MethodView +from hiddifypanel.panel import hiddify +from flask import current_app as app +from flask_cors import cross_origin +import os +from ansi2html import Ansi2HTMLConverter + + +class AdminLogfileSchema(Schema): + file = fields.String(description="The log file name", required=True) + + +class AdminLogApi(MethodView): + decorators = [hiddify.super_admin] + + @app.input(AdminLogfileSchema, arg_name="data", location='form') + @app.output(fields.String(description="The html of the log")) + @cross_origin() + def post(self, data): + file = data.get('file') or abort(400, "Parameter issue: 'file'") + file_path = f"{app.config['HIDDIFY_CONFIG_PATH']}/log/system/{file}" + if not os.path.exists(file_path): + return abort(404, "Invalid log file") + logs = '' + with open(file_path, 'r') as f: + lines = [line for line in f] + logs = "".join(lines) + + conv = Ansi2HTMLConverter() + html_log = f'
{conv.convert(logs)}
' + + return html_log From d8af20f0299422c4adf3a7560089924e6f3d4f0e Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 18 Dec 2023 20:40:32 +0330 Subject: [PATCH 031/112] add: CORS for javascript calls --- hiddifypanel/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hiddifypanel/base.py b/hiddifypanel/base.py index 4b9e2569d..693ee2135 100644 --- a/hiddifypanel/base.py +++ b/hiddifypanel/base.py @@ -3,6 +3,7 @@ from flask import request, g from flask_babelex import Babel from flask_session import Session +from flask_cors import CORS import datetime from dotenv import dotenv_values @@ -19,7 +20,8 @@ def create_app(cli=False, **config): app = APIFlask(__name__, static_url_path="//static/", instance_relative_config=True, version='2.0.0', title="Hiddify API", openapi_blueprint_url_prefix="//api", docs_ui='elements', json_errors=False, enable_openapi=True) - + # setup CORS for javascript calls + cors = CORS(app) # app = Flask(__name__, static_url_path="//static/", instance_relative_config=True) app.wsgi_app = ProxyFix( app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 From 00ffecefa188d362a7c2a90e5809e17b3d406dab Mon Sep 17 00:00:00 2001 From: Sarina Date: Mon, 18 Dec 2023 20:42:00 +0330 Subject: [PATCH 032/112] fix: status js log file call to use api --- hiddifypanel/panel/admin/Actions.py | 17 +- hiddifypanel/panel/admin/templates/index.html | 450 +++++++++--------- .../panel/admin/templates/result.html | 145 +++--- hiddifypanel/panel/common.py | 12 +- 4 files changed, 312 insertions(+), 312 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index b9f644a41..cda68611c 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -4,15 +4,15 @@ import urllib.request from flask_classful import FlaskView, route -from flask import render_template, request, Markup, url_for, make_response, redirect +from flask import render_template, request, Markup, url_for, make_response, redirect, g from flask import current_app as app +# from flask_cors import cross_origin from hiddifypanel import hutils from hiddifypanel.models import * from hiddifypanel.panel import hiddify, usage from hiddifypanel.panel.run_commander import commander, Command from hiddifypanel.panel.hiddify import flash -from hiddifypanel.panel.authentication import basic_auth class Actions(FlaskView): @@ -22,6 +22,7 @@ def index(self): return render_template('index.html') # @hiddify.super_admin + # @cross_origin() def reverselog(self, logfile): if logfile == None: return self.viewlogs() @@ -55,7 +56,7 @@ def viewlogs(self): return Markup("
".join(res)) @hiddify.super_admin - @route('apply_configs', methods=['POST']) + @route('apply_configs', methods=['POST', 'GET']) def apply_configs(self): return self.reinstall(False) @@ -170,16 +171,16 @@ def status(self): # run status.sh commander(Command.status) - - from urllib.parse import urlparse - domain = urlparse(request.base_url).hostname + # TODO: send real apikey + api_key = g.account_uuid return render_template("result.html", out_type="info", out_msg=_("see the log in the bellow screen"), log_path=get_logpath(f"status.log"), - # log_path=f"status.log", show_success=False, - domains=get_domains() + domains=get_domains(), + api_key=api_key, + proxy_path=g.proxy_path, ) @route('update', methods=['POST']) diff --git a/hiddifypanel/panel/admin/templates/index.html b/hiddifypanel/panel/admin/templates/index.html index 7a933b788..f9bf6b885 100644 --- a/hiddifypanel/panel/admin/templates/index.html +++ b/hiddifypanel/panel/admin/templates/index.html @@ -7,33 +7,31 @@ {% block body %} {% macro info_box(id,icon,title,number,percentage,description,coloring=False,color_class='secondary') -%}
- 90 else ("warning" if percentage>75 else ""))) if coloring else ""}} {%if "disk"==id %} d-none d-md-flex {%endif%}"> + 90 else ("warning" if percentage>75 else ""))) if coloring else ""}} {%if "disk"==id %} d-none d-md-flex {%endif%}">
{{title}} - {{number}} + {{number}}
-
+
@@ -48,107 +46,105 @@ {% macro info_box2(id,icon,title,number,percentage,description,coloring=False) -%}
-
90 else ("warning" if percentage>75 else ""))) if coloring else ""}}"> +
90 else ("warning" if percentage>75 else ""))) if coloring else ""}}">
-

{{title}}

- {{number}} -
-
-
+

{{title}}

+ {{number}} +
+
- - {{description}} - +
+ + {{description}} +
- +
{%- endmacro -%} -
+
%set onlines=usage_history['m5']['online'] %set total_users=usage_history['total']['users'] - {{info_box("today","fa-solid fa-calendar", _("Today Usage"), - ((usage_history['today']['usage']/1024**3)|round(1)) ~ " GB", - usage_history['today']['online']/((1,total_users)|max)*100, - _("Online Users") ~": "~ usage_history['today']['online'] ~ " / "~ total_users, - color_class="bg-h-purple" + {{info_box("today","fa-solid fa-calendar", _("Today Usage"), + ((usage_history['today']['usage']/1024**3)|round(1)) ~ " GB", + usage_history['today']['online']/((1,total_users)|max)*100, + _("Online Users") ~": "~ usage_history['today']['online'] ~ " / "~ total_users, + color_class="bg-h-purple" )}} - {{info_box("yesterday","fa-solid fa-calendar-day", _("Yesterday Usage"), - ((usage_history['yesterday']['usage']/1024**3)|round(1)) ~ " GB", - usage_history['yesterday']['online']/((1,total_users)|max)*100, - _("Online Users") ~": "~ usage_history['yesterday']['online'] ~ " / "~ total_users, - color_class="bg-h-blue" + {{info_box("yesterday","fa-solid fa-calendar-day", _("Yesterday Usage"), + ((usage_history['yesterday']['usage']/1024**3)|round(1)) ~ " GB", + usage_history['yesterday']['online']/((1,total_users)|max)*100, + _("Online Users") ~": "~ usage_history['yesterday']['online'] ~ " / "~ total_users, + color_class="bg-h-blue" )}} - {{info_box("last_30_days","fa-solid fa-calendar-days", _("Month Usage"), - ((usage_history['last_30_days']['usage']/1024**3)|round(1)) ~ " GB", - usage_history['last_30_days']['online']/((1,total_users)|max)*100, - _("Online Users") ~": "~ usage_history['last_30_days']['online'] ~ " / "~ total_users, - color_class="bg-h-green" + {{info_box("last_30_days","fa-solid fa-calendar-days", _("Month Usage"), + ((usage_history['last_30_days']['usage']/1024**3)|round(1)) ~ " GB", + usage_history['last_30_days']['online']/((1,total_users)|max)*100, + _("Online Users") ~": "~ usage_history['last_30_days']['online'] ~ " / "~ total_users, + color_class="bg-h-green" )}} - {{info_box("total","fa-solid fa-chart-pie", _("Total Usage"), - ((usage_history['total']['usage']/1024**3)|round(1)) ~ " GB", - usage_history['total']['online']/((1,total_users)|max)*100, - _("Online Users") ~": "~ usage_history['total']['online'] ~ " / "~ total_users, - color_class="bg-h-orange" + {{info_box("total","fa-solid fa-chart-pie", _("Total Usage"), + ((usage_history['total']['usage']/1024**3)|round(1)) ~ " GB", + usage_history['total']['online']/((1,total_users)|max)*100, + _("Online Users") ~": "~ usage_history['total']['online'] ~ " / "~ total_users, + color_class="bg-h-orange" )}} - - - - + + + + {{info_box("online","fa-solid fa-users", - _("Online Users"), - onlines ~ " / "~ total_users, - onlines/((1,total_users)|max)*100, - _('In 5 minutes'), - color_class="bg-h-turquoise" + _("Online Users"), + onlines ~ " / "~ total_users, + onlines/((1,total_users)|max)*100, + _('In 5 minutes'), + color_class="bg-h-turquoise" + )}} + {{info_box("network","fa-solid fa-network-wired", _("Network"), + (' Mb/s    Mb/s')|safe, + 0, + (stats['system']['net_sent_cumulative_GB']|round(1)) ~ "GB " ~_("From Last Restart"), + color_class="bg-h-red" )}} - {{info_box("network","fa-solid fa-network-wired", _("Network"), - (' Mb/s    Mb/s')|safe, - 0, - (stats['system']['net_sent_cumulative_GB']|round(1)) ~ "GB " ~_("From Last Restart"), - color_class="bg-h-red" -)}} - - {{info_box("cpu","fa-solid fa-microchip", _("CPU %(cores)s Cores", cores=stats['system']['num_cpus']), - - ((stats['system']['cpu_percent'])|int) ~ " % " , - (stats['system']['cpu_percent'])|int, - (' '|safe) ~ stats['top5']['cpu'][0][0] ~ " ‏"|safe ~ ((stats['top5']['cpu'][0][1])|int) ~ "% ‏"|safe~ - (' '|safe) ~ stats['top5']['cpu'][1][0] ~ " ‏"|safe ~ ((stats['top5']['cpu'][1][1])|int) ~ "% ‏"|safe~ - (' '|safe) ~ stats['top5']['cpu'][2][0] ~ " ‏"|safe ~ ((stats['top5']['cpu'][2][1])|int) ~ "%", - coloring=True, - color_class="bg-h-grey" + {{info_box("cpu","fa-solid fa-microchip", _("CPU %(cores)s Cores", cores=stats['system']['num_cpus']), + + ((stats['system']['cpu_percent'])|int) ~ " % " , + (stats['system']['cpu_percent'])|int, + (' '|safe) ~ stats['top5']['cpu'][0][0] ~ " ‏"|safe ~ ((stats['top5']['cpu'][0][1])|int) ~ "% ‏"|safe~ + (' '|safe) ~ stats['top5']['cpu'][1][0] ~ " ‏"|safe ~ ((stats['top5']['cpu'][1][1])|int) ~ "% ‏"|safe~ + (' '|safe) ~ stats['top5']['cpu'][2][0] ~ " ‏"|safe ~ ((stats['top5']['cpu'][2][1])|int) ~ "%", + coloring=True, + color_class="bg-h-grey" )}} - {{info_box("ram","fa-solid fa-memory", _("RAM"), - (stats['system']['ram_used']|round(3)) ~ " / " ~ (stats['system']['ram_total']|round(3)) ~ "GB (" ~((stats['system']['ram_used']*100/stats['system']['ram_total'])|int)~" %)", - stats['system']['ram_used']*100/stats['system']['ram_total'], - (' '|safe) ~ stats['top5']['memory'][0][0] ~ " ‏"|safe ~ ((stats['top5']['memory'][0][1]*100/stats['system']['ram_total'])|int) ~ "% ‏"|safe~ - (' '|safe) ~ stats['top5']['memory'][1][0] ~ " ‏"|safe ~ ((stats['top5']['memory'][1][1]*100/stats['system']['ram_total'])|int) ~ "% ‏"|safe~ - (' '|safe) ~ stats['top5']['memory'][2][0] ~ " ‏"|safe ~ ((stats['top5']['memory'][2][1]*100/stats['system']['ram_total'])|int) ~ "% ", - coloring=True, - color_class="bg-h-pink" + {{info_box("ram","fa-solid fa-memory", _("RAM"), + (stats['system']['ram_used']|round(3)) ~ " / " ~ (stats['system']['ram_total']|round(3)) ~ "GB (" ~((stats['system']['ram_used']*100/stats['system']['ram_total'])|int)~" %)", + stats['system']['ram_used']*100/stats['system']['ram_total'], + (' '|safe) ~ stats['top5']['memory'][0][0] ~ " ‏"|safe ~ ((stats['top5']['memory'][0][1]*100/stats['system']['ram_total'])|int) ~ "% ‏"|safe~ + (' '|safe) ~ stats['top5']['memory'][1][0] ~ " ‏"|safe ~ ((stats['top5']['memory'][1][1]*100/stats['system']['ram_total'])|int) ~ "% ‏"|safe~ + (' '|safe) ~ stats['top5']['memory'][2][0] ~ " ‏"|safe ~ ((stats['top5']['memory'][2][1]*100/stats['system']['ram_total'])|int) ~ "% ", + coloring=True, + color_class="bg-h-pink" )}} - {#info_box("connections","fa-solid fa-signal", _("Connections/IP"), - stats['system']['total_unique_ips'] ~ " / " ~ stats['system']['total_connections'], - stats['system']['total_unique_ips']*100/stats['system']['total_connections'], - _("CDN make it incorrect") + {#info_box("connections","fa-solid fa-signal", _("Connections/IP"), + stats['system']['total_unique_ips'] ~ " / " ~ stats['system']['total_connections'], + stats['system']['total_unique_ips']*100/stats['system']['total_connections'], + _("CDN make it incorrect") )#} - {{info_box("disk","fa-solid fa-hard-drive", _("Disk"), - (stats['system']['disk_used']|round(1)) ~ " / "~(stats['system']['disk_total']|round(1))~"GB", - stats['system']['disk_used']*100/stats['system']['disk_total'], - _("Hiddify") ~ ": " ~ (stats['system']['hiddify_used']|round(1)) ~ "GB", - coloring=True, - color_class='bg-h-sky' + {{info_box("disk","fa-solid fa-hard-drive", _("Disk"), + (stats['system']['disk_used']|round(1)) ~ " / "~(stats['system']['disk_total']|round(1))~"GB", + stats['system']['disk_used']*100/stats['system']['disk_total'], + _("Hiddify") ~ ": " ~ (stats['system']['hiddify_used']|round(1)) ~ "GB", + coloring=True, + color_class='bg-h-sky' )}} - -
+ +
% if hconfig(ConfigEnum.is_parent) @@ -159,140 +155,140 @@

{{title}}

%block tail_js {{super()}} -%endblock +%endblock \ No newline at end of file diff --git a/hiddifypanel/panel/admin/templates/result.html b/hiddifypanel/panel/admin/templates/result.html index 248084484..af194e136 100644 --- a/hiddifypanel/panel/admin/templates/result.html +++ b/hiddifypanel/panel/admin/templates/result.html @@ -5,22 +5,22 @@ {% block body %} %if g.temp_admin_link {% macro temp_link() -%}
-{%- endmacro -%} +{%- endmacro -%}
{{_("We have opened a temporary port (for 4 hours) to access the panel in case of any issues. Please copy this link.")}} {{temp_link()}} -
- +
+ {{modal('save_emergency',_('This action is not yet finished.'),_("Please copy the emergency link before leaving this page."),temp_link(),show=False)}} %endif @@ -38,12 +38,12 @@ % endif @@ -53,19 +53,24 @@

{{_('log')}}

{{super()}} % if log_path % endif + +% endif %endblock \ No newline at end of file diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index 97b947512..8bd7fd126 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -158,9 +158,11 @@ def base_middleware(): @app.before_request def api_auth_middleware(): '''In every api request(whether is for the admin or the user) the client should provide api key and we check it''' - if 'api' not in request.path: # type: ignore + if '/api/v1/' not in request.path and '/api/v2/' not in request.path: + return + # skip the CORS preflight request + if request.method == 'OPTIONS' and request.headers.get('Sec-Fetch-Mode') == 'cors': return - # get authenticated account account: AdminUser | User | None = auth.standalone_api_auth_verify() if not account: @@ -208,15 +210,11 @@ def backward_compatibility_middleware(): @app.before_request def basic_auth_middleware(): '''if the request is for user panel(user page), we try to authenticate the user with basic auth or the client session data, we do that for admin panel too''' - if 'api' in request.path: # type: ignore + if '/api/v1/' in request.path or '/api/v2/' in request.path: # type: ignore return account: AdminUser | User | None = None - # if we don't have endpoint, we can't detect the request is for admin panel or user panel, so we can't authenticate - if not request.endpoint: - abort(400, "invalid request") - if request.endpoint and 'UserView' in request.endpoint: account = auth.standalone_user_basic_auth_verification() else: From 9f32621ecbe4bd08006cf8eafe85d3228c5f4e51 Mon Sep 17 00:00:00 2001 From: Sarina Date: Tue, 19 Dec 2023 17:37:33 +0330 Subject: [PATCH 033/112] chg: new route page --- .../user/templates/redirect_to_new_format.html | 18 ++++++++++++++++++ .../panel/user/templates/redirect_to_user.html | 4 ---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 hiddifypanel/panel/user/templates/redirect_to_new_format.html delete mode 100644 hiddifypanel/panel/user/templates/redirect_to_user.html diff --git a/hiddifypanel/panel/user/templates/redirect_to_new_format.html b/hiddifypanel/panel/user/templates/redirect_to_new_format.html new file mode 100644 index 000000000..9146b9260 --- /dev/null +++ b/hiddifypanel/panel/user/templates/redirect_to_new_format.html @@ -0,0 +1,18 @@ +{% extends "master.html" %} +{% block title %}{{hconfig(ConfigEnum.branding_title) or _('user.home.title')}}{% endblock %} +{% from 'macros.html' import modal %} +{% block body_header %} +

+ User page route is redirected to: {{user_link}} +
+

+{% macro reload() -%} + + {{_("copy")}} + +{%- endmacro -%} +{{modal("link-alarm-modal",_("Link is changed!"),_("The page has been moved to href=%(new_link)s",new_link=new_link),footer=copy_link(),show=True)}} +%endif + + +{% endblock %} \ No newline at end of file diff --git a/hiddifypanel/panel/user/templates/redirect_to_user.html b/hiddifypanel/panel/user/templates/redirect_to_user.html deleted file mode 100644 index 90067b0a4..000000000 --- a/hiddifypanel/panel/user/templates/redirect_to_user.html +++ /dev/null @@ -1,4 +0,0 @@ -

- User page route is redirected to: {{user_link}} -
-

\ No newline at end of file From 3727e5123306ad56586f70ae9bc423e50f15caa1 Mon Sep 17 00:00:00 2001 From: Sarina Date: Tue, 19 Dec 2023 17:38:47 +0330 Subject: [PATCH 034/112] chg: init --- hiddifypanel/panel/admin/__init__.py | 29 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/hiddifypanel/panel/admin/__init__.py b/hiddifypanel/panel/admin/__init__.py index cf46ed70d..3e0caee2f 100644 --- a/hiddifypanel/panel/admin/__init__.py +++ b/hiddifypanel/panel/admin/__init__.py @@ -37,21 +37,20 @@ def auto_route(proxy_path=None, user_secret=None): flaskadmin.add_view(DomainAdmin(Domain, db.session)) flaskadmin.add_view(AdminstratorAdmin(AdminUser, db.session)) - with app.app_context(): - from .Dashboard import Dashboard - from .SettingAdmin import SettingAdmin - from .commercial_info import CommercialInfo - from .ProxyAdmin import ProxyAdmin - from .Actions import Actions - from .Backup import Backup - from .QuickSetup import QuickSetup - Dashboard.register(admin_bp, route_base="/") - SettingAdmin.register(admin_bp) - ProxyAdmin.register(admin_bp) - Actions.register(admin_bp) - CommercialInfo.register(admin_bp) - QuickSetup.register(admin_bp) - Backup.register(admin_bp) + from .Dashboard import Dashboard + from .SettingAdmin import SettingAdmin + from .commercial_info import CommercialInfo + from .ProxyAdmin import ProxyAdmin + from .Actions import Actions + from .Backup import Backup + from .QuickSetup import QuickSetup + Dashboard.register(admin_bp, route_base="/") + SettingAdmin.register(admin_bp) + ProxyAdmin.register(admin_bp) + Actions.register(admin_bp) + CommercialInfo.register(admin_bp) + QuickSetup.register(admin_bp) + Backup.register(admin_bp) # admin_bp.add_url_rule('/admin/quicksetup/',endpoint="quicksetup",view_func=QuickSetup.index,methods=["GET"]) # admin_bp.add_url_rule('/admin/quicksetup/',endpoint="quicksetup-save", view_func=QuickSetup.save,methods=["POST"]) From 76c1d814f9903df64591efb1430d939be409eb9c Mon Sep 17 00:00:00 2001 From: Sarina Date: Tue, 19 Dec 2023 17:40:04 +0330 Subject: [PATCH 035/112] chg: backward compatibility --- hiddifypanel/panel/common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index 2d9aa05b8..122ffe570 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -185,15 +185,16 @@ def backward_compatibility_middleware(): if '/admin/' in request.path: # we check if there is such uuid or not, because we don't want to redirect to admin panel if there is no such uuid # otherwise anyone can provide any secret to get access to admin panel + uuid = hutils.utils.get_uuid_from_url_path(request.path) or abort(400, 'invalid request') - if get_admin_by_uuid(uuid): - return render_template('redirect_to_admin.html', admin_link=f'{request.url_root.rstrip("/").replace("http", "https")}/{g.proxy_path}/admin/') + if admin:=get_admin_by_uuid(uuid): + return render_template('redirect_to_new_format.html', new_link=f'https://{admin.username}:{admin.password}@{request.host}/{g.proxy_path}/admin/') else: abort(400, 'invalid request') else: uuid = hutils.utils.get_uuid_from_url_path(request.path) or abort(400, 'invalid request') if user := get_user_by_uuid(uuid): - return render_template('redirect_to_user.html', user_link=f'{request.url_root.rstrip("/").replace("http", "https")}/{g.proxy_path}/#{user.name}') + return render_template('redirect_to_new_format.html', new_link=f'https://{user.username}:{user.password}@{request.host}/{g.proxy_path}/') else: abort(400, 'invalid request') From 9dd19fbc171915f56dc9a4ddaa740cf6fa404932 Mon Sep 17 00:00:00 2001 From: Sarina Date: Tue, 19 Dec 2023 17:45:26 +0330 Subject: [PATCH 036/112] new: better handling user change --- hiddifypanel/panel/authentication.py | 11 +++++++---- .../panel/user/templates/redirect_to_new_format.html | 5 +---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/authentication.py index 42982d460..1cb7e2e11 100644 --- a/hiddifypanel/panel/authentication.py +++ b/hiddifypanel/panel/authentication.py @@ -1,5 +1,7 @@ from typing import Set from apiflask import HTTPBasicAuth, HTTPTokenAuth +from flask_httpauth import MultiAuth + from hiddifypanel.models.user import User, get_user_by_uuid from hiddifypanel.models.admin import AdminUser, get_admin_by_uuid from flask import session @@ -7,6 +9,7 @@ basic_auth = HTTPBasicAuth() api_auth = HTTPTokenAuth("ApiKey") +multi_auth = MultiAuth(basic_auth, api_auth) class AccountRole(StrEnum): @@ -18,8 +21,8 @@ class AccountRole(StrEnum): @basic_auth.verify_password def verify_basic_auth_password(username, password) -> AdminUser | User | None: - username = username.strip() - password = password.strip() + # username = username.strip() + # password = password.strip() if username and password: return User.query.filter( User.username == username, User.password == password).first() or AdminUser.query.filter( @@ -29,7 +32,7 @@ def verify_basic_auth_password(username, password) -> AdminUser | User | None: @api_auth.verify_token def verify_api_auth_token(token) -> User | AdminUser | None: # for now, token is the same as uuid - token = token.strip() + # token = token.strip() if token: return get_user_by_uuid(token) or get_admin_by_uuid(token) @@ -77,7 +80,7 @@ def get_account_role(account) -> AccountRole | None: ''' if isinstance(account, User): return AccountRole.user - elif isinstance(account, AdminUser): + if isinstance(account, AdminUser): match account.mode: case 'super_admin': return AccountRole.super_admin diff --git a/hiddifypanel/panel/user/templates/redirect_to_new_format.html b/hiddifypanel/panel/user/templates/redirect_to_new_format.html index 9146b9260..a8452d24a 100644 --- a/hiddifypanel/panel/user/templates/redirect_to_new_format.html +++ b/hiddifypanel/panel/user/templates/redirect_to_new_format.html @@ -2,10 +2,7 @@ {% block title %}{{hconfig(ConfigEnum.branding_title) or _('user.home.title')}}{% endblock %} {% from 'macros.html' import modal %} {% block body_header %} -

- User page route is redirected to: {{user_link}} -
-

+ {% macro reload() -%} {{_("copy")}} From 35c69339ad4d08c9e31db3d2a0d52184f29d792d Mon Sep 17 00:00:00 2001 From: Sarina Date: Tue, 19 Dec 2023 17:49:57 +0330 Subject: [PATCH 037/112] support app changes --- hiddifypanel/panel/common.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index 122ffe570..0e0f0be37 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -176,28 +176,20 @@ def api_auth_middleware(): @app.before_request def backward_compatibility_middleware(): - if hutils.utils.is_uuid_in_url_path(request.path): - if g.proxy_path != hconfig(ConfigEnum.proxy_path): - # this will make a fingerprint for panel. we should redirect the request to decoy website. - # abort(400, "invalid proxy path") - redirect(hconfig(ConfigEnum.decoy_domain)) - - if '/admin/' in request.path: - # we check if there is such uuid or not, because we don't want to redirect to admin panel if there is no such uuid - # otherwise anyone can provide any secret to get access to admin panel + if g.proxy_path != hconfig(ConfigEnum.proxy_path): + # this will make a fingerprint for panel. we should redirect the request to decoy website. + abort(400, "invalid proxy path") + uuid = hutils.utils.get_uuid_from_url_path(request.path) + if uuid: + user=get_user_by_uuid(uuid) or get_admin_by_uuid(uuid) or abort(400, 'invalid request') + new_link=f'https://{user.username}:{user.password}@{request.host}/{g.proxy_path}/' + if 'Mozilla' in request.user_agent.string: + return redirect(new_link,301) + + if "/admin/" in request.path: + new_link+="admin/" + return render_template('redirect_to_new_format.html', new_link=new_link) - uuid = hutils.utils.get_uuid_from_url_path(request.path) or abort(400, 'invalid request') - if admin:=get_admin_by_uuid(uuid): - return render_template('redirect_to_new_format.html', new_link=f'https://{admin.username}:{admin.password}@{request.host}/{g.proxy_path}/admin/') - else: - abort(400, 'invalid request') - else: - uuid = hutils.utils.get_uuid_from_url_path(request.path) or abort(400, 'invalid request') - if user := get_user_by_uuid(uuid): - return render_template('redirect_to_new_format.html', new_link=f'https://{user.username}:{user.password}@{request.host}/{g.proxy_path}/') - else: - abort(400, 'invalid request') - @app.before_request def basic_auth_middleware(): '''if the request is for user panel(user page), we try to authenticate the user with basic auth or the client session data, we do that for admin panel too''' From 3a67c710d2e41ef7378710a55580e795467e1ccf Mon Sep 17 00:00:00 2001 From: Sarina Date: Tue, 19 Dec 2023 18:28:01 +0330 Subject: [PATCH 038/112] chg: apiflask authentication to auth_back.py file & remove unused imports --- hiddifypanel/panel/admin/Actions.py | 1 - hiddifypanel/panel/admin/Backup.py | 4 +--- hiddifypanel/panel/admin/ConfigAdmin.py | 1 - hiddifypanel/panel/admin/Dashboard.py | 1 - hiddifypanel/panel/admin/SettingAdmin.py | 1 - hiddifypanel/panel/{authentication.py => auth_back.py} | 0 .../panel/commercial/restapi/v2/admin/admin_info_api.py | 1 - .../panel/commercial/restapi/v2/admin/admin_user_api.py | 2 -- .../panel/commercial/restapi/v2/admin/admin_users_api.py | 1 - .../panel/commercial/restapi/v2/admin/server_status_api.py | 1 - hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py | 1 - hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py | 1 - hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py | 1 - hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py | 1 - hiddifypanel/panel/commercial/restapi/v2/user/info_api.py | 1 - hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py | 1 - hiddifypanel/panel/commercial/restapi/v2/user/short_api.py | 2 -- 17 files changed, 1 insertion(+), 20 deletions(-) rename hiddifypanel/panel/{authentication.py => auth_back.py} (100%) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index 960e29453..9d7649b02 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -12,7 +12,6 @@ from hiddifypanel.panel import hiddify, usage from hiddifypanel.panel.run_commander import commander, Command from hiddifypanel.panel.hiddify import flash -from hiddifypanel.panel.authentication import basic_auth class Actions(FlaskView): diff --git a/hiddifypanel/panel/admin/Backup.py b/hiddifypanel/panel/admin/Backup.py index 88ccc58bf..e2c5f38f2 100644 --- a/hiddifypanel/panel/admin/Backup.py +++ b/hiddifypanel/panel/admin/Backup.py @@ -12,13 +12,11 @@ from flask import current_app as app from hiddifypanel.panel.hiddify import flash from flask_wtf.file import FileField, FileRequired -from flask_classful import FlaskView, route +from flask_classful import FlaskView -from hiddifypanel.panel.database import db from hiddifypanel.panel import hiddify from hiddifypanel.models import * from hiddifypanel.panel import hiddify -from hiddifypanel.panel.authentication import basic_auth class Backup(FlaskView): diff --git a/hiddifypanel/panel/admin/ConfigAdmin.py b/hiddifypanel/panel/admin/ConfigAdmin.py index dcccf8faa..02e22e6c8 100644 --- a/hiddifypanel/panel/admin/ConfigAdmin.py +++ b/hiddifypanel/panel/admin/ConfigAdmin.py @@ -4,7 +4,6 @@ from wtforms.validators import ValidationError -from hiddifypanel.panel.authentication import AccountRole, standalone_verify from hiddifypanel.models import ConfigEnum, Domain from .adminlte import AdminLTEModelView diff --git a/hiddifypanel/panel/admin/Dashboard.py b/hiddifypanel/panel/admin/Dashboard.py index f191b6067..a28d3511b 100644 --- a/hiddifypanel/panel/admin/Dashboard.py +++ b/hiddifypanel/panel/admin/Dashboard.py @@ -12,7 +12,6 @@ from hiddifypanel.panel import hiddify from hiddifypanel.panel.database import db from hiddifypanel.panel.hiddify import flash -from hiddifypanel.panel.authentication import basic_auth class Dashboard(FlaskView): diff --git a/hiddifypanel/panel/admin/SettingAdmin.py b/hiddifypanel/panel/admin/SettingAdmin.py index fb536d1d9..636bff668 100644 --- a/hiddifypanel/panel/admin/SettingAdmin.py +++ b/hiddifypanel/panel/admin/SettingAdmin.py @@ -20,7 +20,6 @@ from hiddifypanel.panel.hiddify import flash from hiddifypanel.panel.hiddify import get_random_domains from hiddifypanel.panel import hiddify, custom_widgets -from hiddifypanel.panel.authentication import basic_auth class SettingAdmin(FlaskView): diff --git a/hiddifypanel/panel/authentication.py b/hiddifypanel/panel/auth_back.py similarity index 100% rename from hiddifypanel/panel/authentication.py rename to hiddifypanel/panel/auth_back.py diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py index e3263bc81..38749f596 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py @@ -7,7 +7,6 @@ from hiddifypanel.models.config_enum import ConfigEnum, Lang from hiddifypanel.models.config import hconfig from hiddifypanel.panel import hiddify -from hiddifypanel.panel.authentication import api_auth from .admin_user_api import AdminSchema diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py index 0fcdc6248..918588401 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py @@ -6,11 +6,9 @@ from apiflask import abort -from hiddifypanel.models import * from hiddifypanel.models import * from hiddifypanel.panel import hiddify from hiddifypanel.models import AdminMode, Lang -from hiddifypanel.panel.authentication import api_auth class AdminSchema(Schema): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py index a61df7fec..04c9da136 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py @@ -4,7 +4,6 @@ from .admin_user_api import AdminSchema from hiddifypanel.models import AdminUser from hiddifypanel.panel import hiddify -from hiddifypanel.panel.authentication import api_auth class AdminUsersApi(MethodView): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py index 2cd20af6a..d2381a657 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py @@ -5,7 +5,6 @@ from apiflask import Schema from hiddifypanel.panel import hiddify from hiddifypanel.models import get_daily_usage_stats -from hiddifypanel.panel.authentication import api_auth class ServerStatus(Schema): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py index 72009037f..3779326f6 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py @@ -8,7 +8,6 @@ from hiddifypanel.panel import hiddify from hiddifypanel.drivers import user_driver from hiddifypanel.panel import hiddify -from hiddifypanel.panel.authentication import api_auth from apiflask.fields import UUID, String, Float, Enum, Date, Time, Integer diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py index 1087d4b7b..81793233e 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py @@ -5,7 +5,6 @@ from hiddifypanel.panel import hiddify from hiddifypanel.drivers import user_driver from hiddifypanel.models import User -from hiddifypanel.panel.authentication import api_auth from .user_api import UserSchema diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py index 2078541fb..b7a5ff2f9 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py @@ -14,7 +14,6 @@ from hiddifypanel.panel.user.user import get_common_data from hiddifypanel.hutils.utils import get_latest_release_url, do_base_64 -from hiddifypanel.panel.authentication import api_auth # region App Api DTOs diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py index 1347f5a68..17528990e 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py @@ -9,7 +9,6 @@ from apiflask.fields import String from hiddifypanel.panel.user.user import get_common_data from hiddifypanel.panel.user import link_maker -from hiddifypanel.panel.authentication import api_auth class ConfigSchema(Schema): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py index 09630cf75..10878358d 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py @@ -12,7 +12,6 @@ from hiddifypanel.panel import hiddify from hiddifypanel.panel.database import db from hiddifypanel.panel.user.user import get_common_data -from hiddifypanel.panel.authentication import api_auth class ProfileSchema(Schema): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py index e23a84f12..1b7be65a4 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py @@ -10,7 +10,6 @@ from hiddifypanel.panel.user.user import get_common_data from hiddifypanel.panel import hiddify -from hiddifypanel.panel.authentication import api_auth class MtproxySchema(Schema): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py index bff7df960..d727ae5a9 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py @@ -4,12 +4,10 @@ from flask import g, request from apiflask.fields import String, Integer from flask import current_app as app -from datetime import datetime from flask.views import MethodView from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum from hiddifypanel.panel import hiddify -from hiddifypanel.panel.authentication import api_auth class ShortSchema(Schema): From e011503802b0a96fede0575b4f28f5f664668206 Mon Sep 17 00:00:00 2001 From: Sarina Date: Tue, 19 Dec 2023 23:13:14 +0330 Subject: [PATCH 039/112] add: flask_login & add flask_login.UserMixin to User and AdminUser models --- hiddifypanel/base.py | 1 + hiddifypanel/hutils/utils.py | 54 ++++++++++++++++++++++++++++++++++ hiddifypanel/models/admin.py | 4 +-- hiddifypanel/models/user.py | 3 +- hiddifypanel/panel/__init__.py | 3 +- hiddifypanel/panel/auth.py | 52 ++++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 hiddifypanel/panel/auth.py diff --git a/hiddifypanel/base.py b/hiddifypanel/base.py index 58135c961..1a8a68158 100644 --- a/hiddifypanel/base.py +++ b/hiddifypanel/base.py @@ -65,6 +65,7 @@ def create_app(cli=False, **config): with app.app_context(): init_db() + hiddifypanel.panel.auth.init_app(app) hiddifypanel.panel.common.init_app(app) hiddifypanel.panel.admin.init_app(app) hiddifypanel.panel.user.init_app(app) diff --git a/hiddifypanel/hutils/utils.py b/hiddifypanel/hutils/utils.py index e4993dbf2..74fd1aee8 100644 --- a/hiddifypanel/hutils/utils.py +++ b/hiddifypanel/hutils/utils.py @@ -1,3 +1,5 @@ +import base64 +from strenum import StrEnum from urllib.parse import urlparse from uuid import UUID from flask_babelex import lazy_gettext as _ @@ -154,3 +156,55 @@ def get_uuid_from_url_path(path: str) -> str | None: if is_uuid_valid(section, 4): return section return None + + +class AccountRole(StrEnum): + user = 'user' + admin = 'admin' + super_admin = 'super_admin' + agent = 'agent' + + +def get_account_role(account) -> AccountRole | None: + '''Returns user/admin role + Allowed roles are: + - for user: + - user + - for admin: + - super_admin + - admin + - agent + ''' + from hiddifypanel.models import AdminUser, User + if isinstance(account, User): + return AccountRole.user + if isinstance(account, AdminUser): + match account.mode: + case 'super_admin': + return AccountRole.super_admin + case 'admin': + return AccountRole.admin + case 'agent': + return AccountRole.agent + + +def get_apikey_from_auth_header(auth_header: str) -> str | None: + if auth_header.startswith('ApiKey'): + return auth_header.split('ApiKey ')[1].strip() + return None + + +def get_basic_auth_from_auth_header(auth_header: str) -> str | None: + if auth_header.startswith('Basic'): + return auth_header.split('Basic ')[1].strip() + return None + + +def parse_basic_auth_header(auth_header: str) -> tuple[str, str] | None: + if not auth_header.startswith('Basic'): + return None + header_value = auth_header.split('Basic ') + if len(header_value) < 2: + return None + username, password = map(lambda item: item.strip(), base64.urlsafe_b64decode(header_value[1].strip()).decode('utf-8').split(':')) + return (username, password) if username and password else None diff --git a/hiddifypanel/models/admin.py b/hiddifypanel/models/admin.py index d0ce9b0b9..1cdec6ca1 100644 --- a/hiddifypanel/models/admin.py +++ b/hiddifypanel/models/admin.py @@ -5,11 +5,11 @@ from hiddifypanel import hutils from hiddifypanel.models.usage import DailyUsage from sqlalchemy_serializer import SerializerMixin +from flask_login import UserMixin as FlaskLoginUserMixin from strenum import StrEnum from hiddifypanel.panel.database import db -from wtforms.validators import Regexp, ValidationError from apiflask import abort from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ @@ -27,7 +27,7 @@ class AdminMode(StrEnum): agent = auto() -class AdminUser(db.Model, SerializerMixin): +class AdminUser(db.Model, SerializerMixin, FlaskLoginUserMixin): """ This is a model class for a user in a database that includes columns for their ID, UUID, name, online status, account expiration date, usage limit, package days, mode, start date, current usage, last reset time, and comment. diff --git a/hiddifypanel/models/user.py b/hiddifypanel/models/user.py index 305551b38..e6134faf1 100644 --- a/hiddifypanel/models/user.py +++ b/hiddifypanel/models/user.py @@ -2,6 +2,7 @@ import uuid as uuid_mod from enum import auto +from flask_login import UserMixin as FlaskLoginUserMixin from dateutil import relativedelta from hiddifypanel import hutils from sqlalchemy_serializer import SerializerMixin @@ -49,7 +50,7 @@ def ips(self): return [] if not self.connected_ips else self.connected_ips.split(",") -class User(db.Model, SerializerMixin): +class User(db.Model, SerializerMixin, FlaskLoginUserMixin): """ This is a model class for a user in a database that includes columns for their ID, UUID, name, online status, account expiration date, usage limit, package days, mode, start date, current usage, last reset time, and comment. diff --git a/hiddifypanel/panel/__init__.py b/hiddifypanel/panel/__init__.py index 8bc28af28..bad8ce684 100644 --- a/hiddifypanel/panel/__init__.py +++ b/hiddifypanel/panel/__init__.py @@ -4,4 +4,5 @@ from . import cli from . import database from . import common -from . import commercial \ No newline at end of file +from . import commercial +from . import auth diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py new file mode 100644 index 000000000..454c1f702 --- /dev/null +++ b/hiddifypanel/panel/auth.py @@ -0,0 +1,52 @@ +from flask_login import LoginManager, login_user +from hiddifypanel.models import User +import hiddifypanel.hutils as hutils +from flask import g, session +from hiddifypanel.models.admin import AdminUser, get_admin_by_uuid +from hiddifypanel.models.user import get_user_by_uuid +from hiddifypanel.hutils.utils import AccountRole + + +def init_app(app): + login_manager = LoginManager() + login_manager.init_app(app) + + @login_manager.user_loader + def user_loader_auth(user_id) -> User | AdminUser | None: + # check what type of account is stored in the session + if role := session.get('account_role'): + account = account = User.query.filter(User.id == user_id).first() if role == AccountRole.user else AdminUser.query.filter(AdminUser.id == user_id).first() + if account: + g.account = account + g.account_uuid = account.uuid + g.account_role = hutils.utils.get_account_role(account) + g.is_admin = False if g.account_role == hutils.utils.AccountRole.user else True + return account + + @login_manager.request_loader + def request_loader_auth(request) -> User | AdminUser | None: + auth_header: str = request.headers.get("Authorization") + if not auth_header: + return None + + account = None + if 'api' in request.blueprint: + if apikey := hutils.utils.get_apikey_from_auth_header(auth_header): + account = get_user_by_uuid(apikey) or get_admin_by_uuid(apikey) + else: + if username_password := hutils.utils.parse_basic_auth_header(auth_header): + if request.blueprint == 'user2': + account = User.query.filter(User.username == username_password[0], User.password == username_password[1]).first() + else: + account = AdminUser.query.filter(AdminUser.username == username_password[0], AdminUser.password == username_password[1]).first() + + if account: + g.account = account + g.account_uuid = account.uuid + account_role = hutils.utils.get_account_role(account) + g.is_admin = False if account_role == hutils.utils.AccountRole.user else True + + # store user role to distict between user and admin + session['account_role'] = account_role + login_user(account) + return account From fa2d4ab81ed4c7fef0c066ae5cc126490e2de6fb Mon Sep 17 00:00:00 2001 From: Sarina Date: Tue, 19 Dec 2023 23:14:37 +0330 Subject: [PATCH 040/112] using flask_login instead of old authenticator --- hiddifypanel/panel/admin/Actions.py | 13 ++- hiddifypanel/panel/admin/AdminstratorAdmin.py | 2 + hiddifypanel/panel/admin/Backup.py | 5 +- hiddifypanel/panel/admin/ChildAdmin.py | 2 + hiddifypanel/panel/admin/ConfigAdmin.py | 1 + hiddifypanel/panel/admin/Dashboard.py | 5 +- hiddifypanel/panel/admin/DomainAdmin.py | 2 + hiddifypanel/panel/admin/ProxyAdmin.py | 3 +- hiddifypanel/panel/admin/SettingAdmin.py | 4 +- hiddifypanel/panel/admin/UserAdmin.py | 2 + .../panel/commercial/restapi/v1/resources.py | 9 +- .../panel/commercial/restapi/v1/tgmsg.py | 6 +- hiddifypanel/panel/common.py | 107 +++++++++--------- 13 files changed, 91 insertions(+), 70 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index 9d7649b02..8c450261c 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -5,6 +5,7 @@ from flask_classful import FlaskView, route from flask import render_template, request, Markup, url_for, make_response, redirect +from flask_login import login_required from flask import current_app as app from hiddifypanel import hutils @@ -16,6 +17,7 @@ class Actions(FlaskView): + @login_required @hiddify.super_admin def index(self): return render_template('index.html') @@ -45,6 +47,7 @@ def reverselog(self, logfile): response.headers['Access-Control-Allow-Headers'] = 'Content-Type' return response + @login_required @hiddify.admin def viewlogs(self): config_dir = app.config['HIDDIFY_CONFIG_PATH'] @@ -53,16 +56,19 @@ def viewlogs(self): res.append(f"{filename}") return Markup("
".join(res)) + @login_required @hiddify.super_admin @route('apply_configs', methods=['POST']) def apply_configs(self): return self.reinstall(False) + @login_required @hiddify.super_admin @route('reset', methods=['POST']) def reset(self): return self.reset2() + @login_required @hiddify.super_admin def reset2(self): status = self.status() @@ -88,12 +94,13 @@ def reset2(self): time.sleep(1) return resp + @login_required @hiddify.super_admin @route('reinstall', methods=['POST']) def reinstall(self, complete_install=True, domain_changed=False): return self.reinstall2(complete_install, domain_changed) - # @hiddify.super_admin + @login_required @hiddify.super_admin def reinstall2(self, complete_install=True, domain_changed=False): if int(hconfig(ConfigEnum.db_version)) < 9: @@ -149,6 +156,7 @@ def reinstall2(self, complete_install=True, domain_changed=False): time.sleep(1) return resp + @login_required @hiddify.super_admin def change_reality_keys(self): key = hiddify.generate_x25519_keys() @@ -157,6 +165,7 @@ def change_reality_keys(self): hiddify.flash_config_success(restart_mode='apply', domain_changed=False) return redirect(url_for('admin.SettingAdmin:index')) + @login_required @hiddify.super_admin def status(self): # hiddify.add_temporary_access() @@ -182,6 +191,7 @@ def status(self): ) @route('update', methods=['POST']) + @login_required @hiddify.super_admin def update(self): return self.update2() @@ -242,6 +252,7 @@ def get_some_random_reality_friendly_domain(self): return res+"" + @login_required @hiddify.super_admin def update_usage(self): diff --git a/hiddifypanel/panel/admin/AdminstratorAdmin.py b/hiddifypanel/panel/admin/AdminstratorAdmin.py index 6049144d2..782be2315 100644 --- a/hiddifypanel/panel/admin/AdminstratorAdmin.py +++ b/hiddifypanel/panel/admin/AdminstratorAdmin.py @@ -1,3 +1,4 @@ +from flask_login import login_required from wtforms.validators import Regexp from hiddifypanel.models import * from wtforms.validators import Regexp, ValidationError @@ -161,6 +162,7 @@ def _max_active_users_formatter(view, context, model, name): def search_placeholder(self): return f"{_('search')} {_('user.UUID')} {_('user.name')}" + @login_required @hiddify.admin def is_accessible(self): return True diff --git a/hiddifypanel/panel/admin/Backup.py b/hiddifypanel/panel/admin/Backup.py index e2c5f38f2..cfc5055a4 100644 --- a/hiddifypanel/panel/admin/Backup.py +++ b/hiddifypanel/panel/admin/Backup.py @@ -9,18 +9,17 @@ import json import json from flask import render_template, request, jsonify, redirect, g -from flask import current_app as app +from flask_login import login_required from hiddifypanel.panel.hiddify import flash from flask_wtf.file import FileField, FileRequired from flask_classful import FlaskView from hiddifypanel.panel import hiddify from hiddifypanel.models import * -from hiddifypanel.panel import hiddify class Backup(FlaskView): - decorators = [hiddify.super_admin] + decorators = [login_required, hiddify.super_admin] def index(self): return render_template('backup.html', restore_form=get_restore_form()) diff --git a/hiddifypanel/panel/admin/ChildAdmin.py b/hiddifypanel/panel/admin/ChildAdmin.py index 4029e403c..53b379ffa 100644 --- a/hiddifypanel/panel/admin/ChildAdmin.py +++ b/hiddifypanel/panel/admin/ChildAdmin.py @@ -1,3 +1,4 @@ +from flask_login import login_required from hiddifypanel import hutils from wtforms.validators import Regexp from hiddifypanel.models import * @@ -107,6 +108,7 @@ def on_model_delete(self, model): raise ValidationError(f"at least one domain should exist") hiddify.flash_config_success(restart_mode='apply', domain_changed=True) + @login_required @hiddify.admin def is_accessible(self): return True diff --git a/hiddifypanel/panel/admin/ConfigAdmin.py b/hiddifypanel/panel/admin/ConfigAdmin.py index 02e22e6c8..1c1edf2fc 100644 --- a/hiddifypanel/panel/admin/ConfigAdmin.py +++ b/hiddifypanel/panel/admin/ConfigAdmin.py @@ -22,6 +22,7 @@ class ConfigAdmin(AdminLTEModelView): }, } + @login_required @hiddify.admin def is_accessible(self): return True diff --git a/hiddifypanel/panel/admin/Dashboard.py b/hiddifypanel/panel/admin/Dashboard.py index a28d3511b..3aaabff49 100644 --- a/hiddifypanel/panel/admin/Dashboard.py +++ b/hiddifypanel/panel/admin/Dashboard.py @@ -2,7 +2,7 @@ from flask import render_template, url_for, request, jsonify, g, redirect -from flask import current_app as app +from flask_login import login_required from apiflask import abort from flask_babelex import lazy_gettext as _ from flask_classful import FlaskView, route @@ -15,6 +15,7 @@ class Dashboard(FlaskView): + @login_required @hiddify.admin def get_data(self): admin_id = request.args.get("admin_id") or g.account.id @@ -26,6 +27,7 @@ def get_data(self): usage_history=get_daily_usage_stats(admin_id) )) + @login_required @hiddify.admin def index(self): @@ -84,6 +86,7 @@ def index(self): stats = {'system': hiddify.system_stats(), 'top5': hiddify.top_processes()} return render_template('index.html', stats=stats, usage_history=get_daily_usage_stats(admin_id, child_id), childs=childs) + @login_required @hiddify.super_admin @route('remove_child', methods=['POST']) def remove_child(self): diff --git a/hiddifypanel/panel/admin/DomainAdmin.py b/hiddifypanel/panel/admin/DomainAdmin.py index 4661d1cab..b51794ece 100644 --- a/hiddifypanel/panel/admin/DomainAdmin.py +++ b/hiddifypanel/panel/admin/DomainAdmin.py @@ -1,4 +1,5 @@ import ipaddress +from flask_login import login_required from hiddifypanel.models import * import re from flask import Markup @@ -282,6 +283,7 @@ def after_model_change(self, form, model, is_created): # run get_cert.sh commander(Command.get_cert, domain=model.domain) + @login_required @hiddify.admin def is_accessible(self): return True diff --git a/hiddifypanel/panel/admin/ProxyAdmin.py b/hiddifypanel/panel/admin/ProxyAdmin.py index 300edc7c9..4b93cb6e6 100644 --- a/hiddifypanel/panel/admin/ProxyAdmin.py +++ b/hiddifypanel/panel/admin/ProxyAdmin.py @@ -14,10 +14,11 @@ from flask_classful import FlaskView from hiddifypanel.panel.hiddify import flash from hiddifypanel.panel import hiddify +from flask_login import login_required class ProxyAdmin(FlaskView): - decorators = [hiddify.super_admin] + decorators = [login_required, hiddify.super_admin] def index(self): return render_template('proxy.html', global_config_form=get_global_config_form(), detailed_config_form=get_all_proxy_form()) diff --git a/hiddifypanel/panel/admin/SettingAdmin.py b/hiddifypanel/panel/admin/SettingAdmin.py index 636bff668..57937f3d7 100644 --- a/hiddifypanel/panel/admin/SettingAdmin.py +++ b/hiddifypanel/panel/admin/SettingAdmin.py @@ -4,6 +4,7 @@ # from flask_babelex import gettext as _ from flask import render_template, Markup, url_for, g from flask import current_app as app +from flask_login import login_required import wtforms as wtf from flask_bootstrap import SwitchField @@ -24,12 +25,13 @@ class SettingAdmin(FlaskView): + @login_required @hiddify.super_admin def index(self): form = get_config_form() return render_template('config.html', form=form) - # @hiddify.super_admin + @login_required @hiddify.super_admin def post(self): set_hconfig(ConfigEnum.first_setup, False) diff --git a/hiddifypanel/panel/admin/UserAdmin.py b/hiddifypanel/panel/admin/UserAdmin.py index a7c42eab7..0b474a4b7 100644 --- a/hiddifypanel/panel/admin/UserAdmin.py +++ b/hiddifypanel/panel/admin/UserAdmin.py @@ -1,6 +1,7 @@ import datetime import uuid import re +from flask_login import login_required from wtforms.validators import Regexp, ValidationError from flask import Markup, g, request, url_for @@ -204,6 +205,7 @@ def on_model_delete(self, model): user_driver.remove_client(model) # hiddify.flash_config_success() + @login_required @hiddify.admin def is_accessible(self): return True diff --git a/hiddifypanel/panel/commercial/restapi/v1/resources.py b/hiddifypanel/panel/commercial/restapi/v1/resources.py index 993d6a681..161741aaf 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/resources.py +++ b/hiddifypanel/panel/commercial/restapi/v1/resources.py @@ -4,18 +4,13 @@ # from flask_simplelogin import login_required import datetime from hiddifypanel.models import * -from urllib.parse import urlparse +from flask_login import login_required from hiddifypanel.panel import hiddify from hiddifypanel.drivers import user_driver -# class AllResource(Resource): -# def get(self): -# return jsonify( -# hiddify.dump_db_to_dict() -# ) class UserResource(Resource): - decorators = [hiddify.super_admin] + decorators = [login_required, hiddify.super_admin] def get(self): uuid = request.args.get('uuid') diff --git a/hiddifypanel/panel/commercial/restapi/v1/tgmsg.py b/hiddifypanel/panel/commercial/restapi/v1/tgmsg.py index bca618f9f..46faa484f 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/tgmsg.py +++ b/hiddifypanel/panel/commercial/restapi/v1/tgmsg.py @@ -1,16 +1,18 @@ -from flask import jsonify, request +from flask import jsonify, request from apiflask import abort from flask_restful import Resource # from flask_simplelogin import login_required import datetime from hiddifypanel.panel.database import db from hiddifypanel.models import * -from urllib.parse import urlparse +from flask_login import login_required from hiddifypanel.panel import hiddify, usage from .tgbot import bot class SendMsgResource(Resource): + decorators = [login_required, hiddify.super_admin] + def post(self): if not hconfig(ConfigEnum.telegram_bot_token): diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index 0e0f0be37..5f9a229d2 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -8,7 +8,6 @@ from hiddifypanel.panel import hiddify, github_issue_generator from sys import version as python_version from platform import platform -import hiddifypanel.panel.authentication as auth import hiddifypanel.hutils as hutils from apiflask import APIFlask, HTTPError, abort @@ -149,30 +148,30 @@ def base_middleware(): # setup telegram bot if hconfig(ConfigEnum.telegram_bot_token): import hiddifypanel.panel.commercial.telegrambot as telegrambot - if (not telegrambot.bot) or (not telegrambot.bot.username): + if (not telegrambot.bot) or (not telegrambot.bot.username): # type: ignore telegrambot.register_bot() g.bot = telegrambot.bot else: g.bot = None - @app.before_request - def api_auth_middleware(): - '''In every api request(whether is for the admin or the user) the client should provide api key and we check it''' - if 'api' not in request.path: # type: ignore - return - - # get authenticated account - account: AdminUser | User | None = auth.standalone_api_auth_verify() - if not account: - return abort(401) - # get account role - role = auth.get_account_role(account) - if not role: - return abort(401) - # setup authenticated account things (uuid, is_admin, etc.) - g.account = account - g.account_uuid = account.uuid - g.is_admin = False if role == auth.AccountRole.user else True + # @app.before_request + # def api_auth_middleware(): + # '''In every api request(whether is for the admin or the user) the client should provide api key and we check it''' + # if 'api' not in request.path: # type: ignore + # return + + # # get authenticated account + # account: AdminUser | User | None = auth.standalone_api_auth_verify() + # if not account: + # return abort(401) + # # get account role + # role = auth.get_account_role(account) + # if not role: + # return abort(401) + # # setup authenticated account things (uuid, is_admin, etc.) + # g.account = account + # g.account_uuid = account.uuid + # g.is_admin = False if role == auth.AccountRole.user else True @app.before_request def backward_compatibility_middleware(): @@ -180,43 +179,43 @@ def backward_compatibility_middleware(): # this will make a fingerprint for panel. we should redirect the request to decoy website. abort(400, "invalid proxy path") uuid = hutils.utils.get_uuid_from_url_path(request.path) - if uuid: - user=get_user_by_uuid(uuid) or get_admin_by_uuid(uuid) or abort(400, 'invalid request') - new_link=f'https://{user.username}:{user.password}@{request.host}/{g.proxy_path}/' + if uuid: + user = get_user_by_uuid(uuid) or get_admin_by_uuid(uuid) or abort(400, 'invalid request') + new_link = f'https://{user.username}:{user.password}@{request.host}/{g.proxy_path}/' if 'Mozilla' in request.user_agent.string: - return redirect(new_link,301) - - if "/admin/" in request.path: - new_link+="admin/" - return render_template('redirect_to_new_format.html', new_link=new_link) - - @app.before_request - def basic_auth_middleware(): - '''if the request is for user panel(user page), we try to authenticate the user with basic auth or the client session data, we do that for admin panel too''' - if 'api' in request.path: # type: ignore - return - - account: AdminUser | User | None = None + return redirect(new_link, 301) - # if we don't have endpoint, we can't detect the request is for admin panel or user panel, so we can't authenticate - if not request.endpoint: - abort(400, "invalid request") + if "/admin/" in request.path: + new_link += "admin/" + return render_template('redirect_to_new_format.html', new_link=new_link) - if request.endpoint and 'UserView' in request.endpoint: - account = auth.standalone_user_basic_auth_verification() - else: - account = auth.standalone_admin_basic_auth_verification() - # get authenticated account - if not account: - return abort(401) - # get account role - role = auth.get_account_role(account) - if not role: - return abort(401) - # setup authenticated account things (uuid, is_admin, etc.) - g.account = account - g.account_uuid = account.uuid - g.is_admin = False if role == auth.AccountRole.user else True + # @app.before_request + # def basic_auth_middleware(): + # '''if the request is for user panel(user page), we try to authenticate the user with basic auth or the client session data, we do that for admin panel too''' + # if 'api' in request.path: # type: ignore + # return + + # account: AdminUser | User | None = None + + # # if we don't have endpoint, we can't detect the request is for admin panel or user panel, so we can't authenticate + # if not request.endpoint: + # abort(400, "invalid request") + + # if request.endpoint and 'UserView' in request.endpoint: + # account = auth.standalone_user_basic_auth_verification() + # else: + # account = auth.standalone_admin_basic_auth_verification() + # # get authenticated account + # if not account: + # return abort(401) + # # get account role + # role = auth.get_account_role(account) + # if not role: + # return abort(401) + # # setup authenticated account things (uuid, is_admin, etc.) + # g.account = account + # g.account_uuid = account.uuid + # g.is_admin = False if role == auth.AccountRole.user else True # @app.auth_required(basic_auth, roles=['super_admin', 'admin', 'agent', 'user']) From f9fd1aaee4fc11eba1456baac5e1cc9860f60eff Mon Sep 17 00:00:00 2001 From: Sarina Date: Wed, 20 Dec 2023 01:25:41 +0330 Subject: [PATCH 041/112] add: role property to user and admin models & implement Role enum new: override user/admin get_id method --- hiddifypanel/models/__init__.py | 1 + hiddifypanel/models/admin.py | 14 ++++++++++++++ hiddifypanel/models/role.py | 10 ++++++++++ hiddifypanel/models/user.py | 8 ++++++++ 4 files changed, 33 insertions(+) create mode 100644 hiddifypanel/models/role.py diff --git a/hiddifypanel/models/__init__.py b/hiddifypanel/models/__init__.py index edbae50fa..c10edc4d0 100644 --- a/hiddifypanel/models/__init__.py +++ b/hiddifypanel/models/__init__.py @@ -8,3 +8,4 @@ from .admin import AdminUser, AdminMode, get_super_admin_secret, get_admin_user_db, get_admin_by_uuid, add_or_update_admin, bulk_register_admins, current_admin_or_owner, get_super_admin, get_admin_by_username from .child import Child from .usage import DailyUsage, get_daily_usage_stats +from .role import Role diff --git a/hiddifypanel/models/admin.py b/hiddifypanel/models/admin.py index 1cdec6ca1..bbe88c125 100644 --- a/hiddifypanel/models/admin.py +++ b/hiddifypanel/models/admin.py @@ -3,6 +3,7 @@ from flask import g from hiddifypanel import hutils +from hiddifypanel.models.role import Role from hiddifypanel.models.usage import DailyUsage from sqlalchemy_serializer import SerializerMixin from flask_login import UserMixin as FlaskLoginUserMixin @@ -48,6 +49,19 @@ class AdminUser(db.Model, SerializerMixin, FlaskLoginUserMixin): parent_admin_id = db.Column(db.Integer, db.ForeignKey('admin_user.id'), default=1) parent_admin = db.relationship('AdminUser', remote_side=[id], backref='sub_admins') + @property + def role(self): + match self.mode: + case AdminMode.super_admin: + return Role.super_admin + case AdminMode.admin: + return Role.admin + case AdminMode.agent: + return Role.agent + + def get_id(self): + return f'admin_{self.id}' + def recursive_users_query(self): from .user import User admin_ids = self.recursive_sub_admins_ids() diff --git a/hiddifypanel/models/role.py b/hiddifypanel/models/role.py new file mode 100644 index 000000000..3cb5ae898 --- /dev/null +++ b/hiddifypanel/models/role.py @@ -0,0 +1,10 @@ + +'''This file isn't a model, it's just an enum for roles''' +from strenum import StrEnum + + +class Role(StrEnum): + super_admin = "super_admin" + admin = "admin" + agent = "agent" + user = "user" diff --git a/hiddifypanel/models/user.py b/hiddifypanel/models/user.py index e6134faf1..43183f554 100644 --- a/hiddifypanel/models/user.py +++ b/hiddifypanel/models/user.py @@ -10,6 +10,7 @@ from hiddifypanel.panel.database import db from hiddifypanel.models import Lang +from hiddifypanel.models.role import Role ONE_GIG = 1024*1024*1024 @@ -112,6 +113,13 @@ def ips(self): res[ip] = 1 return list(res.keys()) + @property + def role(self): + return Role.user + + def get_id(self): + return f'user_{self.id}' + @staticmethod def by_id(user_id): """ From 8a6f2b94743733227afec1066e778e78885ee76b Mon Sep 17 00:00:00 2001 From: Sarina Date: Wed, 20 Dec 2023 03:14:41 +0330 Subject: [PATCH 042/112] new: auth.login_required (supports roles) --- hiddifypanel/hutils/utils.py | 43 ++++++++----------------- hiddifypanel/panel/auth.py | 61 ++++++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 50 deletions(-) diff --git a/hiddifypanel/hutils/utils.py b/hiddifypanel/hutils/utils.py index 74fd1aee8..c720e3ccb 100644 --- a/hiddifypanel/hutils/utils.py +++ b/hiddifypanel/hutils/utils.py @@ -1,4 +1,5 @@ import base64 +from typing import Tuple from strenum import StrEnum from urllib.parse import urlparse from uuid import UUID @@ -158,36 +159,6 @@ def get_uuid_from_url_path(path: str) -> str | None: return None -class AccountRole(StrEnum): - user = 'user' - admin = 'admin' - super_admin = 'super_admin' - agent = 'agent' - - -def get_account_role(account) -> AccountRole | None: - '''Returns user/admin role - Allowed roles are: - - for user: - - user - - for admin: - - super_admin - - admin - - agent - ''' - from hiddifypanel.models import AdminUser, User - if isinstance(account, User): - return AccountRole.user - if isinstance(account, AdminUser): - match account.mode: - case 'super_admin': - return AccountRole.super_admin - case 'admin': - return AccountRole.admin - case 'agent': - return AccountRole.agent - - def get_apikey_from_auth_header(auth_header: str) -> str | None: if auth_header.startswith('ApiKey'): return auth_header.split('ApiKey ')[1].strip() @@ -208,3 +179,15 @@ def parse_basic_auth_header(auth_header: str) -> tuple[str, str] | None: return None username, password = map(lambda item: item.strip(), base64.urlsafe_b64decode(header_value[1].strip()).decode('utf-8').split(':')) return (username, password) if username and password else None + + +def parse_auth_id(raw_id) -> Tuple[type | None, str | None]: + splitted = raw_id.split('_') + if len(splitted) < 2: + return None, None + admin_or_user, id = splitted + from hiddifypanel.models import AdminUser, User + account_type = type(User) if admin_or_user == 'user' else type(AdminUser) + if not id or not account_type: + return None, None + return account_type, id diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index 454c1f702..9c243ac3d 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -1,10 +1,10 @@ -from flask_login import LoginManager, login_user -from hiddifypanel.models import User +from functools import wraps +from flask_login import LoginManager, login_user, current_user import hiddifypanel.hutils as hutils -from flask import g, session -from hiddifypanel.models.admin import AdminUser, get_admin_by_uuid +from flask import g +from flask import current_app +from hiddifypanel.models import AdminUser, User, get_admin_by_uuid, Role from hiddifypanel.models.user import get_user_by_uuid -from hiddifypanel.hutils.utils import AccountRole def init_app(app): @@ -12,22 +12,28 @@ def init_app(app): login_manager.init_app(app) @login_manager.user_loader - def user_loader_auth(user_id) -> User | AdminUser | None: - # check what type of account is stored in the session - if role := session.get('account_role'): - account = account = User.query.filter(User.id == user_id).first() if role == AccountRole.user else AdminUser.query.filter(AdminUser.id == user_id).first() - if account: - g.account = account - g.account_uuid = account.uuid - g.account_role = hutils.utils.get_account_role(account) - g.is_admin = False if g.account_role == hutils.utils.AccountRole.user else True - return account + def user_loader_auth(id: str) -> User | AdminUser | None: + # parse id + account_type, id = hutils.utils.parse_auth_id(id) # type: ignore + if not account_type or not id: + return + + if account_type == type(AdminUser): + account = AdminUser.query.filter(AdminUser.id == id).first() + else: + account = User.query.filter(User.id == id).first() + + if account: + g.account = account + g.account_uuid = account.uuid + g.is_admin = False if account.role == Role.user else True + return account @login_manager.request_loader def request_loader_auth(request) -> User | AdminUser | None: auth_header: str = request.headers.get("Authorization") if not auth_header: - return None + return account = None if 'api' in request.blueprint: @@ -43,10 +49,25 @@ def request_loader_auth(request) -> User | AdminUser | None: if account: g.account = account g.account_uuid = account.uuid - account_role = hutils.utils.get_account_role(account) - g.is_admin = False if account_role == hutils.utils.AccountRole.user else True + g.is_admin = False if account.role == 'user' else True - # store user role to distict between user and admin - session['account_role'] = account_role login_user(account) return account + + +def login_required(roles: set[Role] | None = None): + def wrapper(fn): + @wraps(fn) + def decorated_view(*args, **kwargs): + if not current_user.is_authenticated: + return current_app.login_manager.unauthorized() # type: ignore + if roles: + # super_admin role has admin role permission too + if Role.admin in roles and Role.super_admin not in roles: + roles.add(Role.super_admin) + account_role = current_user.role + if account_role not in roles: + return current_app.login_manager.unauthorized() # type: ignore + return fn(*args, **kwargs) + return decorated_view + return wrapper From 26b0713eba52700902e5b530b62a276e3d7dae0a Mon Sep 17 00:00:00 2001 From: Sarina Date: Wed, 20 Dec 2023 03:19:43 +0330 Subject: [PATCH 043/112] using: new login_required --- hiddifypanel/panel/admin/Actions.py | 37 +++++++------------ hiddifypanel/panel/admin/AdminstratorAdmin.py | 5 +-- hiddifypanel/panel/admin/Backup.py | 4 +- hiddifypanel/panel/admin/ChildAdmin.py | 5 +-- hiddifypanel/panel/admin/ConfigAdmin.py | 3 +- hiddifypanel/panel/admin/Dashboard.py | 11 ++---- hiddifypanel/panel/admin/DomainAdmin.py | 5 +-- hiddifypanel/panel/admin/ProxyAdmin.py | 6 +-- hiddifypanel/panel/admin/SettingAdmin.py | 8 ++-- hiddifypanel/panel/admin/UserAdmin.py | 5 +-- .../panel/commercial/restapi/v1/resources.py | 4 +- .../panel/commercial/restapi/v1/tgmsg.py | 6 +-- 12 files changed, 39 insertions(+), 60 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index 8c450261c..0085f44e2 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -5,7 +5,7 @@ from flask_classful import FlaskView, route from flask import render_template, request, Markup, url_for, make_response, redirect -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from flask import current_app as app from hiddifypanel import hutils @@ -17,12 +17,11 @@ class Actions(FlaskView): - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def index(self): return render_template('index.html') - # @hiddify.super_admin + @login_required(roles={Role.super_admin}) def reverselog(self, logfile): if logfile == None: return self.viewlogs() @@ -47,8 +46,7 @@ def reverselog(self, logfile): response.headers['Access-Control-Allow-Headers'] = 'Content-Type' return response - @login_required - @hiddify.admin + @login_required(roles={Role.super_admin}) def viewlogs(self): config_dir = app.config['HIDDIFY_CONFIG_PATH'] res = [] @@ -56,20 +54,17 @@ def viewlogs(self): res.append(f"{filename}") return Markup("
".join(res)) - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) @route('apply_configs', methods=['POST']) def apply_configs(self): return self.reinstall(False) - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) @route('reset', methods=['POST']) def reset(self): return self.reset2() - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def reset2(self): status = self.status() # flash(_("rebooting system may takes time please wait"),'info') @@ -94,14 +89,12 @@ def reset2(self): time.sleep(1) return resp - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) @route('reinstall', methods=['POST']) def reinstall(self, complete_install=True, domain_changed=False): return self.reinstall2(complete_install, domain_changed) - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def reinstall2(self, complete_install=True, domain_changed=False): if int(hconfig(ConfigEnum.db_version)) < 9: return ("Please update your panel before this action.") @@ -156,8 +149,7 @@ def reinstall2(self, complete_install=True, domain_changed=False): time.sleep(1) return resp - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def change_reality_keys(self): key = hiddify.generate_x25519_keys() set_hconfig(ConfigEnum.reality_private_key, key['private_key']) @@ -165,8 +157,7 @@ def change_reality_keys(self): hiddify.flash_config_success(restart_mode='apply', domain_changed=False) return redirect(url_for('admin.SettingAdmin:index')) - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def status(self): # hiddify.add_temporary_access() # configs=read_configs() @@ -191,8 +182,7 @@ def status(self): ) @route('update', methods=['POST']) - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def update(self): return self.update2() @@ -252,8 +242,7 @@ def get_some_random_reality_friendly_domain(self): return res+"" - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def update_usage(self): import json diff --git a/hiddifypanel/panel/admin/AdminstratorAdmin.py b/hiddifypanel/panel/admin/AdminstratorAdmin.py index 782be2315..2ff51acc6 100644 --- a/hiddifypanel/panel/admin/AdminstratorAdmin.py +++ b/hiddifypanel/panel/admin/AdminstratorAdmin.py @@ -1,4 +1,4 @@ -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from wtforms.validators import Regexp from hiddifypanel.models import * from wtforms.validators import Regexp, ValidationError @@ -162,8 +162,7 @@ def _max_active_users_formatter(view, context, model, name): def search_placeholder(self): return f"{_('search')} {_('user.UUID')} {_('user.name')}" - @login_required - @hiddify.admin + @login_required(roles={Role.admin}) def is_accessible(self): return True diff --git a/hiddifypanel/panel/admin/Backup.py b/hiddifypanel/panel/admin/Backup.py index cfc5055a4..4e9024415 100644 --- a/hiddifypanel/panel/admin/Backup.py +++ b/hiddifypanel/panel/admin/Backup.py @@ -9,7 +9,7 @@ import json import json from flask import render_template, request, jsonify, redirect, g -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from hiddifypanel.panel.hiddify import flash from flask_wtf.file import FileField, FileRequired from flask_classful import FlaskView @@ -19,7 +19,7 @@ class Backup(FlaskView): - decorators = [login_required, hiddify.super_admin] + decorators = [login_required({Role.super_admin})] def index(self): return render_template('backup.html', restore_form=get_restore_form()) diff --git a/hiddifypanel/panel/admin/ChildAdmin.py b/hiddifypanel/panel/admin/ChildAdmin.py index 53b379ffa..d48579767 100644 --- a/hiddifypanel/panel/admin/ChildAdmin.py +++ b/hiddifypanel/panel/admin/ChildAdmin.py @@ -1,4 +1,4 @@ -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from hiddifypanel import hutils from wtforms.validators import Regexp from hiddifypanel.models import * @@ -108,7 +108,6 @@ def on_model_delete(self, model): raise ValidationError(f"at least one domain should exist") hiddify.flash_config_success(restart_mode='apply', domain_changed=True) - @login_required - @hiddify.admin + @login_required(roles={Role.admin}) def is_accessible(self): return True diff --git a/hiddifypanel/panel/admin/ConfigAdmin.py b/hiddifypanel/panel/admin/ConfigAdmin.py index 1c1edf2fc..bc3d873b2 100644 --- a/hiddifypanel/panel/admin/ConfigAdmin.py +++ b/hiddifypanel/panel/admin/ConfigAdmin.py @@ -22,8 +22,7 @@ class ConfigAdmin(AdminLTEModelView): }, } - @login_required - @hiddify.admin + @login_required(roles={Role.admin}) def is_accessible(self): return True diff --git a/hiddifypanel/panel/admin/Dashboard.py b/hiddifypanel/panel/admin/Dashboard.py index 3aaabff49..cdfd04c37 100644 --- a/hiddifypanel/panel/admin/Dashboard.py +++ b/hiddifypanel/panel/admin/Dashboard.py @@ -2,7 +2,7 @@ from flask import render_template, url_for, request, jsonify, g, redirect -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from apiflask import abort from flask_babelex import lazy_gettext as _ from flask_classful import FlaskView, route @@ -15,8 +15,7 @@ class Dashboard(FlaskView): - @login_required - @hiddify.admin + @login_required(roles={Role.admin}) def get_data(self): admin_id = request.args.get("admin_id") or g.account.id if admin_id not in g.account.recursive_sub_admins_ids(): @@ -27,8 +26,7 @@ def get_data(self): usage_history=get_daily_usage_stats(admin_id) )) - @login_required - @hiddify.admin + @login_required(roles={Role.admin}) def index(self): if hconfig(ConfigEnum.first_setup): @@ -86,8 +84,7 @@ def index(self): stats = {'system': hiddify.system_stats(), 'top5': hiddify.top_processes()} return render_template('index.html', stats=stats, usage_history=get_daily_usage_stats(admin_id, child_id), childs=childs) - @login_required - @hiddify.super_admin + @login_required(roles={Role.admin}) @route('remove_child', methods=['POST']) def remove_child(self): child_id = request.form['child_id'] diff --git a/hiddifypanel/panel/admin/DomainAdmin.py b/hiddifypanel/panel/admin/DomainAdmin.py index b51794ece..217d068e8 100644 --- a/hiddifypanel/panel/admin/DomainAdmin.py +++ b/hiddifypanel/panel/admin/DomainAdmin.py @@ -1,5 +1,5 @@ import ipaddress -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from hiddifypanel.models import * import re from flask import Markup @@ -283,8 +283,7 @@ def after_model_change(self, form, model, is_created): # run get_cert.sh commander(Command.get_cert, domain=model.domain) - @login_required - @hiddify.admin + @login_required(roles={Role.admin}) def is_accessible(self): return True diff --git a/hiddifypanel/panel/admin/ProxyAdmin.py b/hiddifypanel/panel/admin/ProxyAdmin.py index 4b93cb6e6..8f55fff79 100644 --- a/hiddifypanel/panel/admin/ProxyAdmin.py +++ b/hiddifypanel/panel/admin/ProxyAdmin.py @@ -1,9 +1,9 @@ from flask_babelex import gettext as _ +from hiddifypanel.models.role import Role import wtforms as wtf from flask_wtf import FlaskForm from flask_bootstrap import SwitchField from flask import render_template -from flask import current_app as app from hiddifypanel.models import ConfigEnum, get_hconfigs, BoolConfig, ConfigEnum, hconfig, Proxy @@ -14,11 +14,11 @@ from flask_classful import FlaskView from hiddifypanel.panel.hiddify import flash from hiddifypanel.panel import hiddify -from flask_login import login_required +from hiddifypanel.panel.auth import login_required class ProxyAdmin(FlaskView): - decorators = [login_required, hiddify.super_admin] + decorators = [login_required({Role.super_admin})] def index(self): return render_template('proxy.html', global_config_form=get_global_config_form(), detailed_config_form=get_all_proxy_form()) diff --git a/hiddifypanel/panel/admin/SettingAdmin.py b/hiddifypanel/panel/admin/SettingAdmin.py index 57937f3d7..bd2bd7190 100644 --- a/hiddifypanel/panel/admin/SettingAdmin.py +++ b/hiddifypanel/panel/admin/SettingAdmin.py @@ -4,7 +4,7 @@ # from flask_babelex import gettext as _ from flask import render_template, Markup, url_for, g from flask import current_app as app -from flask_login import login_required +from hiddifypanel.panel.auth import login_required import wtforms as wtf from flask_bootstrap import SwitchField @@ -25,14 +25,12 @@ class SettingAdmin(FlaskView): - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def index(self): form = get_config_form() return render_template('config.html', form=form) - @login_required - @hiddify.super_admin + @login_required(roles={Role.super_admin}) def post(self): set_hconfig(ConfigEnum.first_setup, False) form = get_config_form() diff --git a/hiddifypanel/panel/admin/UserAdmin.py b/hiddifypanel/panel/admin/UserAdmin.py index 0b474a4b7..b8475080c 100644 --- a/hiddifypanel/panel/admin/UserAdmin.py +++ b/hiddifypanel/panel/admin/UserAdmin.py @@ -1,7 +1,7 @@ import datetime import uuid import re -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from wtforms.validators import Regexp, ValidationError from flask import Markup, g, request, url_for @@ -205,8 +205,7 @@ def on_model_delete(self, model): user_driver.remove_client(model) # hiddify.flash_config_success() - @login_required - @hiddify.admin + @login_required(roles={Role.admin}) def is_accessible(self): return True diff --git a/hiddifypanel/panel/commercial/restapi/v1/resources.py b/hiddifypanel/panel/commercial/restapi/v1/resources.py index 161741aaf..883737716 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/resources.py +++ b/hiddifypanel/panel/commercial/restapi/v1/resources.py @@ -4,13 +4,13 @@ # from flask_simplelogin import login_required import datetime from hiddifypanel.models import * -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from hiddifypanel.panel import hiddify from hiddifypanel.drivers import user_driver class UserResource(Resource): - decorators = [login_required, hiddify.super_admin] + decorators = [login_required({Role.super_admin})] def get(self): uuid = request.args.get('uuid') diff --git a/hiddifypanel/panel/commercial/restapi/v1/tgmsg.py b/hiddifypanel/panel/commercial/restapi/v1/tgmsg.py index 46faa484f..3879ad3af 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/tgmsg.py +++ b/hiddifypanel/panel/commercial/restapi/v1/tgmsg.py @@ -1,17 +1,17 @@ -from flask import jsonify, request +from flask import request from apiflask import abort from flask_restful import Resource # from flask_simplelogin import login_required import datetime from hiddifypanel.panel.database import db from hiddifypanel.models import * -from flask_login import login_required +from hiddifypanel.panel.auth import login_required from hiddifypanel.panel import hiddify, usage from .tgbot import bot class SendMsgResource(Resource): - decorators = [login_required, hiddify.super_admin] + decorators = [login_required({Role.super_admin})] def post(self): From d07f294a77ab3896360d6356239eee6f72103629 Mon Sep 17 00:00:00 2001 From: Sarina Date: Wed, 20 Dec 2023 15:50:06 +0330 Subject: [PATCH 044/112] fix: merging conflicts --- hiddifypanel/panel/admin/Actions.py | 2 +- hiddifypanel/panel/auth.py | 6 ++- .../panel/commercial/restapi/v1/resources.py | 2 +- .../restapi/v2/admin/admin_log_api.py | 4 +- hiddifypanel/panel/common.py | 38 +++++++++---------- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index 16908c47d..768accea7 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -4,7 +4,7 @@ import urllib.request from flask_classful import FlaskView, route -from flask import render_template, request, Markup, url_for, make_response, redirect +from flask import render_template, request, Markup, url_for, make_response, redirect, g from hiddifypanel.panel.auth import login_required from flask import current_app as app # from flask_cors import cross_origin diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index 9c243ac3d..d97345d6c 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -36,9 +36,11 @@ def request_loader_auth(request) -> User | AdminUser | None: return account = None + is_api_request = False if 'api' in request.blueprint: if apikey := hutils.utils.get_apikey_from_auth_header(auth_header): account = get_user_by_uuid(apikey) or get_admin_by_uuid(apikey) + is_api_request = True else: if username_password := hutils.utils.parse_basic_auth_header(auth_header): if request.blueprint == 'user2': @@ -50,8 +52,8 @@ def request_loader_auth(request) -> User | AdminUser | None: g.account = account g.account_uuid = account.uuid g.is_admin = False if account.role == 'user' else True - - login_user(account) + if not is_api_request: + login_user(account) return account diff --git a/hiddifypanel/panel/commercial/restapi/v1/resources.py b/hiddifypanel/panel/commercial/restapi/v1/resources.py index 883737716..5cd1a4877 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/resources.py +++ b/hiddifypanel/panel/commercial/restapi/v1/resources.py @@ -56,7 +56,7 @@ def delete(self, uuid=None): class AdminUserResource(Resource): - decorators = [hiddify.super_admin] + decorators = [login_required({Role.super_admin})] def get(self, uuid=None): uuid = request.args.get('uuid') diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py index 4ab22f15d..ce074deee 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py @@ -1,10 +1,12 @@ from apiflask import Schema, fields, abort from flask.views import MethodView +from hiddifypanel.models.role import Role from hiddifypanel.panel import hiddify from flask import current_app as app from flask_cors import cross_origin import os from ansi2html import Ansi2HTMLConverter +from hiddifypanel.panel.auth import login_required class AdminLogfileSchema(Schema): @@ -12,7 +14,7 @@ class AdminLogfileSchema(Schema): class AdminLogApi(MethodView): - decorators = [hiddify.super_admin] + decorators = [login_required({Role.super_admin})] @app.input(AdminLogfileSchema, arg_name="data", location='form') @app.output(fields.String(description="The html of the log")) diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index b237a0351..0aea5411a 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -197,25 +197,25 @@ def backward_compatibility_middleware(): # account: AdminUser | User | None = None - # # if we don't have endpoint, we can't detect the request is for admin panel or user panel, so we can't authenticate - # if not request.endpoint: - # abort(400, "invalid request") - - # if request.endpoint and 'UserView' in request.endpoint: - # account = auth.standalone_user_basic_auth_verification() - # else: - # account = auth.standalone_admin_basic_auth_verification() - # # get authenticated account - # if not account: - # return abort(401) - # # get account role - # role = auth.get_account_role(account) - # if not role: - # return abort(401) - # # setup authenticated account things (uuid, is_admin, etc.) - # g.account = account - # g.account_uuid = account.uuid - # g.is_admin = False if role == auth.AccountRole.user else True + # # if we don't have endpoint, we can't detect the request is for admin panel or user panel, so we can't authenticate + # if not request.endpoint: + # abort(400, "invalid request") + + # if request.endpoint and 'UserView' in request.endpoint: + # account = auth.standalone_user_basic_auth_verification() + # else: + # account = auth.standalone_admin_basic_auth_verification() + # # get authenticated account + # if not account: + # return abort(401) + # # get account role + # role = auth.get_account_role(account) + # if not role: + # return abort(401) + # # setup authenticated account things (uuid, is_admin, etc.) + # g.account = account + # g.account_uuid = account.uuid + # g.is_admin = False if role == auth.AccountRole.user else True # @app.auth_required(basic_auth, roles=['super_admin', 'admin', 'agent', 'user']) From 4aa7e3417ce850b6c061acc7a4acd3fa2d9ea53e Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 045/112] add: get current logged account apikey (draft) --- hiddifypanel/panel/hiddify.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index ac7f8c7bb..d8f3c6199 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -105,6 +105,11 @@ def wrapper(*args, **kwargs): return wrapper +def current_account_api_key(): + # TODO: send real apikey + return g.account.uuid + + def abs_url(path): return f"/{g.proxy_path}/{g.account_uuid}/{path}" From 3bfa5cdc6dc183c1f65f9f429c091212a94d218d Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 046/112] new: JS to get logs files in result.html --- hiddifypanel/panel/admin/Actions.py | 75 ++++++++----------- .../panel/admin/templates/result.html | 23 +++++- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index 768accea7..ebeb6fcf1 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -60,35 +60,32 @@ def viewlogs(self): def apply_configs(self): return self.reinstall(False) - @login_required(roles={Role.super_admin}) + # @login_required(roles={Role.super_admin}) @route('reset', methods=['POST']) + @login_required(roles={Role.super_admin}) def reset(self): return self.reset2() @login_required(roles={Role.super_admin}) def reset2(self): status = self.status() - # flash(_("rebooting system may takes time please wait"),'info') - # os.system(f"echo 'reboot'|at now + 3s ") - resp = render_template("result.html", + # run restart.sh + commander(Command.restart_services) + + # flask don't response at all while using time.sleep + # import time + # time.sleep(1) + + return render_template("result.html", out_type="info", out_msg="", - log_path=get_logpath("restart.log"), + log_file_url=get_log_api_url(), + log_file='restart.log', show_success=True, domains=get_domains(), - - ) - # file = "restart.sh" - # config = current_app.config - # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/{file} --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) - - # run restart.sh - commander(Command.restart_services) - - import time - time.sleep(1) - return resp + api_key=hiddify.current_account_api_key(), + proxy_path=g.proxy_path) @login_required(roles={Role.super_admin}) @route('reinstall', methods=['POST']) @@ -118,11 +115,6 @@ def reinstall2(self, complete_install=True, domain_changed=False): except: server_ip = "server_ip" - # subprocess.Popen(f"{config_dir}/update.sh",env=my_env,cwd=f"{config_dir}") - # os.system(f'cd {config_dir};{env} ./install.sh &') - # rc = subprocess.call(f"cd {config_dir};./{file} & disown",shell=True) - - admin_secret = get_super_admin_secret() proxy_path = hconfig(ConfigEnum.proxy_path) admin_links = f"
{_('Admin Links')}
    " admin_links += f"
  • {_('Not Secure')}:
  • " @@ -131,23 +123,24 @@ def reinstall2(self, complete_install=True, domain_changed=False): for d in domains: link = f'https://{d}/{proxy_path}/admin/' admin_links += f"
  • {link}
  • " - resp = render_template("result.html", out_type="info", out_msg=_("Success! Please wait around 4 minutes to make sure everything is updated. During this time, please save your proxy links which are:") + admin_links, - log_path=get_logpath("0-install.log"), + log_file_url=get_log_api_url(), + log_file="0-install.log", show_success=True, domains=get_domains(), - ) + api_key=hiddify.current_account_api_key(), + proxy_path=g.proxy_path) # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/{file} --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) # run install.sh or apply_configs.sh commander(Command.install if complete_install else Command.apply) - import time - time.sleep(1) + # import time + # time.sleep(1) return resp @login_required(roles={Role.super_admin}) @@ -160,25 +153,16 @@ def change_reality_keys(self): @login_required(roles={Role.super_admin}) def status(self): - # hiddify.add_temporary_access() - # configs=read_configs() - # subprocess.Popen(f"{config_dir}/update.sh",env=my_env,cwd=f"{config_dir}") - # os.system(f'cd {config_dir};{env} ./install.sh &') - # rc = subprocess.call(f"cd {config_dir};./{file} & disown",shell=True) - - # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/status.sh --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) - # run status.sh commander(Command.status) - # TODO: send real apikey - api_key = g.account_uuid return render_template("result.html", out_type="info", out_msg=_("see the log in the bellow screen"), - log_path=get_logpath(f"status.log"), + log_file_url=get_log_api_url(), + log_file="status.log", show_success=False, domains=get_domains(), - api_key=api_key, + api_key=hiddify.current_account_api_key(), proxy_path=g.proxy_path, ) @@ -204,9 +188,11 @@ def update2(self): out_type="success", out_msg=_("Success! Please wait around 5 minutes to make sure everything is updated."), show_success=True, - log_path=get_logpath(f"update.log"), + log_file_url=get_log_api_url(), + log_file="update.log", domains=get_domains(), - ) + api_key=hiddify.current_account_api_key(), + proxy_path=g.proxy_path) def get_some_random_reality_friendly_domain(self): test_domain = request.args.get("test_domain") @@ -251,12 +237,13 @@ def update_usage(self): return render_template("result.html", out_type="info", out_msg=f'
    {json.dumps(usage.update_local_usage(),indent=2)}
    ', - log_path=None + log_file_url=None ) -def get_logpath(logfile): - return f'{hiddify.get_admin_path()}actions/reverselog/{logfile}/' +def get_log_api_url(): + # return f'{hiddify.get_admin_path()}actions/reverselog/{logfile}/' + return f'/{g.proxy_path}/api/v2/admin/log/' def get_domains(): diff --git a/hiddifypanel/panel/admin/templates/result.html b/hiddifypanel/panel/admin/templates/result.html index af194e136..4cade91e3 100644 --- a/hiddifypanel/panel/admin/templates/result.html +++ b/hiddifypanel/panel/admin/templates/result.html @@ -34,14 +34,14 @@
%endif -% if log_path +% if log_file_url @@ -51,7 +51,7 @@

{{_('log')}}

%block tail_js {{super()}} -% if log_path +% if log_file_url % endif %endblock \ No newline at end of file From 3b7ba2e6ea1a0c3486a5aaca5227a0c87477efd5 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 047/112] new: use log api for getting logs --- hiddifypanel/panel/admin/Actions.py | 8 +++--- .../panel/admin/templates/view_logs.html | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 hiddifypanel/panel/admin/templates/view_logs.html diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index ebeb6fcf1..75c283f53 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -22,6 +22,7 @@ class Actions(FlaskView): def index(self): return render_template('index.html') + # TODO: delete this function @login_required(roles={Role.super_admin}) def reverselog(self, logfile): if logfile == None: @@ -50,10 +51,11 @@ def reverselog(self, logfile): @login_required(roles={Role.super_admin}) def viewlogs(self): config_dir = app.config['HIDDIFY_CONFIG_PATH'] - res = [] + + log_files = [] for filename in sorted(os.listdir(f'{config_dir}/log/system/')): - res.append(f"{filename}") - return Markup("
".join(res)) + log_files.append(filename) + return render_template('view_logs.html', log_files=log_files, api_key=hiddify.current_account_api_key()) @login_required(roles={Role.super_admin}) @route('apply_configs', methods=['POST']) diff --git a/hiddifypanel/panel/admin/templates/view_logs.html b/hiddifypanel/panel/admin/templates/view_logs.html new file mode 100644 index 000000000..c9db98d02 --- /dev/null +++ b/hiddifypanel/panel/admin/templates/view_logs.html @@ -0,0 +1,26 @@ +

+ {% for file in log_files %} + {{file}} +
+ {% endfor %} +

+ + + + \ No newline at end of file From 8c4a6f6a35389640b435e8a33e3f141d33591428 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 048/112] add: new format of domain showing (https://user:pass@domain.com) --- hiddifypanel/panel/admin/Actions.py | 17 +++++------------ hiddifypanel/panel/admin/ChildAdmin.py | 3 ++- hiddifypanel/panel/hiddify.py | 7 ++++++- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index 75c283f53..cbb473b61 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -122,9 +122,12 @@ def reinstall2(self, complete_install=True, domain_changed=False): admin_links += f"
  • {_('Not Secure')}:
  • " domains = get_panel_domains() # domains=[*domains,f'{server_ip}.sslip.io'] + + username, password = hiddify.current_account_user_pass() for d in domains: - link = f'https://{d}/{proxy_path}/admin/' + link = f'https://{username}:{password}@{d}/{proxy_path}/admin/' admin_links += f"
  • {link}
  • " + resp = render_template("result.html", out_type="info", out_msg=_("Success! Please wait around 4 minutes to make sure everything is updated. During this time, please save your proxy links which are:") + @@ -165,8 +168,7 @@ def status(self): show_success=False, domains=get_domains(), api_key=hiddify.current_account_api_key(), - proxy_path=g.proxy_path, - ) + proxy_path=g.proxy_path) @route('update', methods=['POST']) @login_required(roles={Role.super_admin}) @@ -175,14 +177,6 @@ def update(self): def update2(self): hiddify.add_temporary_access() - - # os.chdir(config_dir) - # rc = subprocess.call(f"./install.sh &",shell=True) - # rc = subprocess.call(f"cd {config_dir};./update.sh & disown",shell=True) - # os.system(f'cd {config_dir};./update.sh &') - - # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/update.sh --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) - # run update.sh commander(Command.update) @@ -244,7 +238,6 @@ def update_usage(self): def get_log_api_url(): - # return f'{hiddify.get_admin_path()}actions/reverselog/{logfile}/' return f'/{g.proxy_path}/api/v2/admin/log/' diff --git a/hiddifypanel/panel/admin/ChildAdmin.py b/hiddifypanel/panel/admin/ChildAdmin.py index d48579767..ba03f6307 100644 --- a/hiddifypanel/panel/admin/ChildAdmin.py +++ b/hiddifypanel/panel/admin/ChildAdmin.py @@ -49,7 +49,8 @@ class ChildAdmin(AdminLTEModelView): form_columns = ['domain', 'show_domains'] def _domain_admin_link(view, context, model, name): - admin_link = f'https://{model.domain}{hiddify.get_admin_path()}' + username, password = hiddify.current_account_user_pass() + admin_link = f'https://{username}:{password}@{model.domain}{hiddify.get_admin_path()}' return Markup(f'') diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index d8f3c6199..8adbbe605 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -1,9 +1,10 @@ +import glob import subprocess import psutil from typing import Tuple from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import x25519 -from flask import abort, g, jsonify +from flask import abort, current_app, g, jsonify from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ from wtforms.validators import ValidationError @@ -110,6 +111,10 @@ def current_account_api_key(): return g.account.uuid +def current_account_user_pass(): + return g.account.username, g.account.password + + def abs_url(path): return f"/{g.proxy_path}/{g.account_uuid}/{path}" From 80fa30e1c91ee07383e8d9031a76d2b13480b350 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 049/112] fix: no need to send g.proxy_path to templates (jinja has it) --- hiddifypanel/panel/admin/Actions.py | 12 ++++-------- hiddifypanel/panel/admin/templates/result.html | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index cbb473b61..c37ed4d18 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -86,8 +86,7 @@ def reset2(self): log_file='restart.log', show_success=True, domains=get_domains(), - api_key=hiddify.current_account_api_key(), - proxy_path=g.proxy_path) + api_key=hiddify.current_account_api_key()) @login_required(roles={Role.super_admin}) @route('reinstall', methods=['POST']) @@ -136,8 +135,7 @@ def reinstall2(self, complete_install=True, domain_changed=False): log_file="0-install.log", show_success=True, domains=get_domains(), - api_key=hiddify.current_account_api_key(), - proxy_path=g.proxy_path) + api_key=hiddify.current_account_api_key()) # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/{file} --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) @@ -167,8 +165,7 @@ def status(self): log_file="status.log", show_success=False, domains=get_domains(), - api_key=hiddify.current_account_api_key(), - proxy_path=g.proxy_path) + api_key=hiddify.current_account_api_key()) @route('update', methods=['POST']) @login_required(roles={Role.super_admin}) @@ -187,8 +184,7 @@ def update2(self): log_file_url=get_log_api_url(), log_file="update.log", domains=get_domains(), - api_key=hiddify.current_account_api_key(), - proxy_path=g.proxy_path) + api_key=hiddify.current_account_api_key()) def get_some_random_reality_friendly_domain(self): test_domain = request.args.get("test_domain") diff --git a/hiddifypanel/panel/admin/templates/result.html b/hiddifypanel/panel/admin/templates/result.html index 4cade91e3..d9238e6da 100644 --- a/hiddifypanel/panel/admin/templates/result.html +++ b/hiddifypanel/panel/admin/templates/result.html @@ -58,7 +58,7 @@

    {{_('log')}}

    var seconds = 0 domains = '{{",".join(domains)}}'.split(",") log_api_path = '/api/v2/admin/log/' - log_urls = domains.map(p => "https://" + p + "/" + '{{ proxy_path }}' + log_api_path) + log_urls = domains.map(p => "https://" + p + "/" + '{{ g.proxy_path }}' + log_api_path) function sendRequestsSequentially(urls) { let index = 0; From b1b88d5cd57c69a97273a3c839caedce432f7eb4 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 050/112] add: user:pass to /admin/adminuser/ links --- hiddifypanel/panel/admin/AdminstratorAdmin.py | 3 ++- hiddifypanel/panel/hiddify.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/hiddifypanel/panel/admin/AdminstratorAdmin.py b/hiddifypanel/panel/admin/AdminstratorAdmin.py index faa63a741..0bc0db58a 100644 --- a/hiddifypanel/panel/admin/AdminstratorAdmin.py +++ b/hiddifypanel/panel/admin/AdminstratorAdmin.py @@ -90,7 +90,8 @@ def _name_formatter(view, context, model, name): proxy_path = hconfig(ConfigEnum.proxy_path) d = get_panel_domains()[0] if d: - link = f"{model.name} " + username, password = hiddify.current_account_user_pass() + link = f"{model.name} " if model.parent_admin: return Markup(model.parent_admin.name + "‏‎ / ‏‎"+link) return Markup(link) diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 8adbbe605..dedf8e926 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -241,9 +241,10 @@ def get_user_link(uuid, domain, mode='', username=''): if "*" in d: d = d.replace("*", get_random_string(5, 15)) - link = f"https://{d}/{proxy_path}/#{username}" + auth_username, auth_password = current_account_user_pass() + link = f"https://{auth_username}:{auth_password}@{d}/{proxy_path}/#{username}" if mode == "admin": - link = f"https://{d}/{proxy_path}/admin/#{username}" + link = f"https://{auth_username}:{auth_password}@{d}/{proxy_path}/admin/#{username}" link_multi = f"{link}multi" # if mode == 'new': # link = f"{link}new" From 08d200f78b27e2d79f4a5475b78a0088da77d51f Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 051/112] fix: accept authentication link to login(eg. https://username:password@domain.com/path) --- hiddifypanel/base.py | 3 +++ hiddifypanel/panel/auth.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/hiddifypanel/base.py b/hiddifypanel/base.py index 32dfbd1c9..acaad36b1 100644 --- a/hiddifypanel/base.py +++ b/hiddifypanel/base.py @@ -113,7 +113,10 @@ def check_csrf(): @app.after_request def apply_no_robot(response): response.headers["X-Robots-Tag"] = "noindex, nofollow" + if response.status_code == 401: + response.headers['WWW-Authenticate'] = 'Basic realm="Hiddify Secret Space"' return response + app.jinja_env.globals['get_locale'] = get_locale app.jinja_env.globals['version'] = hiddifypanel.__version__ app.jinja_env.globals['static_url_for'] = hiddify.static_url_for diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index d97345d6c..c6811ae6a 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -2,6 +2,7 @@ from flask_login import LoginManager, login_user, current_user import hiddifypanel.hutils as hutils from flask import g +from apiflask import abort from flask import current_app from hiddifypanel.models import AdminUser, User, get_admin_by_uuid, Role from hiddifypanel.models.user import get_user_by_uuid @@ -56,6 +57,12 @@ def request_loader_auth(request) -> User | AdminUser | None: login_user(account) return account + @login_manager.unauthorized_handler + def unauthorized(): + # TODO: show the login page + + abort(401, "Unauthorized") + def login_required(roles: set[Role] | None = None): def wrapper(fn): From de0ccdc55729bcf086320ea0d2183938ed7efbd4 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 052/112] fix: /admin/adminuser/ links username password bug --- hiddifypanel/panel/admin/AdminstratorAdmin.py | 3 +-- hiddifypanel/panel/hiddify.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/hiddifypanel/panel/admin/AdminstratorAdmin.py b/hiddifypanel/panel/admin/AdminstratorAdmin.py index 0bc0db58a..a7df0fa93 100644 --- a/hiddifypanel/panel/admin/AdminstratorAdmin.py +++ b/hiddifypanel/panel/admin/AdminstratorAdmin.py @@ -90,8 +90,7 @@ def _name_formatter(view, context, model, name): proxy_path = hconfig(ConfigEnum.proxy_path) d = get_panel_domains()[0] if d: - username, password = hiddify.current_account_user_pass() - link = f"{model.name} " + link = f"{model.name} " if model.parent_admin: return Markup(model.parent_admin.name + "‏‎ / ‏‎"+link) return Markup(link) diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index dedf8e926..3a55d1a9b 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -241,10 +241,8 @@ def get_user_link(uuid, domain, mode='', username=''): if "*" in d: d = d.replace("*", get_random_string(5, 15)) - auth_username, auth_password = current_account_user_pass() - link = f"https://{auth_username}:{auth_password}@{d}/{proxy_path}/#{username}" - if mode == "admin": - link = f"https://{auth_username}:{auth_password}@{d}/{proxy_path}/admin/#{username}" + account = AdminUser.query.filter(AdminUser.uuid == uuid).first() if mode == 'admin' else User.query.filter(User.uuid == uuid).first() + link = f"https://{account.username}:{account.password}@{d}/{proxy_path}/admin/#{username}" if mode == 'admin' else f"https://{account.username}:{account.password}@{d}/{proxy_path}/#{username}" link_multi = f"{link}multi" # if mode == 'new': # link = f"{link}new" From 40bf37d73faa8a39088ac6e9e84d01c496a6596e Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 053/112] fix: first try to authenticate with HTTP Authorization header value then check the user session data Think user is authenticated as A1 then they want to login as A2 on same domain. in this case we should keep our eye on header not session. because session is authenticated by A1 but now we want to authenticate with A2 --- hiddifypanel/panel/auth.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index c6811ae6a..44259074b 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -1,7 +1,7 @@ from functools import wraps from flask_login import LoginManager, login_user, current_user import hiddifypanel.hutils as hutils -from flask import g +from flask import g, request from apiflask import abort from flask import current_app from hiddifypanel.models import AdminUser, User, get_admin_by_uuid, Role @@ -14,6 +14,10 @@ def init_app(app): @login_manager.user_loader def user_loader_auth(id: str) -> User | AdminUser | None: + # first of all check if user sent Authorization header, our priority is with Authorization header + if request.headers.get("Authorization"): + return request_loader_auth(request) + # parse id account_type, id = hutils.utils.parse_auth_id(id) # type: ignore if not account_type or not id: From 818058cad25d0fdc3ca05d23bb7428755cde6d10 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 054/112] fix: bug --- hiddifypanel/panel/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index 44259074b..cd5c75c97 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -42,6 +42,8 @@ def request_loader_auth(request) -> User | AdminUser | None: account = None is_api_request = False + if not request.blueprint: + return if 'api' in request.blueprint: if apikey := hutils.utils.get_apikey_from_auth_header(auth_header): account = get_user_by_uuid(apikey) or get_admin_by_uuid(apikey) From cb27616faef8810d17666462bb27aaa6476ecc94 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 055/112] add: user:pass to non-secure link too --- hiddifypanel/panel/admin/Actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index c37ed4d18..f4ecfa9e7 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -118,11 +118,11 @@ def reinstall2(self, complete_install=True, domain_changed=False): proxy_path = hconfig(ConfigEnum.proxy_path) admin_links = f"
    {_('Admin Links')}
      " - admin_links += f"
    • {_('Not Secure')}:
    • " + username, password = hiddify.current_account_user_pass() + admin_links += f"
    • {_('Not Secure')}:
    • " domains = get_panel_domains() # domains=[*domains,f'{server_ip}.sslip.io'] - username, password = hiddify.current_account_user_pass() for d in domains: link = f'https://{username}:{password}@{d}/{proxy_path}/admin/' admin_links += f"
    • {link}
    • " From 22ca465a5dfa7c6f981b65edd0659c5314900d0a Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 056/112] add: authentication for user panel --- hiddifypanel/panel/user/user.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/hiddifypanel/panel/user/user.py b/hiddifypanel/panel/user/user.py index feb3055aa..e559c7686 100644 --- a/hiddifypanel/panel/user/user.py +++ b/hiddifypanel/panel/user/user.py @@ -2,9 +2,6 @@ from flask import render_template, request, Response, g, url_for, jsonify, flash from apiflask import abort from hiddifypanel.hutils import auto_ip_selector -from wtforms.validators import Regexp, ValidationError -import urllib -import uuid import datetime from hiddifypanel.models import * from hiddifypanel.panel.database import db @@ -16,6 +13,7 @@ import user_agents from flask_babelex import gettext as _ import re +from hiddifypanel.panel.auth import login_required class UserView(FlaskView): @@ -161,6 +159,7 @@ def create_item(name, type, domain, protocol, transport, security, link): # endregion @route('/test/') + @login_required(roles={Role.user}) def test(self): ua = request.user_agent.string if re.match('^Mozilla', ua, re.IGNORECASE): @@ -198,6 +197,7 @@ def test(self): # return render_template('home/multi.html',**c,ua=user_agent) @route('/') + @login_required(roles={Role.user}) def auto_sub(self): ua = request.user_agent.string if re.match('^Mozilla', ua, re.IGNORECASE): @@ -206,6 +206,7 @@ def auto_sub(self): @route('/sub') @route('/sub/') + @login_required(roles={Role.user}) def force_sub(self): return self.get_proper_config() or self.all_configs(base64=False) @@ -228,6 +229,7 @@ def get_proper_config(self): return self.all_configs(base64=True) @ route('/auto') + @login_required(roles={Role.user}) def auto_select(self): c = get_common_data(g.account_uuid, mode="new") user_agent = user_agents.parse(request.user_agent.string) @@ -236,6 +238,7 @@ def auto_select(self): @ route('/new/') @ route('/new') + @login_required(roles={Role.user}) # @ route('/') def new(self): conf = self.get_proper_config() @@ -249,6 +252,7 @@ def new(self): @ route('/clash//proxies.yml') @ route('/clash/proxies.yml') + @login_required(roles={Role.user}) def clash_proxies(self, meta_or_normal="normal"): mode = request.args.get("mode") domain = request.args.get("domain", None) @@ -261,6 +265,7 @@ def clash_proxies(self, meta_or_normal="normal"): return resp @ route('/report', methods=["POST"]) + @login_required(roles={Role.user}) def report(self): data = request.get_json() user_ip = auto_ip_selector.get_real_user_ip() @@ -296,6 +301,7 @@ def report(self): @ route('/clash/.yml', methods=["GET", "HEAD"]) @ route('/clash//.yml', methods=["GET", "HEAD"]) + @login_required(roles={Role.user}) def clash_config(self, meta_or_normal="normal", typ="all.yml"): mode = request.args.get("mode") @@ -311,6 +317,7 @@ def clash_config(self, meta_or_normal="normal", typ="all.yml"): return add_headers(resp, c) @ route('/full-singbox.json', methods=["GET", "HEAD"]) + @login_required(roles={Role.user}) def full_singbox(self): mode = "new" # request.args.get("mode") c = get_common_data(g.account_uuid, mode) @@ -324,6 +331,7 @@ def full_singbox(self): return add_headers(resp, c) @ route('/singbox.json', methods=["GET", "HEAD"]) + @login_required(roles={Role.user}) def singbox(self): if not hconfig(ConfigEnum.ssh_server_enable): return "SSH server is disable in settings" @@ -338,6 +346,7 @@ def singbox(self): return add_headers(resp, c) @ route('/all.txt', methods=["GET", "HEAD"]) + @login_required(roles={Role.user}) def all_configs(self, base64=False): mode = "new" # request.args.get("mode") base64 = base64 or request.args.get("base64", "").lower() == "true" @@ -359,6 +368,7 @@ def all_configs(self, base64=False): return add_headers(resp, c) @ route('/manifest.webmanifest') + @login_required(roles={Role.user}) def create_pwa_manifest(self): domain = urlparse(request.base_url).hostname @@ -383,6 +393,7 @@ def create_pwa_manifest(self): ] }) + @login_required(roles={Role.user}) @ route("/offline.html") def offline(): return f"Not Connected click for reload" From 334939ad7d966b2cb3a7212edea3377898bc9a53 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 057/112] fix: backward compatibility --- hiddifypanel/panel/common.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index 0aea5411a..9e4b6bf67 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -180,14 +180,15 @@ def backward_compatibility_middleware(): abort(400, "invalid proxy path") uuid = hutils.utils.get_uuid_from_url_path(request.path) if uuid: - user = get_user_by_uuid(uuid) or get_admin_by_uuid(uuid) or abort(400, 'invalid request') - new_link = f'https://{user.username}:{user.password}@{request.host}/{g.proxy_path}/' - if 'Mozilla' in request.user_agent.string: - return redirect(new_link, 301) - + account = get_user_by_uuid(uuid) or get_admin_by_uuid(uuid) or abort(400, 'invalid request') + new_link = f'https://{account.username}:{account.password}@{request.host}/{g.proxy_path}/' if "/admin/" in request.path: new_link += "admin/" - return render_template('redirect_to_new_format.html', new_link=new_link) + + # if request made by a browser, show new format page else redirect to new format + if g.user_agent.browser: + return render_template('redirect_to_new_format.html', new_link=new_link) + return redirect(new_link, 301) # @app.before_request # def basic_auth_middleware(): From 1fa739ed47e06f36b7355ee820a1824d2fbed24e Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 058/112] fix: new.html assests link --- hiddifypanel/panel/user/templates/new.html | 51 ++++++++++++---------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/hiddifypanel/panel/user/templates/new.html b/hiddifypanel/panel/user/templates/new.html index 2684d42b8..b7a1fd8af 100644 --- a/hiddifypanel/panel/user/templates/new.html +++ b/hiddifypanel/panel/user/templates/new.html @@ -1,27 +1,30 @@ - - - {%set base='https://'+domain+'/'+hconfigs[ConfigEnum.proxy_path]+"/"+user.uuid+"/"%} - - - - - - - Hiddify | Panel - - - - -
      - - - - - + + + {%set base='https://'+domain+'/'+hconfigs[ConfigEnum.proxy_path]+"/"+user.uuid+"/"%} + + + + + + + + Hiddify | Panel + + + + + +
      + + + + + + \ No newline at end of file From e9a079d3c9934310a3ac5730ad57497feccee017 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 059/112] new: redirect old apis url to new url --- hiddifypanel/panel/common.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index 9e4b6bf67..7292ee4dd 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -181,6 +181,11 @@ def backward_compatibility_middleware(): uuid = hutils.utils.get_uuid_from_url_path(request.path) if uuid: account = get_user_by_uuid(uuid) or get_admin_by_uuid(uuid) or abort(400, 'invalid request') + # redirect api calls + if hiddify.is_api_call(request.path): + new_link = f'{request.url.replace(f"/{uuid}","").replace("http://","https://")}/' + return redirect(new_link, 301) + new_link = f'https://{account.username}:{account.password}@{request.host}/{g.proxy_path}/' if "/admin/" in request.path: new_link += "admin/" From f06fccdad95e68afad98c52f46e805af51306a3c Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 060/112] fix: bug & authenticate api calls with session cookie --- hiddifypanel/hutils/utils.py | 19 ++++++++++++++----- hiddifypanel/panel/auth.py | 20 ++++++++++---------- hiddifypanel/panel/hiddify.py | 5 +++++ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/hiddifypanel/hutils/utils.py b/hiddifypanel/hutils/utils.py index c720e3ccb..4581c752e 100644 --- a/hiddifypanel/hutils/utils.py +++ b/hiddifypanel/hutils/utils.py @@ -1,6 +1,5 @@ import base64 -from typing import Tuple -from strenum import StrEnum +from typing import Any, Tuple from urllib.parse import urlparse from uuid import UUID from flask_babelex import lazy_gettext as _ @@ -181,13 +180,23 @@ def parse_basic_auth_header(auth_header: str) -> tuple[str, str] | None: return (username, password) if username and password else None -def parse_auth_id(raw_id) -> Tuple[type | None, str | None]: +def parse_auth_id(raw_id) -> Tuple[Any | None, str | None]: + """ + Parses the given raw ID to extract the account type and ID. + Args: + raw_id (str): The raw ID to be parsed. + Returns: + Tuple[Any | None, str | None]: A tuple containing the account type and ID. + The account type is of type Role (either Role.user or Role.admin) + and the ID is a string. If the raw ID cannot be parsed, None is returned + for both the account type and ID. + """ splitted = raw_id.split('_') if len(splitted) < 2: return None, None admin_or_user, id = splitted - from hiddifypanel.models import AdminUser, User - account_type = type(User) if admin_or_user == 'user' else type(AdminUser) + from hiddifypanel.models.role import Role + account_type = Role.user if admin_or_user == 'user' else Role.admin if not id or not account_type: return None, None return account_type, id diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index cd5c75c97..57ff6bffa 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -6,6 +6,7 @@ from flask import current_app from hiddifypanel.models import AdminUser, User, get_admin_by_uuid, Role from hiddifypanel.models.user import get_user_by_uuid +import hiddifypanel.panel.hiddify as hiddify def init_app(app): @@ -15,15 +16,15 @@ def init_app(app): @login_manager.user_loader def user_loader_auth(id: str) -> User | AdminUser | None: # first of all check if user sent Authorization header, our priority is with Authorization header - if request.headers.get("Authorization"): + if not hiddify.is_api_call(request.path) and request.headers.get("Authorization"): return request_loader_auth(request) # parse id - account_type, id = hutils.utils.parse_auth_id(id) # type: ignore - if not account_type or not id: + role, id = hutils.utils.parse_auth_id(id) # type: ignore + if not role or not id: return - if account_type == type(AdminUser): + if role == Role.admin: account = AdminUser.query.filter(AdminUser.id == id).first() else: account = User.query.filter(User.id == id).first() @@ -41,13 +42,12 @@ def request_loader_auth(request) -> User | AdminUser | None: return account = None - is_api_request = False - if not request.blueprint: - return - if 'api' in request.blueprint: + is_api_call = False + + if hiddify.is_api_call(request.path): if apikey := hutils.utils.get_apikey_from_auth_header(auth_header): account = get_user_by_uuid(apikey) or get_admin_by_uuid(apikey) - is_api_request = True + is_api_call = True else: if username_password := hutils.utils.parse_basic_auth_header(auth_header): if request.blueprint == 'user2': @@ -59,7 +59,7 @@ def request_loader_auth(request) -> User | AdminUser | None: g.account = account g.account_uuid = account.uuid g.is_admin = False if account.role == 'user' else True - if not is_api_request: + if not is_api_call: login_user(account) return account diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 3a55d1a9b..692004ff7 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -1,4 +1,5 @@ import glob +import json import subprocess import psutil from typing import Tuple @@ -115,6 +116,10 @@ def current_account_user_pass(): return g.account.username, g.account.password +def is_api_call(req_path: str): + return 'api/v1/' in req_path or 'api/v2/' in req_path + + def abs_url(path): return f"/{g.proxy_path}/{g.account_uuid}/{path}" From c2d05f25122fec80c70fca8b791b078270d12c84 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 061/112] new: add login_required to user's apis --- .../panel/commercial/restapi/v2/admin/admin_info_api.py | 5 +++-- .../panel/commercial/restapi/v2/admin/admin_user_api.py | 5 ++--- .../panel/commercial/restapi/v2/admin/admin_users_api.py | 4 +++- .../commercial/restapi/v2/admin/server_status_api.py | 4 +++- .../panel/commercial/restapi/v2/admin/user_api.py | 5 ++--- .../panel/commercial/restapi/v2/admin/users_api.py | 4 +++- hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py | 8 ++++---- .../panel/commercial/restapi/v2/user/configs_api.py | 5 +++-- hiddifypanel/panel/commercial/restapi/v2/user/info_api.py | 5 +++-- .../panel/commercial/restapi/v2/user/mtproxies.py | 5 +++-- .../panel/commercial/restapi/v2/user/short_api.py | 4 +++- 11 files changed, 32 insertions(+), 22 deletions(-) diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py index 38749f596..9dfa0bfed 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py @@ -2,16 +2,17 @@ from flask import g from flask.views import MethodView from apiflask import abort - +from hiddifypanel.panel.auth import login_required from hiddifypanel.models.admin import AdminUser, get_admin_user_db from hiddifypanel.models.config_enum import ConfigEnum, Lang from hiddifypanel.models.config import hconfig +from hiddifypanel.models.role import Role from hiddifypanel.panel import hiddify from .admin_user_api import AdminSchema class AdminInfoApi(MethodView): - decorators = [hiddify.admin] + decorators = [login_required({Role.admin})] @app.output(AdminSchema) def get(self): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py index 918588401..f40465cb7 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_user_api.py @@ -4,8 +4,7 @@ from flask.views import MethodView from apiflask import Schema from apiflask import abort - - +from hiddifypanel.panel.auth import login_required from hiddifypanel.models import * from hiddifypanel.panel import hiddify from hiddifypanel.models import AdminMode, Lang @@ -25,7 +24,7 @@ class AdminSchema(Schema): class AdminUserApi(MethodView): - decorators = [hiddify.admin] + decorators = [login_required({Role.admin})] @app.output(AdminSchema) def get(self, uuid): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py index 04c9da136..950a09a0b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_users_api.py @@ -1,13 +1,15 @@ from flask import current_app as app from apiflask import abort from flask.views import MethodView +from hiddifypanel.panel.auth import login_required +from hiddifypanel.models.role import Role from .admin_user_api import AdminSchema from hiddifypanel.models import AdminUser from hiddifypanel.panel import hiddify class AdminUsersApi(MethodView): - decorators = [hiddify.admin] + decorators = [login_required({Role.admin})] @app.output(AdminSchema(many=True)) def get(self): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py index d2381a657..4a399b6f6 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/server_status_api.py @@ -3,6 +3,8 @@ from flask.views import MethodView from apiflask.fields import Dict from apiflask import Schema +from hiddifypanel.panel.auth import login_required +from hiddifypanel.models.role import Role from hiddifypanel.panel import hiddify from hiddifypanel.models import get_daily_usage_stats @@ -13,7 +15,7 @@ class ServerStatus(Schema): class AdminServerStatusApi(MethodView): - decorators = [hiddify.admin] + decorators = [login_required({Role.admin})] @app.output(ServerStatus) def get(self): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py index 3779326f6..6f634e478 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py @@ -2,8 +2,7 @@ from flask.views import MethodView from flask import current_app as app from apiflask import abort, Schema - - +from hiddifypanel.panel.auth import login_required from hiddifypanel.models import * from hiddifypanel.panel import hiddify from hiddifypanel.drivers import user_driver @@ -67,7 +66,7 @@ class UserSchema(Schema): class UserApi(MethodView): - decorators = [hiddify.admin] + decorators = [login_required({Role.admin})] @app.output(UserSchema) def get(self, uuid): diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py index 81793233e..e088da26b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/users_api.py @@ -1,6 +1,8 @@ from flask.views import MethodView from flask import current_app as app from apiflask import abort +from hiddifypanel.panel.auth import login_required +from hiddifypanel.models.role import Role from hiddifypanel.models.user import get_user_by_uuid from hiddifypanel.panel import hiddify from hiddifypanel.drivers import user_driver @@ -9,7 +11,7 @@ class UsersApi(MethodView): - decorators = [hiddify.admin] + decorators = [login_required({Role.admin})] @app.output(UserSchema(many=True)) def get(self): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py index 4e41b8946..44eef941e 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py @@ -10,10 +10,10 @@ import user_agents from strenum import StrEnum from enum import auto - - from hiddifypanel.panel.user.user import get_common_data from hiddifypanel.hutils.utils import get_latest_release_url, do_base_64 +from hiddifypanel.models.role import Role +from hiddifypanel.panel.auth import login_required # region App Api DTOs @@ -66,7 +66,7 @@ class AppInSchema(Schema): class AppAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [login_required({Role.user})] def __init__(self) -> None: super().__init__() @@ -88,7 +88,7 @@ def __init__(self) -> None: @app.input(AppInSchema, arg_name='data', location="query") @app.output(AppSchema(many=True)) - @hiddify.user_auth + @login_required({Role.user}) def get(self, data): # parse user agent if data['platform'] == Platform.auto: diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py index 17528990e..7a49ae63b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py @@ -2,9 +2,10 @@ from flask import g, request from flask import current_app as app from flask.views import MethodView +from hiddifypanel.panel.auth import login_required from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum -from hiddifypanel.panel import hiddify +from hiddifypanel.models.role import Role from apiflask import Schema from apiflask.fields import String from hiddifypanel.panel.user.user import get_common_data @@ -22,7 +23,7 @@ class ConfigSchema(Schema): class AllConfigsAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [login_required({Role.user})] @app.output(ConfigSchema(many=True)) def get(self): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py index 10878358d..f845317d7 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py @@ -4,12 +4,13 @@ from apiflask.fields import Integer, String, Float, URL, Enum from flask import g, request from flask import current_app as app +from hiddifypanel.panel.auth import login_required from hiddifypanel.models import Lang +from hiddifypanel.models.role import Role from hiddifypanel.models.user import days_to_reset, get_user_by_uuid from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum -from hiddifypanel.panel import hiddify from hiddifypanel.panel.database import db from hiddifypanel.panel.user.user import get_common_data @@ -37,7 +38,7 @@ class UserInfoChangableSchema(Schema): class InfoAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [login_required({Role.user})] @app.output(ProfileSchema) def get(self): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py index 1b7be65a4..988aaac9b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py @@ -3,13 +3,14 @@ from flask import g from flask import current_app as app from apiflask.fields import String +from hiddifypanel.panel.auth import login_required from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum from hiddifypanel.models.domain import DomainType +from hiddifypanel.models.role import Role from hiddifypanel.panel.user.user import get_common_data -from hiddifypanel.panel import hiddify class MtproxySchema(Schema): @@ -18,7 +19,7 @@ class MtproxySchema(Schema): class MTProxiesAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [login_required({Role.user})] @app.output(MtproxySchema(many=True)) def get(self): diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py index d727ae5a9..7f05b753f 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py @@ -5,8 +5,10 @@ from apiflask.fields import String, Integer from flask import current_app as app from flask.views import MethodView +from hiddifypanel.panel.auth import login_required from hiddifypanel.models.config import hconfig from hiddifypanel.models.config_enum import ConfigEnum +from hiddifypanel.models.role import Role from hiddifypanel.panel import hiddify @@ -17,7 +19,7 @@ class ShortSchema(Schema): class ShortAPI(MethodView): - decorators = [hiddify.user_auth] + decorators = [login_required({Role.user})] @app.output(ShortSchema) def get(self): From 997e43167f8d4b26117162dc802a2da052cd497c Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 062/112] add: login_required to quicksetup --- hiddifypanel/panel/admin/QuickSetup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hiddifypanel/panel/admin/QuickSetup.py b/hiddifypanel/panel/admin/QuickSetup.py index 0b3255e8c..7cc8e558b 100644 --- a/hiddifypanel/panel/admin/QuickSetup.py +++ b/hiddifypanel/panel/admin/QuickSetup.py @@ -1,3 +1,4 @@ +from hiddifypanel.panel.auth import login_required from hiddifypanel import hutils from hiddifypanel.models import * import flask_babel @@ -9,13 +10,12 @@ from flask_wtf import FlaskForm from flask_bootstrap import SwitchField from hiddifypanel.panel import hiddify -from flask_admin.base import expose # from gettext import gettext as _ -from wtforms.validators import Regexp, ValidationError +from wtforms.validators import ValidationError import re -from flask import render_template, current_app, Markup, redirect, url_for -from hiddifypanel.models import User, Domain, DomainType, StrConfig, ConfigEnum, get_hconfigs +from flask import render_template +from hiddifypanel.models import Domain, DomainType, StrConfig, ConfigEnum, get_hconfigs from hiddifypanel.panel.database import db from wtforms.fields import * @@ -24,7 +24,7 @@ class QuickSetup(FlaskView): - decorators = [hiddify.admin] + decorators = [login_required({Role.super_admin})] def index(self): return render_template( From 8e09af0ef38e3490878c5aacec2ae82b9fcda156 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:06 +0330 Subject: [PATCH 063/112] fix: bug --- hiddifypanel/panel/hiddify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 692004ff7..11c7a6edb 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -175,7 +175,7 @@ def quick_apply_users(): # run install.sh apply_users commander(Command.apply_users) - time.sleep(1) + # time.sleep(1) return {"status": 'success'} From 46f35d75b2a1e1834a0f4895f6d06171ad4f6f6c Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 064/112] fix: fill username & password in user/admin creation --- hiddifypanel/models/admin.py | 48 +++++++++--------------------------- hiddifypanel/models/user.py | 48 +++++++++--------------------------- hiddifypanel/models/utils.py | 42 +++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 72 deletions(-) create mode 100644 hiddifypanel/models/utils.py diff --git a/hiddifypanel/models/admin.py b/hiddifypanel/models/admin.py index bbe88c125..b37698bf0 100644 --- a/hiddifypanel/models/admin.py +++ b/hiddifypanel/models/admin.py @@ -5,8 +5,10 @@ from hiddifypanel import hutils from hiddifypanel.models.role import Role from hiddifypanel.models.usage import DailyUsage +from hiddifypanel.models.utils import fill_username, fill_password from sqlalchemy_serializer import SerializerMixin from flask_login import UserMixin as FlaskLoginUserMixin +from sqlalchemy import event from strenum import StrEnum from hiddifypanel.panel.database import db @@ -62,6 +64,12 @@ def role(self): def get_id(self): return f'admin_{self.id}' + def is_username_unique(self): + return AdminUser.query.filter_by(username=self.username).first() is None + + def is_password_unique(self): + return AdminUser.query.filter_by(password=self.password).first() is None + def recursive_users_query(self): from .user import User admin_ids = self.recursive_sub_admins_ids() @@ -197,39 +205,7 @@ def current_admin_or_owner(): return AdminUser.query.filter(AdminUser.id == 1).first() -def fill_username(admin: AdminUser) -> None: - minimum_username_length = 10 - if not admin.username or len(admin.username) < 10: - base_username = '' - rand_str = '' - # if the username chats isn't only string.ascii_letters, it's invalid - # because we can't set non ascii characters in the http header (https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication) - if admin.name: - # user actual name - base_username = admin.name.replace(' ', '_') - if len(base_username) > minimum_username_length - 1: - # check if the name is unique, if it's not we add some random char to it - while AdminUser.query.filter(AdminUser.username == base_username + rand_str).first(): - rand_str = hutils.utils.get_random_string(2, 4) - else: - needed_length = minimum_username_length - len(base_username) - rand_str = hutils.utils.get_random_string(needed_length, needed_length) - while AdminUser.query.filter(AdminUser.username == base_username + rand_str).first(): - rand_str = hutils.utils.get_random_string(needed_length, needed_length) - else: - base_username = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) - while AdminUser.query.filter(AdminUser.username == base_username + rand_str).first(): - rand_str = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) - - admin.username = base_username + rand_str if rand_str else base_username - - -def fill_password(admin: AdminUser) -> None: - # TODO: hash the password - if not admin.password or len(admin.password) < 16: - base_passwd = hutils.utils.get_random_password() - rand_str = '' - # if passwd is duplicated, we create another one - while AdminUser.query.filter(AdminUser.password == base_passwd + rand_str).first(): - rand_str = hutils.utils.get_random_password() - admin.password = base_passwd + rand_str +@event.listens_for(AdminUser, "before_insert") +def before_insert(mapper, connection, target): + fill_username(target) + fill_password(target) diff --git a/hiddifypanel/models/user.py b/hiddifypanel/models/user.py index 43183f554..454d8a5e3 100644 --- a/hiddifypanel/models/user.py +++ b/hiddifypanel/models/user.py @@ -7,10 +7,12 @@ from hiddifypanel import hutils from sqlalchemy_serializer import SerializerMixin from strenum import StrEnum +from sqlalchemy import event from hiddifypanel.panel.database import db from hiddifypanel.models import Lang from hiddifypanel.models.role import Role +from hiddifypanel.models.utils import fill_password, fill_username ONE_GIG = 1024*1024*1024 @@ -120,6 +122,12 @@ def role(self): def get_id(self): return f'user_{self.id}' + def is_username_unique(self): + return User.query.filter_by(username=self.username).first() is None + + def is_password_unique(self): + return User.query.filter_by(password=self.password).first() is None + @staticmethod def by_id(user_id): """ @@ -369,39 +377,7 @@ def bulk_register_users(users=[], commit=True, remove=False): db.session.commit() -def fill_username(user: User) -> None: - minimum_username_length = 10 - if not user.username or len(user.username) < 10: - base_username = '' - rand_str = '' - # if the username chats isn't only string.ascii_letters, it's invalid - # because we can't set non ascii characters in the http header (https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication) - if user.name: - # user actual name - base_username = user.name.replace(' ', '_') - if len(base_username) > minimum_username_length - 1: - # check if the name is unique, if it's not we add some random char to it - while User.query.filter(User.username == base_username + rand_str).first(): - rand_str = hutils.utils.get_random_string(2, 4) - else: - needed_length = minimum_username_length - len(base_username) - rand_str = hutils.utils.get_random_string(needed_length, needed_length) - while User.query.filter(User.username == base_username + rand_str).first(): - rand_str = hutils.utils.get_random_string(needed_length, needed_length) - else: - base_username = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) - while User.query.filter(User.username == base_username + rand_str).first(): - rand_str = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) - - user.username = base_username + rand_str if rand_str else base_username - - -def fill_password(user: User) -> None: - # TODO: hash the password - if not user.password or len(user.password) < 16: - base_passwd = hutils.utils.get_random_password() - rand_str = '' - # if passwd is duplicated, we create another one - while User.query.filter(User.password == base_passwd + rand_str).first(): - rand_str = hutils.utils.get_random_password() - user.password = base_passwd + rand_str +@event.listens_for(User, 'before_insert') +def on_user_insert(mapper, connection, target): + fill_username(target) + fill_password(target) diff --git a/hiddifypanel/models/utils.py b/hiddifypanel/models/utils.py new file mode 100644 index 000000000..5e2de8593 --- /dev/null +++ b/hiddifypanel/models/utils.py @@ -0,0 +1,42 @@ + + +from hiddifypanel import hutils + + +def fill_username(model) -> None: + minimum_username_length = 10 + if not model.username or len(model.username) < 10: + base_username = '' + rand_str = '' + # if the username chats isn't only string.ascii_letters, it's invalid + # because we can't set non ascii characters in the http header (https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication) + if model.name: + # user actual name + base_username = model.name.replace(' ', '_') + if len(base_username) > minimum_username_length - 1: + # check if the name is unique, if it's not we add some random char to it + model.username = base_username + rand_str + while not model.is_username_unique(): + rand_str = hutils.utils.get_random_string(2, 4) + else: + needed_length = minimum_username_length - len(base_username) + rand_str = hutils.utils.get_random_string(needed_length, needed_length) + model.username = base_username + rand_str + while not model.is_username_unique(): + rand_str = hutils.utils.get_random_string(needed_length, needed_length) + else: + base_username = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) + model.username = base_username + rand_str + while not model.is_username_unique(): + rand_str = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) + + +def fill_password(model) -> None: + # TODO: hash the password + if not model.password or len(model.password) < 16: + base_passwd = hutils.utils.get_random_password() + rand_str = '' + # if passwd is duplicated, we create another one + model.password = base_passwd + rand_str + while not model.is_password_unique(): + rand_str = hutils.utils.get_random_password() From f83c0572a762bf6c36af11caaacd98e3c2d66f45 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 065/112] fix: init_db filling user/admin username|passowrd fields --- hiddifypanel/models/utils.py | 2 -- hiddifypanel/panel/init_db.py | 9 +++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/hiddifypanel/models/utils.py b/hiddifypanel/models/utils.py index 5e2de8593..cc13b7475 100644 --- a/hiddifypanel/models/utils.py +++ b/hiddifypanel/models/utils.py @@ -1,5 +1,3 @@ - - from hiddifypanel import hutils diff --git a/hiddifypanel/panel/init_db.py b/hiddifypanel/panel/init_db.py index 1c6e9f736..09a930026 100644 --- a/hiddifypanel/panel/init_db.py +++ b/hiddifypanel/panel/init_db.py @@ -13,6 +13,7 @@ from hiddifypanel.panel import hiddify from hiddifypanel.panel.database import db from hiddifypanel.panel.hiddify import get_random_domains, get_random_string +import hiddifypanel.models.utils as model_utils MAX_DB_VERSION = 60 @@ -147,13 +148,13 @@ def init_db(): def _v59(): # set user model username and password for u in User.query.all(): - user.fill_username(u) - user.fill_password(u) + model_utils.fill_username(u) + model_utils.fill_password(u) # set admin model username and password for a in AdminUser.query.all(): - admin.fill_username(a) - admin.fill_password(a) + model_utils.fill_username(u) + model_utils.fill_password(u) def __fix_username_and_password(model_type: AdminUser | User): From b9fca95bd7e28ef50e292dcb4bff903210f07842 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 066/112] clean up --- hiddifypanel/models/utils.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/hiddifypanel/models/utils.py b/hiddifypanel/models/utils.py index cc13b7475..0067c3dc1 100644 --- a/hiddifypanel/models/utils.py +++ b/hiddifypanel/models/utils.py @@ -3,12 +3,15 @@ def fill_username(model) -> None: minimum_username_length = 10 - if not model.username or len(model.username) < 10: + if not model.username or len(model.username) < minimum_username_length: base_username = '' rand_str = '' - # if the username chats isn't only string.ascii_letters, it's invalid - # because we can't set non ascii characters in the http header (https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication) - if model.name: + if not model.name: + base_username = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) + model.username = base_username + rand_str + while not model.is_username_unique(): + rand_str = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) + else: # user actual name base_username = model.name.replace(' ', '_') if len(base_username) > minimum_username_length - 1: @@ -22,11 +25,6 @@ def fill_username(model) -> None: model.username = base_username + rand_str while not model.is_username_unique(): rand_str = hutils.utils.get_random_string(needed_length, needed_length) - else: - base_username = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) - model.username = base_username + rand_str - while not model.is_username_unique(): - rand_str = hutils.utils.get_random_string(minimum_username_length, minimum_username_length) def fill_password(model) -> None: From 7fdf6eeeeb2c06886b694642ce9d2adfdd717d2f Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 067/112] chg: better names --- hiddifypanel/panel/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index 57ff6bffa..04cf001b8 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -14,10 +14,10 @@ def init_app(app): login_manager.init_app(app) @login_manager.user_loader - def user_loader_auth(id: str) -> User | AdminUser | None: + def cookie_auth(id: str) -> User | AdminUser | None: # first of all check if user sent Authorization header, our priority is with Authorization header if not hiddify.is_api_call(request.path) and request.headers.get("Authorization"): - return request_loader_auth(request) + return header_auth(request) # parse id role, id = hutils.utils.parse_auth_id(id) # type: ignore @@ -36,7 +36,7 @@ def user_loader_auth(id: str) -> User | AdminUser | None: return account @login_manager.request_loader - def request_loader_auth(request) -> User | AdminUser | None: + def header_auth(request) -> User | AdminUser | None: auth_header: str = request.headers.get("Authorization") if not auth_header: return From 5bc5b2ad9935601a40b795080fa80ab52d61c535 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 068/112] fix: typo del: unused function --- hiddifypanel/panel/init_db.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/hiddifypanel/panel/init_db.py b/hiddifypanel/panel/init_db.py index 09a930026..e976e42db 100644 --- a/hiddifypanel/panel/init_db.py +++ b/hiddifypanel/panel/init_db.py @@ -153,13 +153,8 @@ def _v59(): # set admin model username and password for a in AdminUser.query.all(): - model_utils.fill_username(u) - model_utils.fill_password(u) - - -def __fix_username_and_password(model_type: AdminUser | User): - - pass + model_utils.fill_username(a) + model_utils.fill_password(a) def _v57(): From f8232b022bec0b130bf22c4389bb5bd5f60f8061 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 069/112] fix: typo & imports --- hiddifypanel/panel/admin/ConfigAdmin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hiddifypanel/panel/admin/ConfigAdmin.py b/hiddifypanel/panel/admin/ConfigAdmin.py index bc3d873b2..4c6010dca 100644 --- a/hiddifypanel/panel/admin/ConfigAdmin.py +++ b/hiddifypanel/panel/admin/ConfigAdmin.py @@ -1,6 +1,8 @@ import re import uuid +from hiddifypanel.models.role import Role from hiddifypanel.panel import hiddify +from hiddifypanel.panel.auth import login_required from wtforms.validators import ValidationError @@ -45,7 +47,7 @@ def on_model_change(self, form, model, is_created): if not self._is_valid_uuid(model.value): raise ValidationError('Invalid UUID e.g.,' + str(uuid.uuid4())) - if model.key in [ConfigEnum.telegratelem_secret]: + if model.key in [ConfigEnum.telegram_secret]: if not re.match("^[0-9a-fA-F]{32}$", model.value): raise ValidationError('Invalid UUID e.g.,' + uuid.uuid4().hex) From 80453e354da10fff36da00caedbc295a25033c5c Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 070/112] chg: basic athentication realm value --- hiddifypanel/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hiddifypanel/base.py b/hiddifypanel/base.py index acaad36b1..3fb672268 100644 --- a/hiddifypanel/base.py +++ b/hiddifypanel/base.py @@ -114,7 +114,7 @@ def check_csrf(): def apply_no_robot(response): response.headers["X-Robots-Tag"] = "noindex, nofollow" if response.status_code == 401: - response.headers['WWW-Authenticate'] = 'Basic realm="Hiddify Secret Space"' + response.headers['WWW-Authenticate'] = 'Basic realm="Hiddify"' return response app.jinja_env.globals['get_locale'] = get_locale From ff0ed17a780bfaf238d51737795982dc3f2eb9ee Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 071/112] fix: better account type selection in parse_auth_id --- hiddifypanel/hutils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hiddifypanel/hutils/utils.py b/hiddifypanel/hutils/utils.py index 4581c752e..ed8cfb299 100644 --- a/hiddifypanel/hutils/utils.py +++ b/hiddifypanel/hutils/utils.py @@ -196,7 +196,7 @@ def parse_auth_id(raw_id) -> Tuple[Any | None, str | None]: return None, None admin_or_user, id = splitted from hiddifypanel.models.role import Role - account_type = Role.user if admin_or_user == 'user' else Role.admin + account_type = Role.admin if admin_or_user == 'admin' else Role.user if not id or not account_type: return None, None return account_type, id From 6af56acbd489f39b692b94b99142abda86637918 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 072/112] fix: domain in api --- hiddifypanel/panel/commercial/restapi/v2/user/info_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py index f845317d7..db3bea807 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py @@ -47,7 +47,7 @@ def get(self): dto = ProfileSchema() # user is exist for sure dto.profile_title = c['user'].name - dto.profile_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/#{g.account.name}" + dto.profile_url = f"https://{g.account.username}:{g.account.password}@{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/#{g.account.name}" dto.profile_usage_current = g.account.current_usage_GB dto.profile_usage_total = g.account.usage_limit_GB dto.profile_remaining_days = g.account.remaining_days @@ -58,7 +58,7 @@ def get(self): dto.admin_message_url = hconfig(ConfigEnum.branding_site) dto.brand_title = hconfig(ConfigEnum.branding_title) dto.brand_icon_url = "" - dto.doh = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/dns/dns-query" + dto.doh = f"https://{g.account.username}:{g.account.password}@{urlparse(request.base_url).hostname}/{g.proxy_path}/dns/dns-query" dto.lang = c['user'].lang return dto From a2353b016d761a15dd71402f50d860037a1d7862 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 073/112] fix: disable CORS for all endpoints and enable it just for adminlog api for now --- hiddifypanel/base.py | 2 -- .../panel/commercial/restapi/v2/admin/admin_log_api.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/hiddifypanel/base.py b/hiddifypanel/base.py index 3fb672268..37c627504 100644 --- a/hiddifypanel/base.py +++ b/hiddifypanel/base.py @@ -20,8 +20,6 @@ def create_app(cli=False, **config): app = APIFlask(__name__, static_url_path="//static/", instance_relative_config=True, version='2.0.0', title="Hiddify API", openapi_blueprint_url_prefix="//api", docs_ui='elements', json_errors=False, enable_openapi=True) - # setup CORS for javascript calls - cors = CORS(app) # app = Flask(__name__, static_url_path="//static/", instance_relative_config=True) app.wsgi_app = ProxyFix( app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py index ce074deee..44d3808fb 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_log_api.py @@ -18,7 +18,8 @@ class AdminLogApi(MethodView): @app.input(AdminLogfileSchema, arg_name="data", location='form') @app.output(fields.String(description="The html of the log")) - @cross_origin() + # enable CORS for javascript calls + @cross_origin(supports_credentials=True) def post(self, data): file = data.get('file') or abort(400, "Parameter issue: 'file'") file_path = f"{app.config['HIDDIFY_CONFIG_PATH']}/log/system/{file}" From de68dad665a9df2b024b5ff38115a099405bcb12 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 074/112] chg: better readability --- hiddifypanel/panel/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index 04cf001b8..12ab77170 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -16,7 +16,7 @@ def init_app(app): @login_manager.user_loader def cookie_auth(id: str) -> User | AdminUser | None: # first of all check if user sent Authorization header, our priority is with Authorization header - if not hiddify.is_api_call(request.path) and request.headers.get("Authorization"): + if request.headers.get("Authorization") and not hiddify.is_api_call(request.path): return header_auth(request) # parse id From 9fbf1f4b5716f27709ffed96157ccc8a3ed18ec5 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 075/112] chg: send api calls with cookie instead of ApiKey --- hiddifypanel/panel/admin/Actions.py | 14 +++++--------- hiddifypanel/panel/admin/templates/result.html | 8 ++++---- hiddifypanel/panel/admin/templates/view_logs.html | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/hiddifypanel/panel/admin/Actions.py b/hiddifypanel/panel/admin/Actions.py index f4ecfa9e7..9b0b316f7 100644 --- a/hiddifypanel/panel/admin/Actions.py +++ b/hiddifypanel/panel/admin/Actions.py @@ -55,7 +55,7 @@ def viewlogs(self): log_files = [] for filename in sorted(os.listdir(f'{config_dir}/log/system/')): log_files.append(filename) - return render_template('view_logs.html', log_files=log_files, api_key=hiddify.current_account_api_key()) + return render_template('view_logs.html', log_files=log_files) @login_required(roles={Role.super_admin}) @route('apply_configs', methods=['POST']) @@ -85,8 +85,7 @@ def reset2(self): log_file_url=get_log_api_url(), log_file='restart.log', show_success=True, - domains=get_domains(), - api_key=hiddify.current_account_api_key()) + domains=get_domains()) @login_required(roles={Role.super_admin}) @route('reinstall', methods=['POST']) @@ -134,8 +133,7 @@ def reinstall2(self, complete_install=True, domain_changed=False): log_file_url=get_log_api_url(), log_file="0-install.log", show_success=True, - domains=get_domains(), - api_key=hiddify.current_account_api_key()) + domains=get_domains()) # subprocess.Popen(f"sudo {config['HIDDIFY_CONFIG_PATH']}/{file} --no-gui".split(" "), cwd=f"{config['HIDDIFY_CONFIG_PATH']}", start_new_session=True) @@ -164,8 +162,7 @@ def status(self): log_file_url=get_log_api_url(), log_file="status.log", show_success=False, - domains=get_domains(), - api_key=hiddify.current_account_api_key()) + domains=get_domains()) @route('update', methods=['POST']) @login_required(roles={Role.super_admin}) @@ -183,8 +180,7 @@ def update2(self): show_success=True, log_file_url=get_log_api_url(), log_file="update.log", - domains=get_domains(), - api_key=hiddify.current_account_api_key()) + domains=get_domains()) def get_some_random_reality_friendly_domain(self): test_domain = request.args.get("test_domain") diff --git a/hiddifypanel/panel/admin/templates/result.html b/hiddifypanel/panel/admin/templates/result.html index d9238e6da..07e14e3a2 100644 --- a/hiddifypanel/panel/admin/templates/result.html +++ b/hiddifypanel/panel/admin/templates/result.html @@ -67,8 +67,8 @@

      {{_('log')}}

      url: urls[index] + "?random=" + Math.random(), type: 'POST', data: { file: '{{ log_file}}' }, - headers: { - 'Authorization': 'ApiKey {{api_key}}' + xhrFields: { + withCredentials: true }, }).then(response => { @@ -166,8 +166,8 @@

      {{_('log')}}

      url: $('#logbtn').attr('href'), type: 'POST', data: { file: '{{ log_file}}' }, - beforeSend: function (xhr) { - xhr.setRequestHeader('Authorization', 'ApiKey {{api_key}}') + xhrFields: { + withCredentials: true } }).then(response => { Promise.resolve(response) diff --git a/hiddifypanel/panel/admin/templates/view_logs.html b/hiddifypanel/panel/admin/templates/view_logs.html index c9db98d02..bc027f9f6 100644 --- a/hiddifypanel/panel/admin/templates/view_logs.html +++ b/hiddifypanel/panel/admin/templates/view_logs.html @@ -15,8 +15,8 @@

      url: $(this).attr("href"), type: 'POST', data: { file: $(this).text() }, - beforeSend: function (xhr) { - xhr.setRequestHeader('Authorization', 'ApiKey {{api_key}}') + xhrFields: { + withCredentials: true } }).then(response => { Promise.resolve(response) From 2d364f621176cd92b1d1a9398845388577f22bf7 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 076/112] chg: using server_status log api --- hiddifypanel/panel/admin/Dashboard.py | 19 ++++++++++--------- hiddifypanel/panel/admin/templates/index.html | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/hiddifypanel/panel/admin/Dashboard.py b/hiddifypanel/panel/admin/Dashboard.py index cdfd04c37..557d46aff 100644 --- a/hiddifypanel/panel/admin/Dashboard.py +++ b/hiddifypanel/panel/admin/Dashboard.py @@ -15,16 +15,17 @@ class Dashboard(FlaskView): - @login_required(roles={Role.admin}) - def get_data(self): - admin_id = request.args.get("admin_id") or g.account.id - if admin_id not in g.account.recursive_sub_admins_ids(): - abort(403, _("Access Denied!")) + # TODO: delete this method + # @login_required(roles={Role.admin}) + # def get_data(self): + # admin_id = request.args.get("admin_id") or g.account.id + # if admin_id not in g.account.recursive_sub_admins_ids(): + # abort(403, _("Access Denied!")) - return jsonify(dict( - stats={'system': hiddify.system_stats(), 'top5': hiddify.top_processes()}, - usage_history=get_daily_usage_stats(admin_id) - )) + # return jsonify(dict( + # stats={'system': hiddify.system_stats(), 'top5': hiddify.top_processes()}, + # usage_history=get_daily_usage_stats(admin_id) + # )) @login_required(roles={Role.admin}) def index(self): diff --git a/hiddifypanel/panel/admin/templates/index.html b/hiddifypanel/panel/admin/templates/index.html index f9bf6b885..fd0ff7853 100644 --- a/hiddifypanel/panel/admin/templates/index.html +++ b/hiddifypanel/panel/admin/templates/index.html @@ -276,7 +276,7 @@

      {{title}}

      setInterval(function () { $.ajax({ - url: "{{url_for('admin.Dashboard:get_data',admin_id=request.args.get('admin_id'))}}", + url: "{{url_for('api_admin.AdminServerStatusApi',admin_id=request.args.get('admin_id'))}}", method: "GET", success: function (data) { console.log("Success!"); From c19612d2a2789ff26835d6c65de52ff54916ac1a Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 077/112] chg: authenticate user panel endponit with Authorization header even there is cookie --- hiddifypanel/panel/auth.py | 10 +++++++--- hiddifypanel/panel/hiddify.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index 12ab77170..0cc67f2c0 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -15,9 +15,13 @@ def init_app(app): @login_manager.user_loader def cookie_auth(id: str) -> User | AdminUser | None: - # first of all check if user sent Authorization header, our priority is with Authorization header - if request.headers.get("Authorization") and not hiddify.is_api_call(request.path): - return header_auth(request) + # first of all check if user sent Authorization header, our priority is with Authorization header (this is valid just for non-api requests) + if not hiddify.is_api_call(request.path): + if request.headers.get("Authorization"): + return header_auth(request) + # if client requested user panel, force it login with Authorization header + if hiddify.is_user_panel_call(request): + return header_auth(request) # parse id role, id = hutils.utils.parse_auth_id(id) # type: ignore diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 11c7a6edb..b615fc1a7 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -5,7 +5,7 @@ from typing import Tuple from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import x25519 -from flask import abort, current_app, g, jsonify +from flask import abort, current_app, g, jsonify, request from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ from wtforms.validators import ValidationError @@ -120,6 +120,16 @@ def is_api_call(req_path: str): return 'api/v1/' in req_path or 'api/v2/' in req_path +def is_user_panel_call(request): + # if request.blueprint and request.blueprint == 'user2': + # return True + + user_panel_url = f'{request.host}/{g.proxy_path}/' + if f'{request.host}{request.path}' == user_panel_url: + return True + return False + + def abs_url(path): return f"/{g.proxy_path}/{g.account_uuid}/{path}" From 61b5d2598b7e158fc3ff3bc4159346840eb3faee Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 078/112] add: user/pass to user admin links --- hiddifypanel/panel/admin/UserAdmin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hiddifypanel/panel/admin/UserAdmin.py b/hiddifypanel/panel/admin/UserAdmin.py index b8475080c..4a27963b0 100644 --- a/hiddifypanel/panel/admin/UserAdmin.py +++ b/hiddifypanel/panel/admin/UserAdmin.py @@ -136,7 +136,8 @@ def _name_formatter(view, context, model, name): else: link = ' ' - link += f"{model.name} " + d = get_panel_domains()[0] + link += f"{model.name} " return Markup(extra+link) def _ul_formatter(view, context, model, name): From 5f89280dacf28b2d19af97f5860a37b329fd222d Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 079/112] chg: refactor --- hiddifypanel/hutils/utils.py | 2 +- hiddifypanel/panel/auth.py | 12 ++++++------ hiddifypanel/panel/hiddify.py | 17 ++++++++++++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/hiddifypanel/hutils/utils.py b/hiddifypanel/hutils/utils.py index ed8cfb299..e0adaf9f3 100644 --- a/hiddifypanel/hutils/utils.py +++ b/hiddifypanel/hutils/utils.py @@ -180,7 +180,7 @@ def parse_basic_auth_header(auth_header: str) -> tuple[str, str] | None: return (username, password) if username and password else None -def parse_auth_id(raw_id) -> Tuple[Any | None, str | None]: +def parse_login_id(raw_id) -> Tuple[Any | None, str | None]: """ Parses the given raw ID to extract the account type and ID. Args: diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index 0cc67f2c0..addeda583 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -17,14 +17,14 @@ def init_app(app): def cookie_auth(id: str) -> User | AdminUser | None: # first of all check if user sent Authorization header, our priority is with Authorization header (this is valid just for non-api requests) if not hiddify.is_api_call(request.path): - if request.headers.get("Authorization"): + # if client requested user panel/admin panel, force it login with Authorization header + if hiddify.is_admin_panel_call(request): return header_auth(request) - # if client requested user panel, force it login with Authorization header if hiddify.is_user_panel_call(request): return header_auth(request) # parse id - role, id = hutils.utils.parse_auth_id(id) # type: ignore + role, id = hutils.utils.parse_login_id(id) # type: ignore if not role or not id: return @@ -54,10 +54,10 @@ def header_auth(request) -> User | AdminUser | None: is_api_call = True else: if username_password := hutils.utils.parse_basic_auth_header(auth_header): - if request.blueprint == 'user2': - account = User.query.filter(User.username == username_password[0], User.password == username_password[1]).first() - else: + if hiddify.is_admin_panel_call(request): account = AdminUser.query.filter(AdminUser.username == username_password[0], AdminUser.password == username_password[1]).first() + elif hiddify.is_user_panel_call(request): + account = User.query.filter(User.username == username_password[0], User.password == username_password[1]).first() if account: g.account = account diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index b615fc1a7..7c3696e2a 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -1,11 +1,12 @@ import glob import json import subprocess +import uuid import psutil from typing import Tuple from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import x25519 -from flask import abort, current_app, g, jsonify, request +from flask import abort, current_app, g, jsonify, request, session from flask_babelex import gettext as __ from flask_babelex import lazy_gettext as _ from wtforms.validators import ValidationError @@ -121,8 +122,8 @@ def is_api_call(req_path: str): def is_user_panel_call(request): - # if request.blueprint and request.blueprint == 'user2': - # return True + if request.blueprint and request.blueprint == 'user2': + return True user_panel_url = f'{request.host}/{g.proxy_path}/' if f'{request.host}{request.path}' == user_panel_url: @@ -130,6 +131,16 @@ def is_user_panel_call(request): return False +def is_admin_panel_call(request): + if request.blueprint and request.blueprint == 'admin': + return True + + admin_panel_url = f'{request.host}/{g.proxy_path}/admin/' + if f'{request.host}{request.path}' == admin_panel_url: + return True + return False + + def abs_url(path): return f"/{g.proxy_path}/{g.account_uuid}/{path}" From 7540c400082756d8de800e1025ed6297a4566a0a Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:07 +0330 Subject: [PATCH 080/112] add: user/pass to hiddifypanel admin-links command(cli) --- hiddifypanel/panel/cli.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/hiddifypanel/panel/cli.py b/hiddifypanel/panel/cli.py index f2f20ce6f..2774a227a 100644 --- a/hiddifypanel/panel/cli.py +++ b/hiddifypanel/panel/cli.py @@ -69,15 +69,16 @@ def all_configs(): server_ip = hutils.ip.get_ip(4) configs['admin_path'] = path + owner = get_super_admin() configs['panel_links'] = [] - configs['panel_links'].append(f"http://{server_ip}{path}") - configs['panel_links'].append(f"https://{server_ip}{path}") + configs['panel_links'].append(f"http://{owner.username}:{owner.password}@{server_ip}{path}") + configs['panel_links'].append(f"https://{owner.username}:{owner.password}@{server_ip}{path}") domains = get_panel_domains() # if not any([d for d in domains if 'sslip.io' not in d.domain]): # configs['panel_links'].append(f"https://{server_ip}{path}") for d in domains: - configs['panel_links'].append(f"https://{d.domain}{path}") + configs['panel_links'].append(f"https://{owner.username}:{owner.password}@{d.domain}{path}") print(json.dumps(configs, indent=4)) @@ -94,16 +95,17 @@ def admin_links(): proxy_path = hconfig(ConfigEnum.proxy_path) server_ip = hutils.ip.get_ip(4) - admin_links = f"Not Secure (do not use it- only if others not work):\n http://{server_ip}/{proxy_path}/admin/\n" + owner = get_super_admin() + admin_links = f"Not Secure (do not use it - only if others not work):\n http://{owner.username}:{owner.password}@{server_ip}/{proxy_path}/admin/\n" domains = get_panel_domains() admin_links += f"Secure:\n" if not any([d for d in domains if 'sslip.io' not in d.domain]): - admin_links += f" (not signed) https://{server_ip}/{proxy_path}/admin/\n" + admin_links += f" (not signed) https://{owner.username}:{owner.password}@{server_ip}/{proxy_path}/admin/\n" # domains=[*domains,f'{server_ip}.sslip.io'] for d in domains: - admin_links += f" https://{d.domain}/{proxy_path}/admin/\n" + admin_links += f" https://{owner.username}:{owner.password}@{d.domain}/{proxy_path}/admin/\n" print(admin_links) return admin_links @@ -111,11 +113,11 @@ def admin_links(): def admin_path(): proxy_path = hconfig(ConfigEnum.proxy_path) - admin = Admin.query.filter(Admin.mode == AdminMode.super_admin).first() + admin = AdminUser.query.filter(AdminUser.mode == AdminMode.super_admin).first() if not admin: - db.session.add(Admin(mode=AdminMode.super_admin)) + db.session.add(AdminUser(mode=AdminMode.super_admin)) db.session.commit() - admin = Admin.query.filter(Admin.mode == AdminMode.super_admin).first() + admin = AdminUser.query.filter(AdminUser.mode == AdminMode.super_admin).first() print(f"/{proxy_path}/admin/") From ee36b9edba2c73a5c62ca028f66af1e027020488 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 081/112] refactor --- hiddifypanel/models/admin.py | 2 +- hiddifypanel/panel/hiddify.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/hiddifypanel/models/admin.py b/hiddifypanel/models/admin.py index b37698bf0..9abd79a5c 100644 --- a/hiddifypanel/models/admin.py +++ b/hiddifypanel/models/admin.py @@ -131,7 +131,7 @@ def get_super_admin_secret(): return get_super_admin().uuid -def get_super_admin(): +def get_super_admin() -> AdminUser: admin = AdminUser.query.filter(AdminUser.mode == AdminMode.super_admin).first() if not admin: db.session.add(AdminUser(mode=AdminMode.super_admin, name="Owner")) diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 7c3696e2a..18ee5c94c 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -141,10 +141,6 @@ def is_admin_panel_call(request): return False -def abs_url(path): - return f"/{g.proxy_path}/{g.account_uuid}/{path}" - - def asset_url(path): return f"/{g.proxy_path}/{path}" From 524047da96946dc42886394b7b3cb77b339bccfc Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 082/112] chg: APIs links to new format --- .../panel/commercial/restapi/v2/user/apps_api.py | 14 +++++++------- .../commercial/restapi/v2/user/configs_api.py | 2 +- .../panel/commercial/restapi/v2/user/info_api.py | 2 +- .../panel/commercial/restapi/v2/user/short_api.py | 3 ++- hiddifypanel/panel/hiddify.py | 3 ++- hiddifypanel/panel/user/user.py | 9 +++++---- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py index 44eef941e..0e55d9932 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py @@ -72,19 +72,19 @@ def __init__(self) -> None: super().__init__() self.hiddify_github_repo = 'https://github.com/hiddify' - self.user_panel_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/" + self.user_panel_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/" self.user_panel_encoded_url = quote_plus(self.user_panel_url) c = get_common_data(g.account_uuid, 'new') self.subscription_link_url = f"{self.user_panel_url}all.txt?name={c['db_domain'].alias or c['db_domain'].domain}-{c['asn']}&asn={c['asn']}&mode={c['mode']}" self.subscription_link_encoded_url = do_base_64(self.subscription_link_url) domain = c['db_domain'].alias or c['db_domain'].domain self.profile_title = c['profile_title'] - # self.clash_all_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_all_{domain}-{c['mode']}" - # self.clash_foreign_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_normal_{domain}-{c['mode']}" - # self.clash_blocked_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_lite_{c['db_domain'].alias or c['db_domain'].domain}-{c['mode']}" - # self.clash_meta_all_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/meta/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mall_{domain}-{c['mode']}" - # self.clash_meta_foreign_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/meta/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mnormal_{domain}-{c['mode']}" - self.clash_meta_blocked_sites = f"https://{domain}/{g.proxy_path}/{g.account_uuid}/clash/meta/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mlite_{domain}-{c['mode']}" + # self.clash_all_sites = f"https://{domain}/{g.proxy_path}/clash/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_all_{domain}-{c['mode']}" + # self.clash_foreign_sites = f"https://{domain}/{g.proxy_path}/clash/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_normal_{domain}-{c['mode']}" + # self.clash_blocked_sites = f"https://{domain}/{g.proxy_path}/clash/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_lite_{c['db_domain'].alias or c['db_domain'].domain}-{c['mode']}" + # self.clash_meta_all_sites = f"https://{domain}/{g.proxy_path}/clash/meta/all.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mall_{domain}-{c['mode']}" + # self.clash_meta_foreign_sites = f"https://{domain}/{g.proxy_path}/clash/meta/normal.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mnormal_{domain}-{c['mode']}" + self.clash_meta_blocked_sites = f"https://{domain}/{g.proxy_path}/clash/meta/lite.yml?mode={c['mode']}&asn={c['asn']}&name={c['asn']}_mlite_{domain}-{c['mode']}" @app.input(AppInSchema, arg_name='data', location="query") @app.output(AppSchema(many=True)) diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py index 7a49ae63b..841a891a2 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py @@ -39,7 +39,7 @@ def create_item(name, domain, type, protocol, transport, security, link): return dto items = [] - base_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/" + base_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/" c = get_common_data(g.account_uuid, 'new') # Add Auto diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py index db3bea807..d0e9e63b2 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py @@ -47,7 +47,7 @@ def get(self): dto = ProfileSchema() # user is exist for sure dto.profile_title = c['user'].name - dto.profile_url = f"https://{g.account.username}:{g.account.password}@{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/#{g.account.name}" + dto.profile_url = f"https://{g.account.username}:{g.account.password}@{urlparse(request.base_url).hostname}/{g.proxy_path}/#{g.account.name}" dto.profile_usage_current = g.account.current_usage_GB dto.profile_usage_total = g.account.usage_limit_GB dto.profile_remaining_days = g.account.remaining_days diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py index 7f05b753f..0e2de2f48 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/short_api.py @@ -23,7 +23,8 @@ class ShortAPI(MethodView): @app.output(ShortSchema) def get(self): - short, expire_in = hiddify.add_short_link("/"+hconfig(ConfigEnum.proxy_path)+"/"+str(g.account_uuid)+"/") + # TODO: check credentials of this url, does it work? + short, expire_in = hiddify.add_short_link(f'https://{g.account.username}:{g.account.password}@{urlparse(request.host)}/{hconfig(ConfigEnum.proxy_path)}/#{g.account.name}') full_url = f"https://{urlparse(request.base_url).hostname}/{short}" dto = ShortSchema() dto.full_url = full_url diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 18ee5c94c..950531b52 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -36,6 +36,7 @@ def add_temporary_access(): g.temp_admin_link = temp_admin_link +# with user panel url format we don't really need this function def add_short_link(link: str, period_min: int = 5) -> Tuple[str, int]: short_code, expire_date = add_short_link_imp(link, period_min) return short_code, (expire_date - datetime.now()).seconds @@ -77,7 +78,7 @@ def exec_command(cmd, cwd=None): def user_auth(function): def wrapper(*args, **kwargs): - if g.account_uuid == None: + if g.account.uuid == None: return jsonify({"error": "auth failed"}) if not g.account: return jsonify({"error": "user not found"}) diff --git a/hiddifypanel/panel/user/user.py b/hiddifypanel/panel/user/user.py index e559c7686..3dff810c1 100644 --- a/hiddifypanel/panel/user/user.py +++ b/hiddifypanel/panel/user/user.py @@ -21,19 +21,20 @@ class UserView(FlaskView): # region api @route('/short/') @route('/short') + # TODO: delete this function and use /short/ api instead def short_link(self): - short = hiddify.add_short_link( - "/"+hconfig(ConfigEnum.proxy_path)+"/"+g.account_uuid+"/") + short = hiddify.add_short_link(f'https://{g.account.username}:{g.account.password}@{urlparse(request.base_url).hostname}/{g.proxy_path}/#{g.account.name}') return f"
      https://{urlparse(request.base_url).hostname}/{short}/

      "+_("This link will expire in 5 minutes") @route('/info/') @route('/info') + # TODO: delete this function and use /me/ api instead def info(self): c = get_common_data(g.account_uuid, 'new') data = { 'profile_title': c['profile_title'], - 'profile_url': f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/#{g.account.name}", + 'profile_url': f"https://{g.account.username}:{g.account.password}{urlparse(request.base_url).hostname}/{g.proxy_path}/#{g.account.name}", 'profile_usage_current': g.account.current_usage_GB, 'profile_usage_total': g.account.usage_limit_GB, 'profile_remaining_days': g.account.remaining_days, @@ -82,7 +83,7 @@ def create_item(name, type, domain, protocol, transport, security, link): } items = [] - base_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/{g.account_uuid}/" + base_url = f"https://{g.account.username}:{g.account.password}{urlparse(request.base_url).hostname}/{g.proxy_path}/" c = get_common_data(g.account_uuid, 'new') # Add Auto From 3c7a32028821e9737e030ec9e3d9a1e4316850f4 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 083/112] del: using g.account.uuid instead of g.account_uuid --- hiddifypanel/panel/auth.py | 4 ++-- .../restapi/v2/admin/admin_info_api.py | 4 +--- .../commercial/restapi/v2/user/apps_api.py | 2 +- .../commercial/restapi/v2/user/configs_api.py | 2 +- .../commercial/restapi/v2/user/info_api.py | 8 +++---- .../commercial/restapi/v2/user/mtproxies.py | 4 ++-- hiddifypanel/panel/common.py | 9 ++++---- hiddifypanel/panel/user/link_maker.py | 2 +- .../user/templates/clash_config copy.yml | 2 +- hiddifypanel/panel/user/user.py | 22 +++++++++---------- 10 files changed, 29 insertions(+), 30 deletions(-) diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index addeda583..b1077c181 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -35,7 +35,7 @@ def cookie_auth(id: str) -> User | AdminUser | None: if account: g.account = account - g.account_uuid = account.uuid + # g.account_uuid = account.uuid g.is_admin = False if account.role == Role.user else True return account @@ -61,7 +61,7 @@ def header_auth(request) -> User | AdminUser | None: if account: g.account = account - g.account_uuid = account.uuid + # g.account_uuid = account.uuid g.is_admin = False if account.role == 'user' else True if not is_api_call: login_user(account) diff --git a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py index 9dfa0bfed..144c85c13 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/admin/admin_info_api.py @@ -16,9 +16,7 @@ class AdminInfoApi(MethodView): @app.output(AdminSchema) def get(self): - # in this case g.account_uuid is equal to admin uuid - admin_uuid = g.account_uuid - admin = get_admin_user_db(admin_uuid) or abort(404, "user not found") + admin = get_admin_user_db(g.account.uuid) or abort(404, "user not found") dto = AdminSchema() dto.name = admin.name diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py index 0e55d9932..22e081ad6 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py @@ -74,7 +74,7 @@ def __init__(self) -> None: self.user_panel_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/" self.user_panel_encoded_url = quote_plus(self.user_panel_url) - c = get_common_data(g.account_uuid, 'new') + c = get_common_data(g.account.uuid, 'new') self.subscription_link_url = f"{self.user_panel_url}all.txt?name={c['db_domain'].alias or c['db_domain'].domain}-{c['asn']}&asn={c['asn']}&mode={c['mode']}" self.subscription_link_encoded_url = do_base_64(self.subscription_link_url) domain = c['db_domain'].alias or c['db_domain'].domain diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py index 841a891a2..3c601a9f8 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/configs_api.py @@ -40,7 +40,7 @@ def create_item(name, domain, type, protocol, transport, security, link): items = [] base_url = f"https://{urlparse(request.base_url).hostname}/{g.proxy_path}/" - c = get_common_data(g.account_uuid, 'new') + c = get_common_data(g.account.uuid, 'new') # Add Auto items.append( diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py index d0e9e63b2..fae213b6b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/info_api.py @@ -42,7 +42,7 @@ class InfoAPI(MethodView): @app.output(ProfileSchema) def get(self): - c = get_common_data(g.account_uuid, 'new') + c = get_common_data(g.account.uuid, 'new') dto = ProfileSchema() # user is exist for sure @@ -52,7 +52,7 @@ def get(self): dto.profile_usage_total = g.account.usage_limit_GB dto.profile_remaining_days = g.account.remaining_days dto.profile_reset_days = days_to_reset(g.account) - dto.telegram_bot_url = f"https://t.me/{c['bot'].username}?start={g.account_uuid}" if c['bot'] else "" + dto.telegram_bot_url = f"https://t.me/{c['bot'].username}?start={g.account.uuid}" if c['bot'] else "" dto.telegram_id = c['user'].telegram_id dto.admin_message_html = hconfig(ConfigEnum.branding_freetext) dto.admin_message_url = hconfig(ConfigEnum.branding_site) @@ -70,13 +70,13 @@ def patch(self, data): except: return {'message': 'The telegram id field is invalid'} - user = get_user_by_uuid(g.account_uuid) + user = get_user_by_uuid(g.account.uuid) if user.telegram_id != tg_id: user.telegram_id = tg_id db.session.commit() if data['language']: - user = get_user_by_uuid(g.account_uuid) + user = get_user_by_uuid(g.account.uuid) if user.lang != data['language']: user.lang = data['language'] db.session.commit() diff --git a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py index 988aaac9b..2c94e326b 100644 --- a/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py +++ b/hiddifypanel/panel/commercial/restapi/v2/user/mtproxies.py @@ -24,10 +24,10 @@ class MTProxiesAPI(MethodView): @app.output(MtproxySchema(many=True)) def get(self): # check mtproxie is enable - if not hconfig(ConfigEnum.telegram_enable, g.account_uuid): + if not hconfig(ConfigEnum.telegram_enable, g.account.uuid): abort(status_code=404, message="Telegram mtproxy is not enable") # get domains - c = get_common_data(g.account_uuid, 'new') + c = get_common_data(g.account.uuid, 'new') dtos = [] # TODO: Remove duplicated domains mapped to a same ipv4 and v6 for d in c['domains']: diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index 7292ee4dd..404d570f7 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -95,8 +95,9 @@ def set_default_path_values(spec): @app.url_defaults def add_proxy_path_user(endpoint, values): - if 'user_secret' not in values and hasattr(g, 'user_uuid'): - values['user_secret'] = f'{g.account_uuid}' + # TODO: delete this + # if 'user_secret' not in values and hasattr(g, 'user_uuid'): + # values['user_secret'] = f'{g.account_uuid}' if 'proxy_path' not in values: # values['proxy_path']=f'{g.proxy_path}' values['proxy_path'] = hconfig(ConfigEnum.proxy_path) @@ -247,8 +248,8 @@ def github_issue_details(): def generate_github_issue_link_for_500_error(error, traceback, remove_sensetive_data=True, remove_unrelated_traceback_datails=True): def remove_sensetive_data_from_github_issue_link(issue_link): - if hasattr(g, 'user_uuid') and g.account_uuid: - issue_link.replace(f'{g.account_uuid}', '*******************') + if hasattr(g, 'acount') and hasattr(g.account, 'uuid') and g.account.uuid: + issue_link.replace(f'{g.account.uuid}', '*******************') if hconfig(ConfigEnum.proxy_path) and hconfig(ConfigEnum.proxy_path): issue_link.replace(hconfig(ConfigEnum.proxy_path), '**********') diff --git a/hiddifypanel/panel/user/link_maker.py b/hiddifypanel/panel/user/link_maker.py index 35bf9cd33..e1c8acc1a 100644 --- a/hiddifypanel/panel/user/link_maker.py +++ b/hiddifypanel/panel/user/link_maker.py @@ -110,7 +110,7 @@ def is_tls(): 'port': port, 'server': cdn_forced_host, 'sni': domain_db.servernames if is_cdn and domain_db.servernames else domain, - 'uuid': str(g.account_uuid), + 'uuid': str(g.account.uuid), 'proto': proxy.proto, 'transport': proxy.transport, 'proxy_path': hconfigs[ConfigEnum.proxy_path], diff --git a/hiddifypanel/panel/user/templates/clash_config copy.yml b/hiddifypanel/panel/user/templates/clash_config copy.yml index ff96e9b26..9040077ac 100644 --- a/hiddifypanel/panel/user/templates/clash_config copy.yml +++ b/hiddifypanel/panel/user/templates/clash_config copy.yml @@ -209,7 +209,7 @@ proxies: server: 127.0.0.1 port: 3005 cipher: chacha20-ietf-poly1305 - password: {{g.account_uuid}} + password: {{g.account.uuid}} udp_over_tcp: true % for d in domains % if hconfig(ConfigEnum.tuic_enable) and d.mode !="cdn" and meta_or_normal=='meta' diff --git a/hiddifypanel/panel/user/user.py b/hiddifypanel/panel/user/user.py index 3dff810c1..0a52d2f9b 100644 --- a/hiddifypanel/panel/user/user.py +++ b/hiddifypanel/panel/user/user.py @@ -31,7 +31,7 @@ def short_link(self): @route('/info') # TODO: delete this function and use /me/ api instead def info(self): - c = get_common_data(g.account_uuid, 'new') + c = get_common_data(g.account.uuid, 'new') data = { 'profile_title': c['profile_title'], 'profile_url': f"https://{g.account.username}:{g.account.password}{urlparse(request.base_url).hostname}/{g.proxy_path}/#{g.account.name}", @@ -39,7 +39,7 @@ def info(self): 'profile_usage_total': g.account.usage_limit_GB, 'profile_remaining_days': g.account.remaining_days, 'profile_reset_days': days_to_reset(g.account), - 'telegram_bot_url': f"https://t.me/{c['bot'].username}?start={g.account_uuid}" if c['bot'] else "", + 'telegram_bot_url': f"https://t.me/{c['bot'].username}?start={g.account.uuid}" if c['bot'] else "", 'admin_message_html': hconfig(ConfigEnum.branding_freetext), 'admin_message_url': hconfig(ConfigEnum.branding_site), 'brand_title': hconfig(ConfigEnum.branding_title), @@ -54,7 +54,7 @@ def info(self): @route('/mtproxies') def mtproxies(self): # get domains - c = get_common_data(g.account_uuid, 'new') + c = get_common_data(g.account.uuid, 'new') mtproxies = [] # TODO: Remove duplicated domains mapped to a same ipv4 and v6 for d in c['domains']: @@ -84,7 +84,7 @@ def create_item(name, type, domain, protocol, transport, security, link): items = [] base_url = f"https://{g.account.username}:{g.account.password}{urlparse(request.base_url).hostname}/{g.proxy_path}/" - c = get_common_data(g.account_uuid, 'new') + c = get_common_data(g.account.uuid, 'new') # Add Auto items.append( @@ -232,7 +232,7 @@ def get_proper_config(self): @ route('/auto') @login_required(roles={Role.user}) def auto_select(self): - c = get_common_data(g.account_uuid, mode="new") + c = get_common_data(g.account.uuid, mode="new") user_agent = user_agents.parse(request.user_agent.string) # return render_template('home/handle_smart.html', **c) return render_template('home/auto_page.html', **c, ua=user_agent) @@ -246,7 +246,7 @@ def new(self): if conf: return conf - c = get_common_data(g.account_uuid, mode="new") + c = get_common_data(g.account.uuid, mode="new") user_agent = user_agents.parse(request.user_agent.string) # return render_template('home/multi.html', **c, ua=user_agent) return render_template('new.html', **c, ua=user_agent) @@ -258,7 +258,7 @@ def clash_proxies(self, meta_or_normal="normal"): mode = request.args.get("mode") domain = request.args.get("domain", None) - c = get_common_data(g.account_uuid, mode, filter_domain=domain) + c = get_common_data(g.account.uuid, mode, filter_domain=domain) resp = Response(render_template('clash_proxies.yml', meta_or_normal=meta_or_normal, **c)) resp.mimetype = "text/plain" @@ -306,7 +306,7 @@ def report(self): def clash_config(self, meta_or_normal="normal", typ="all.yml"): mode = request.args.get("mode") - c = get_common_data(g.account_uuid, mode) + c = get_common_data(g.account.uuid, mode) hash_rnd = random.randint(0, 1000000) # hash(f'{c}') if request.method == 'HEAD': @@ -321,7 +321,7 @@ def clash_config(self, meta_or_normal="normal", typ="all.yml"): @login_required(roles={Role.user}) def full_singbox(self): mode = "new" # request.args.get("mode") - c = get_common_data(g.account_uuid, mode) + c = get_common_data(g.account.uuid, mode) # response.content_type = 'text/plain'; if request.method == 'HEAD': resp = "" @@ -337,7 +337,7 @@ def singbox(self): if not hconfig(ConfigEnum.ssh_server_enable): return "SSH server is disable in settings" mode = "new" # request.args.get("mode") - c = get_common_data(g.account_uuid, mode) + c = get_common_data(g.account.uuid, mode) # response.content_type = 'text/plain'; if request.method == 'HEAD': resp = "" @@ -351,7 +351,7 @@ def singbox(self): def all_configs(self, base64=False): mode = "new" # request.args.get("mode") base64 = base64 or request.args.get("base64", "").lower() == "true" - c = get_common_data(g.account_uuid, mode) + c = get_common_data(g.account.uuid, mode) # response.content_type = 'text/plain'; if request.method == 'HEAD': resp = "" From 5a80ee31c2348c716796a2811339f916f0e4f6aa Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 084/112] chg: refactor --- hiddifypanel/panel/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index b1077c181..faa01b0a0 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -36,7 +36,7 @@ def cookie_auth(id: str) -> User | AdminUser | None: if account: g.account = account # g.account_uuid = account.uuid - g.is_admin = False if account.role == Role.user else True + g.is_admin = True if account.role in {Role.super_admin, Role.admin} else False return account @login_manager.request_loader @@ -62,7 +62,7 @@ def header_auth(request) -> User | AdminUser | None: if account: g.account = account # g.account_uuid = account.uuid - g.is_admin = False if account.role == 'user' else True + g.is_admin = True if account.role in {Role.super_admin, Role.admin} else False # False if account.role == 'user' else True if not is_api_call: login_user(account) return account From c2ba50b9bdf7bf6287913c318b3ea1a8771db25e Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 085/112] add: new fields to str_config(proxy paths) --- hiddifypanel/models/config_enum.py | 14 +++++++++++++- hiddifypanel/panel/init_db.py | 10 +++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/hiddifypanel/models/config_enum.py b/hiddifypanel/models/config_enum.py index f96f7a540..d69cf25f7 100644 --- a/hiddifypanel/models/config_enum.py +++ b/hiddifypanel/models/config_enum.py @@ -2,13 +2,15 @@ from strenum import StrEnum + class Lang(StrEnum): en = auto() fa = auto() ru = auto() pt = auto() zh = auto() - + + class ConfigCategory(StrEnum): admin = auto() branding = auto() @@ -73,7 +75,12 @@ class ConfigEnum(StrEnum): kcp_ports = auto() kcp_enable = auto() decoy_domain = auto() + # will be deprecated proxy_path = auto() + proxy_path_super_admin = auto() + proxy_path_admin = auto() + proxy_path_agent = auto() + proxy_path_user = auto() firewall = auto() netdata = auto() http_proxy_enable = auto() @@ -164,6 +171,11 @@ def info(self): self.cloudflare: {'category': ConfigCategory.too_advanced, 'commercial': True}, self.license: {'category': ConfigCategory.hidden, 'commercial': True}, self.proxy_path: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, + self.proxy_path_super_admin: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, + self.proxy_path_admin: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, + self.proxy_path_agent: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, + self.proxy_path_user: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, + self.path_vmess: {'category': ConfigCategory.too_advanced}, self.path_vless: {'category': ConfigCategory.too_advanced}, self.path_trojan: {'category': ConfigCategory.too_advanced}, diff --git a/hiddifypanel/panel/init_db.py b/hiddifypanel/panel/init_db.py index e976e42db..3ed936052 100644 --- a/hiddifypanel/panel/init_db.py +++ b/hiddifypanel/panel/init_db.py @@ -15,7 +15,7 @@ from hiddifypanel.panel.hiddify import get_random_domains, get_random_string import hiddifypanel.models.utils as model_utils -MAX_DB_VERSION = 60 +MAX_DB_VERSION = 70 def init_db(): @@ -145,6 +145,14 @@ def init_db(): # add_config_if_not_exist(ConfigEnum.hysteria_enable, True) # add_config_if_not_exist(ConfigEnum.hysteria_port, random.randint(5000, 20000)) + +def _v60(): + add_config_if_not_exist(ConfigEnum.proxy_path_super_admin, get_random_string()) + add_config_if_not_exist(ConfigEnum.proxy_path_admin, get_random_string()) + add_config_if_not_exist(ConfigEnum.proxy_path_agent, get_random_string()) + add_config_if_not_exist(ConfigEnum.proxy_path_user, get_random_string()) + + def _v59(): # set user model username and password for u in User.query.all(): From 9d143ea9cee1b92400a6601f4910e0e326578396 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 086/112] clean: unused var --- hiddifypanel/panel/init_db.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hiddifypanel/panel/init_db.py b/hiddifypanel/panel/init_db.py index 3ed936052..99f167aaa 100644 --- a/hiddifypanel/panel/init_db.py +++ b/hiddifypanel/panel/init_db.py @@ -345,7 +345,6 @@ def _v17(): def _v1(): - next10year = datetime.date.today() + relativedelta.relativedelta(years=6) external_ip = str(hutils.ip.get_ip(4)) rnd_domains = get_random_domains(5) From 0caf45df13f1d80c2f34bdcb286df43106d56207 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 087/112] fix: add just proxy_path_admin and proxy_path_client --- hiddifypanel/models/config_enum.py | 8 ++------ hiddifypanel/panel/init_db.py | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/hiddifypanel/models/config_enum.py b/hiddifypanel/models/config_enum.py index d69cf25f7..6514e4552 100644 --- a/hiddifypanel/models/config_enum.py +++ b/hiddifypanel/models/config_enum.py @@ -77,10 +77,8 @@ class ConfigEnum(StrEnum): decoy_domain = auto() # will be deprecated proxy_path = auto() - proxy_path_super_admin = auto() proxy_path_admin = auto() - proxy_path_agent = auto() - proxy_path_user = auto() + proxy_path_client = auto() firewall = auto() netdata = auto() http_proxy_enable = auto() @@ -171,10 +169,8 @@ def info(self): self.cloudflare: {'category': ConfigCategory.too_advanced, 'commercial': True}, self.license: {'category': ConfigCategory.hidden, 'commercial': True}, self.proxy_path: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, - self.proxy_path_super_admin: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, self.proxy_path_admin: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, - self.proxy_path_agent: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, - self.proxy_path_user: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, + self.proxy_path_client: {'category': ConfigCategory.too_advanced, 'apply_mode': 'apply', 'show_in_parent': True}, self.path_vmess: {'category': ConfigCategory.too_advanced}, self.path_vless: {'category': ConfigCategory.too_advanced}, diff --git a/hiddifypanel/panel/init_db.py b/hiddifypanel/panel/init_db.py index 99f167aaa..0b1768ff8 100644 --- a/hiddifypanel/panel/init_db.py +++ b/hiddifypanel/panel/init_db.py @@ -147,10 +147,8 @@ def init_db(): # add_config_if_not_exist(ConfigEnum.hysteria_port, random.randint(5000, 20000)) def _v60(): - add_config_if_not_exist(ConfigEnum.proxy_path_super_admin, get_random_string()) add_config_if_not_exist(ConfigEnum.proxy_path_admin, get_random_string()) - add_config_if_not_exist(ConfigEnum.proxy_path_agent, get_random_string()) - add_config_if_not_exist(ConfigEnum.proxy_path_user, get_random_string()) + add_config_if_not_exist(ConfigEnum.proxy_path_client, get_random_string()) def _v59(): From a2d1f2aacdaac0422f7359cd0102f29199b5e603 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 088/112] fix: typo in redirect.html file --- hiddifypanel/panel/user/templates/redirect_to_new_format.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hiddifypanel/panel/user/templates/redirect_to_new_format.html b/hiddifypanel/panel/user/templates/redirect_to_new_format.html index a8452d24a..0e39a6b24 100644 --- a/hiddifypanel/panel/user/templates/redirect_to_new_format.html +++ b/hiddifypanel/panel/user/templates/redirect_to_new_format.html @@ -3,13 +3,12 @@ {% from 'macros.html' import modal %} {% block body_header %} -{% macro reload() -%} +{% macro copy_link() -%} {{_("copy")}} {%- endmacro -%} {{modal("link-alarm-modal",_("Link is changed!"),_("The page has been moved to href=%(new_link)s",new_link=new_link),footer=copy_link(),show=True)}} -%endif {% endblock %} \ No newline at end of file From 9381402d0190dec7d514315b8dec95081f02ba24 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 089/112] chg: api v1 blueprint name --- hiddifypanel/panel/commercial/restapi/v1/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hiddifypanel/panel/commercial/restapi/v1/__init__.py b/hiddifypanel/panel/commercial/restapi/v1/__init__.py index 671c3593e..cc11afff7 100644 --- a/hiddifypanel/panel/commercial/restapi/v1/__init__.py +++ b/hiddifypanel/panel/commercial/restapi/v1/__init__.py @@ -9,7 +9,7 @@ from . import tgbot from .tgmsg import SendMsgResource from .resources import * -bp = APIBlueprint("api", __name__, url_prefix="//api/v1", tag="api_v1", enable_openapi=False) +bp = APIBlueprint("api_v1", __name__, url_prefix="//api/v1", tag="api_v1", enable_openapi=False) api = Api(bp) From ec9462ab10bdea0343483b24ea5d68265f753c10 Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 090/112] new: handle new proxy paths (test) --- hiddifypanel/panel/auth.py | 8 +- hiddifypanel/panel/common.py | 165 ++++++++++++++++------------------ hiddifypanel/panel/hiddify.py | 65 +++++++++++--- 3 files changed, 138 insertions(+), 100 deletions(-) diff --git a/hiddifypanel/panel/auth.py b/hiddifypanel/panel/auth.py index faa01b0a0..66acfedf9 100644 --- a/hiddifypanel/panel/auth.py +++ b/hiddifypanel/panel/auth.py @@ -18,9 +18,9 @@ def cookie_auth(id: str) -> User | AdminUser | None: # first of all check if user sent Authorization header, our priority is with Authorization header (this is valid just for non-api requests) if not hiddify.is_api_call(request.path): # if client requested user panel/admin panel, force it login with Authorization header - if hiddify.is_admin_panel_call(request): + if hiddify.is_admin_panel_call(): return header_auth(request) - if hiddify.is_user_panel_call(request): + if hiddify.is_user_panel_call(): return header_auth(request) # parse id @@ -54,9 +54,9 @@ def header_auth(request) -> User | AdminUser | None: is_api_call = True else: if username_password := hutils.utils.parse_basic_auth_header(auth_header): - if hiddify.is_admin_panel_call(request): + if hiddify.is_admin_panel_call(): account = AdminUser.query.filter(AdminUser.username == username_password[0], AdminUser.password == username_password[1]).first() - elif hiddify.is_user_panel_call(request): + elif hiddify.is_user_panel_call(): account = User.query.filter(User.username == username_password[0], User.password == username_password[1]).first() if account: diff --git a/hiddifypanel/panel/common.py b/hiddifypanel/panel/common.py index 404d570f7..1ddfc6ec6 100644 --- a/hiddifypanel/panel/common.py +++ b/hiddifypanel/panel/common.py @@ -92,6 +92,14 @@ def set_default_path_values(spec): # parameter['schema'] = {'type': 'string', 'default': g.account_uuid} return spec + @app.url_value_preprocessor + def pull_secret_code(endpoint, values): + # just remove proxy_path + # by doing that we don't need to get proxy_path in every view function, we have it in g.proxy_path. it's done in base_middleware function + if values: + values.pop('proxy_path', None) + values.pop('user_secret', None) + @app.url_defaults def add_proxy_path_user(endpoint, values): @@ -113,6 +121,69 @@ def videos(file): # diff=datetime.datetime.now()-value # return format_timedelta(diff, add_direction=True, locale=hconfig(ConfigEnum.lang)) + @app.before_request + def backward_compatibility_middleware(): + # get needed variables + proxy_path = hiddify.get_proxy_path_from_url(request.url) + if not proxy_path: + abort(400, "invalid") + user_agent = user_agents.parse(request.user_agent.string) + # need this variable in redirect_to_new_format view + g.user_agent = user_agent + if user_agent.is_bot: + abort(400, "invalid") + + # get proxy paths + deprecated_proxy_path = hconfig(ConfigEnum.proxy_path) + admin_proxy_path = hconfig(ConfigEnum.proxy_path_admin) + client_proxy_path = hconfig(ConfigEnum.proxy_path_client) + + is_api_call = False + incorrect_request = False + new_link = '' + # handle deprecated proxy path + + if proxy_path == deprecated_proxy_path: + incorrect_request = True + # request.url = request.url.replace('http://', 'https://') + if hiddify.is_admin_panel_call(deprecated_format=True): + new_link = f'https://{request.host}/{admin_proxy_path}/admin/' + elif hiddify.is_user_panel_call(deprecated_format=True): + new_link = f'https://{request.host}/{client_proxy_path}/' + elif hiddify.is_api_call(request.path): + if hiddify.is_admin_api_call(): + new_link = request.url.replace(deprecated_proxy_path, admin_proxy_path) + elif hiddify.is_user_api_call(): + new_link = request.url.replace(deprecated_proxy_path, client_proxy_path) + else: + return abort(400, 'invalid request') + is_api_call = True + else: + return abort(400, 'invalid request') + + # handle uuid url format + if uuid := hutils.utils.get_uuid_from_url_path(request.path): + incorrect_request = True + account = get_user_by_uuid(uuid) or get_admin_by_uuid(uuid) or abort(400, 'invalid request') + if not new_link: + new_link = f'https://{account.username}:{account.password}@{request.host}/{proxy_path}/' + if "/admin/" in request.path: + new_link += "admin/" + else: + if 'https://' in new_link: + new_link = new_link.replace('https://', f'https://{account.username}:{account.password}@') + elif 'http://' in new_link: + new_link = new_link.replace('http://', f'http://{account.username}:{account.password}@') + else: + abort(400, 'DEBUG: WTF, how did this happen? there is no "https://" or "http://" in new_link') + if incorrect_request: + new_link = new_link.replace('http://', 'https://') + # if request made by a browser, show new format page else redirect to new format + # redirect api calls always + if not is_api_call and user_agent.browser: + return render_template('redirect_to_new_format.html', new_link=new_link) + return redirect(new_link, 301) + @app.before_request def base_middleware(): if request.endpoint == 'static' or request.endpoint == "videos": @@ -124,14 +195,15 @@ def base_middleware(): abort(400, "invalid") # validate proxy path + g.proxy_path = hutils.utils.get_proxy_path_from_url(request.url) - if not g.proxy_path: - abort(400, "invalid") - if g.proxy_path != hconfig(ConfigEnum.proxy_path): - if app.config['DEBUG']: - abort(400, Markup( - f"Invalid Proxy Path admin")) - abort(400, "Invalid Proxy Path") + hiddify.proxy_path_validator(g.proxy_path) + + # if g.proxy_path != hconfig(ConfigEnum.proxy_path): + # if app.config['DEBUG']: + # abort(400, Markup( + # f"Invalid Proxy Path admin")) + # abort(400, "Invalid Proxy Path") # setup dark mode if request.args.get('darkmode') != None: @@ -155,85 +227,6 @@ def base_middleware(): else: g.bot = None - # @app.before_request - # def api_auth_middleware(): - # '''In every api request(whether is for the admin or the user) the client should provide api key and we check it''' - # if 'api' not in request.path: # type: ignore - # return - - # # get authenticated account - # account: AdminUser | User | None = auth.standalone_api_auth_verify() - # if not account: - # return abort(401) - # # get account role - # role = auth.get_account_role(account) - # if not role: - # return abort(401) - # # setup authenticated account things (uuid, is_admin, etc.) - # g.account = account - # g.account_uuid = account.uuid - # g.is_admin = False if role == auth.AccountRole.user else True - - @app.before_request - def backward_compatibility_middleware(): - if g.proxy_path != hconfig(ConfigEnum.proxy_path): - # this will make a fingerprint for panel. we should redirect the request to decoy website. - abort(400, "invalid proxy path") - uuid = hutils.utils.get_uuid_from_url_path(request.path) - if uuid: - account = get_user_by_uuid(uuid) or get_admin_by_uuid(uuid) or abort(400, 'invalid request') - # redirect api calls - if hiddify.is_api_call(request.path): - new_link = f'{request.url.replace(f"/{uuid}","").replace("http://","https://")}/' - return redirect(new_link, 301) - - new_link = f'https://{account.username}:{account.password}@{request.host}/{g.proxy_path}/' - if "/admin/" in request.path: - new_link += "admin/" - - # if request made by a browser, show new format page else redirect to new format - if g.user_agent.browser: - return render_template('redirect_to_new_format.html', new_link=new_link) - return redirect(new_link, 301) - - # @app.before_request - # def basic_auth_middleware(): - # '''if the request is for user panel(user page), we try to authenticate the user with basic auth or the client session data, we do that for admin panel too''' - # if 'api' in request.path: # type: ignore - # return - - # account: AdminUser | User | None = None - - # # if we don't have endpoint, we can't detect the request is for admin panel or user panel, so we can't authenticate - # if not request.endpoint: - # abort(400, "invalid request") - - # if request.endpoint and 'UserView' in request.endpoint: - # account = auth.standalone_user_basic_auth_verification() - # else: - # account = auth.standalone_admin_basic_auth_verification() - # # get authenticated account - # if not account: - # return abort(401) - # # get account role - # role = auth.get_account_role(account) - # if not role: - # return abort(401) - # # setup authenticated account things (uuid, is_admin, etc.) - # g.account = account - # g.account_uuid = account.uuid - # g.is_admin = False if role == auth.AccountRole.user else True - - # @app.auth_required(basic_auth, roles=['super_admin', 'admin', 'agent', 'user']) - - @app.url_value_preprocessor - def pull_secret_code(endpoint, values): - # just remove proxy_path - # by doing that we don't need to get proxy_path in every view function, we have it in g.proxy_path. it's done in base_middleware function - if values: - values.pop('proxy_path', None) - values.pop('user_secret', None) - def github_issue_details(): details = { 'hiddify_version': f'{hiddifypanel.__version__}', diff --git a/hiddifypanel/panel/hiddify.py b/hiddifypanel/panel/hiddify.py index 950531b52..1ef4758d1 100644 --- a/hiddifypanel/panel/hiddify.py +++ b/hiddifypanel/panel/hiddify.py @@ -114,35 +114,80 @@ def current_account_api_key(): return g.account.uuid -def current_account_user_pass(): +def current_account_user_pass() -> Tuple[str, str]: return g.account.username, g.account.password -def is_api_call(req_path: str): +def is_api_call(req_path: str) -> bool: return 'api/v1/' in req_path or 'api/v2/' in req_path -def is_user_panel_call(request): - if request.blueprint and request.blueprint == 'user2': +def is_user_api_call() -> bool: + if request.blueprint and request.blueprint == 'api_user': + return True + user_api_call_format = '/api/v2/user/' + if user_api_call_format in request.path: + return True + return False + + +def is_admin_api_call() -> bool: + if request.blueprint and request.blueprint == 'api_admin' or request.blueprint == 'api_v1': + return True + admin_api_call_format = '/api/v2/admin/' + if admin_api_call_format in request.path: return True + return False - user_panel_url = f'{request.host}/{g.proxy_path}/' + +def is_user_panel_call(deprecated_format=False) -> bool: + if request.blueprint and request.blueprint == 'user2': + return True + if deprecated_format: + user_panel_url = f'{request.host}/{hconfig(ConfigEnum.proxy_path)}/' + else: + user_panel_url = f'{request.host}/{hconfig(ConfigEnum.proxy_path_client)}/' if f'{request.host}{request.path}' == user_panel_url: return True return False -def is_admin_panel_call(request): +def is_admin_panel_call(deprecated_format=False) -> bool: if request.blueprint and request.blueprint == 'admin': return True - - admin_panel_url = f'{request.host}/{g.proxy_path}/admin/' - if f'{request.host}{request.path}' == admin_panel_url: + if deprecated_format: + admin_panel_prefix = f'{request.host}/{hconfig(ConfigEnum.proxy_path)}/admin/' + else: + admin_panel_prefix = f'{request.host}/{hconfig(ConfigEnum.proxy_path_admin)}/admin/' + if f'{request.host}{request.path}'.startswith(admin_panel_prefix): return True return False -def asset_url(path): +def proxy_path_validator(proxy_path): + # DEPRECATED PROXY_PATH HANDLED BY BACKWARD COMPATIBILITY MIDDLEWARE + # does not nginx handle proxy path validation? + + if not proxy_path: + return abort(400, 'invalid request') + + dbg_mode = True if current_app.config['DEBUG'] else False + admin_proxy_path = hconfig(ConfigEnum.proxy_path_admin) + client_proxy_path = hconfig(ConfigEnum.proxy_path_client) + + if is_api_call(request.path): + if is_admin_api_call() and proxy_path != admin_proxy_path: + return abort(400, Markup(f"Invalid Proxy Path Admin Panel")) if dbg_mode else abort(400, 'invalid request') + if is_user_api_call() and proxy_path != client_proxy_path: + return abort(400, Markup(f"Invalid Proxy Path User Panel")) if dbg_mode else abort(400, 'invalid request') + + if is_admin_panel_call() and proxy_path != admin_proxy_path: + return abort(400, Markup(f"Invalid Proxy Path Admin Panel")) if dbg_mode else abort(400, 'invalid request') + if is_user_panel_call() and proxy_path != client_proxy_path: + return abort(400, Markup(f"Invalid Proxy Path User Panel")) if dbg_mode else abort(400, 'invalid request') + + +def asset_url(path) -> str: return f"/{g.proxy_path}/{path}" From 54da0870ca2075dab26a2ae1c04d2956646671be Mon Sep 17 00:00:00 2001 From: Sarina Date: Sat, 30 Dec 2023 03:43:08 +0330 Subject: [PATCH 091/112] fix: bug --- .../admin/templates/model/user_list.html | 135 +++++++++--------- 1 file changed, 70 insertions(+), 65 deletions(-) diff --git a/hiddifypanel/panel/admin/templates/model/user_list.html b/hiddifypanel/panel/admin/templates/model/user_list.html index b050c1a5b..b82968c1d 100644 --- a/hiddifypanel/panel/admin/templates/model/user_list.html +++ b/hiddifypanel/panel/admin/templates/model/user_list.html @@ -3,116 +3,121 @@ {% block body %} %if hconfig(ConfigEnum.telegram_bot_token) and g.bot
      -
      - - {{_("Send Message to User's Telegram")}} - - - - -
      - - - -
      +
      + + {{_("Send Message to User's Telegram")}} + + + + +
      + + + +
      - %endif - {{super()}} +%endif +{{super()}} + +
      + {{_('User usage will be updated every 6 minutes. To update it now click here',link=url_for("admin.Actions:update_usage"))}} + +
      + + {% endblock %} {% block tail_js%} {{super()}}