353 lines
No EOL
16 KiB
Python
353 lines
No EOL
16 KiB
Python
from core.Errors import MissingSubscriptionError, InvalidSubscriptionError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError, PaymentRequiredError, UnknownConnectionTypeError
|
|
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
|
from core.controllers.InvoiceController import InvoiceController
|
|
from core.controllers.LocationController import LocationController
|
|
from core.controllers.ProfileController import ProfileController
|
|
from core.controllers.SubscriptionController import SubscriptionController
|
|
from core.controllers.SubscriptionPlanController import SubscriptionPlanController
|
|
from core.models.session.SessionConnection import SessionConnection
|
|
from core.models.session.SessionProfile import SessionProfile
|
|
from core.models.system.SystemConnection import SystemConnection
|
|
from core.models.system.SystemProfile import SystemProfile
|
|
from cli.helpers import sanitize_profile, parse_application_argument, parse_location_argument
|
|
from cli.observers import profile_observer, application_version_observer, connection_observer, invoice_observer
|
|
from core.Errors import SingboxNotInstalledException
|
|
from cli.killswitch_monitor import KillswitchMonitor
|
|
import pprint
|
|
import signal
|
|
import sys
|
|
|
|
NAME = 'profile'
|
|
|
|
|
|
def register(subparsers):
|
|
parser = subparsers.add_parser(NAME)
|
|
subs = parser.add_subparsers(title='subcommands', dest='subcommand')
|
|
|
|
base = _base_parser()
|
|
pristine = _pristine_parser()
|
|
connection_protection = _connection_protection_parser()
|
|
endpoint_verification = _endpoint_verification_parser()
|
|
profile_state_protection = _profile_state_protection_parser()
|
|
|
|
subs.add_parser('list')
|
|
subs.add_parser('show', parents=[base])
|
|
subs.add_parser('destroy', parents=[base])
|
|
subs.add_parser('enable', parents=[base, pristine, connection_protection, endpoint_verification, profile_state_protection])
|
|
subs.add_parser('disable', parents=[base, connection_protection])
|
|
|
|
create_parser = subs.add_parser('create')
|
|
create_subs = create_parser.add_subparsers(title='profile_types', dest='profile_type')
|
|
|
|
session_parser = create_subs.add_parser('session', parents=[base])
|
|
session_parser.add_argument('--name', '-n', default='')
|
|
session_parser.add_argument('--location', '-l', default='')
|
|
session_parser.add_argument('--application', '-a', required=True)
|
|
session_parser.add_argument('--connection', '-c', dest='connection_type', choices=['system', 'tor', 'wireguard'], default='system')
|
|
session_parser.add_argument('--mask-connection', '-m', action='store_true')
|
|
session_parser.add_argument('--resolution', '-r', default='1280x720')
|
|
|
|
system_parser = create_subs.add_parser('system', parents=[base])
|
|
system_parser.add_argument('--name', '-n', default='')
|
|
system_parser.add_argument('--location', '-l', default='')
|
|
system_parser.add_argument('--connection', '-c', dest='connection_type', choices=['wireguard'], default='wireguard')
|
|
|
|
operator_parser = create_subs.add_parser('operator', parents=[base])
|
|
operator_parser.add_argument('--name', '-n', default='')
|
|
operator_parser.add_argument('--operator-id', '-o', type=int, required=True)
|
|
operator_parser.add_argument('--protocol', '-pr', choices=['vless', 'hysteria2'], required=True)
|
|
|
|
return parser
|
|
|
|
|
|
def handle(arguments, main_parser):
|
|
if arguments.subcommand is None:
|
|
main_parser.parse_args(['profile', '--help'])
|
|
return
|
|
|
|
ignore = _build_ignore(arguments)
|
|
|
|
if arguments.subcommand == 'list':
|
|
profiles = ProfileController.get_all()
|
|
for key, value in profiles.items():
|
|
profiles[key] = sanitize_profile(value)
|
|
pprint.pp(profiles)
|
|
|
|
elif arguments.subcommand == 'show':
|
|
pprint.pp(ProfileController.get(arguments.id))
|
|
|
|
elif arguments.subcommand == 'create':
|
|
|
|
if arguments.profile_type == 'operator':
|
|
from core.models.manage.session_management import get_session
|
|
from core.models.orm_models.Location import Location
|
|
from sqlalchemy import select
|
|
session = get_session()
|
|
capable_field = Location.is_vless_capable if arguments.protocol == 'vless' else Location.is_hysteria2_capable
|
|
loc = session.execute(
|
|
select(Location).where(
|
|
(Location.operator_id == arguments.operator_id) &
|
|
(capable_field == True)
|
|
)
|
|
).scalar_one_or_none()
|
|
if loc is None:
|
|
main_parser.error(f'No location found for operator {arguments.operator_id} with protocol {arguments.protocol}')
|
|
connection = SystemConnection(arguments.protocol, arguments.operator_id)
|
|
profile = SystemProfile(arguments.id, arguments.name, None, loc, connection)
|
|
ProfileController.create(profile, profile_observer=profile_observer)
|
|
return
|
|
|
|
location_details = parse_location_argument(arguments.location)
|
|
location = LocationController.get(location_details.get('country_code'), location_details.get('code'))
|
|
|
|
if location is None:
|
|
main_parser.error('the following argument should be a valid reference: --location/-l')
|
|
|
|
if arguments.profile_type == 'session':
|
|
app_details = parse_application_argument(arguments.application)
|
|
app_version = ApplicationVersionController.get(app_details.get('application_code'), app_details.get('version_number'))
|
|
if app_version is None:
|
|
main_parser.error('the following argument should be a valid reference: --application/-a')
|
|
connection = SessionConnection(arguments.connection_type, arguments.mask_connection)
|
|
profile = SessionProfile(arguments.id, arguments.name, None, location, arguments.resolution, app_version, connection)
|
|
else:
|
|
connection = SystemConnection(arguments.connection_type)
|
|
profile = SystemProfile(arguments.id, arguments.name, None, location, connection)
|
|
|
|
ProfileController.create(profile, profile_observer=profile_observer)
|
|
|
|
elif arguments.subcommand == 'destroy':
|
|
profile = ProfileController.get(arguments.id)
|
|
if profile is not None:
|
|
ProfileController.destroy(profile, profile_observer=profile_observer)
|
|
else:
|
|
main_parser.error('the following argument should be a valid reference: --id/-i')
|
|
|
|
elif arguments.subcommand == 'enable':
|
|
profile = ProfileController.get(arguments.id)
|
|
if profile is None:
|
|
main_parser.error('the following argument should be a valid reference: --id/-i')
|
|
|
|
try:
|
|
from cli import observers as obs
|
|
|
|
monitor = KillswitchMonitor()
|
|
timer_state = {'stop': None, 'reconnecting': None, '_retrying': False}
|
|
obs.get_ui_state()['timer_state'] = timer_state
|
|
obs.get_ui_state()['monitor'] = monitor
|
|
|
|
monitor.set_on_disconnect(lambda: _do_disable(profile, ignore))
|
|
monitor.set_on_leak(lambda: _on_leak(profile, ignore, monitor, obs))
|
|
monitor.set_on_displaced(lambda: _on_displaced(monitor))
|
|
|
|
signal.signal(signal.SIGUSR1, lambda s, f: _on_displaced(monitor))
|
|
signal.signal(signal.SIGHUP, lambda s, f: (_do_disable(profile, ignore), sys.exit(0)))
|
|
|
|
try:
|
|
ProfileController.enable(
|
|
profile, ignore=ignore, pristine=arguments.pristine,
|
|
asynchronous=True, profile_observer=profile_observer,
|
|
application_version_observer=application_version_observer,
|
|
connection_observer=connection_observer
|
|
)
|
|
monitor.run_blocking()
|
|
_stop_timer(obs.get_ui_state()['timer_state'])
|
|
except KeyboardInterrupt:
|
|
print()
|
|
_do_disable(profile, ignore)
|
|
|
|
except SingboxNotInstalledException:
|
|
main_parser.error('sing-box is not installed. Please run the installation script first.')
|
|
except UnknownConnectionTypeError:
|
|
print('No connection method configured. Please run:\n')
|
|
print(' python -m cli set connection system')
|
|
print(' python -m cli sync\n')
|
|
except PaymentRequiredError:
|
|
print('Payment processing is currently unavailable. The subscription could not be activated.\nPlease try again later or contact support.')
|
|
except (InvalidSubscriptionError, MissingSubscriptionError) as exception:
|
|
_handle_subscription_error(exception, profile, ignore, arguments, main_parser)
|
|
|
|
elif arguments.subcommand == 'disable':
|
|
profile = ProfileController.get(arguments.id)
|
|
if profile is not None:
|
|
ProfileController.disable(profile, ignore=ignore, profile_observer=profile_observer, connection_observer=connection_observer)
|
|
else:
|
|
main_parser.error('the following argument should be a valid reference: --id/-i')
|
|
|
|
|
|
# ── Enable helpers ────────────────────────────────────────────────────────────
|
|
|
|
def _stop_timer(timer_state: dict) -> None:
|
|
if timer_state['stop'] is not None:
|
|
timer_state['stop'].set()
|
|
timer_state['stop'] = None
|
|
|
|
|
|
def _do_disable(profile, ignore) -> None:
|
|
from cli import observers as obs
|
|
_stop_timer(obs.get_ui_state()['timer_state'])
|
|
ProfileController.disable(
|
|
profile,
|
|
ignore=ignore,
|
|
profile_observer=profile_observer,
|
|
connection_observer=connection_observer
|
|
)
|
|
|
|
|
|
def _on_leak(profile, ignore, monitor, obs) -> None:
|
|
_stop_timer(obs.get_ui_state()['timer_state'])
|
|
print('\n')
|
|
print(' Leak detected. Network blocked.')
|
|
print()
|
|
choice = None
|
|
while choice not in ('1', '2'):
|
|
print(' [1] Disable profile and restore internet')
|
|
print(' [2] Retry connection')
|
|
try:
|
|
choice = input('\n Choose an option: ').strip()
|
|
except KeyboardInterrupt:
|
|
print()
|
|
_do_disable(profile, ignore)
|
|
return
|
|
|
|
if choice == '1':
|
|
_do_disable(profile, ignore)
|
|
elif choice == '2':
|
|
try:
|
|
obs.get_ui_state()['timer_state']['_retrying'] = True
|
|
ProfileController.enable(
|
|
profile, ignore=ignore, pristine=False,
|
|
asynchronous=True, profile_observer=profile_observer,
|
|
application_version_observer=application_version_observer,
|
|
connection_observer=connection_observer
|
|
)
|
|
monitor.run_blocking()
|
|
except KeyboardInterrupt:
|
|
print()
|
|
_do_disable(profile, ignore)
|
|
|
|
|
|
def _on_displaced(monitor) -> None:
|
|
from cli import observers as obs
|
|
_stop_timer(obs.get_ui_state()['timer_state'])
|
|
monitor.stop()
|
|
print('\n')
|
|
print(' Connection terminated: taken over by another session.')
|
|
print()
|
|
sys.exit(0)
|
|
|
|
|
|
# ── Subscription error handler ────────────────────────────────────────────────
|
|
|
|
def _handle_subscription_error(exception, profile, ignore, arguments, main_parser):
|
|
if type(exception).__name__ == 'InvalidSubscriptionError':
|
|
print('The profile\'s subscription appears to be invalid.\n')
|
|
elif type(exception).__name__ == 'MissingSubscriptionError':
|
|
print('The profile is not tied to a subscription.\n')
|
|
|
|
manage_subscription_input = None
|
|
|
|
while manage_subscription_input not in ('1', '2', '3', ''):
|
|
print('Please select from the following:\n')
|
|
print(' 1) Request new subscription')
|
|
print(' 2) Enter billing code')
|
|
print('\n 3) Exit')
|
|
|
|
manage_subscription_input = input('\nEnter your choice [1]: ')
|
|
|
|
if manage_subscription_input in ('1', ''):
|
|
print('\nCreating subscription...\n')
|
|
|
|
subscription_plan = SubscriptionPlanController.get(profile.connection, 720)
|
|
if subscription_plan is None:
|
|
if not SubscriptionPlanController.get_all():
|
|
print('\nData not synchronized. Please run the following commands and try again:\n')
|
|
print(' python -m cli set connection system')
|
|
print(' python -m cli sync\n')
|
|
else:
|
|
print('\nNo compatible subscription plan was found. Please contact support.\n')
|
|
return
|
|
|
|
potential_subscription = SubscriptionController.create(subscription_plan, profile, connection_observer=connection_observer)
|
|
if potential_subscription is None:
|
|
print('\nThe subscription could not be created. Please try again later.\n')
|
|
return
|
|
|
|
ProfileController.attach_subscription(profile, potential_subscription)
|
|
|
|
if potential_subscription.operator_id is None:
|
|
subscription = InvoiceController.handle_payment(potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer)
|
|
if subscription is None:
|
|
print('\nPayment processing is currently unavailable. Please try again later or contact support.\n')
|
|
return
|
|
ProfileController.attach_subscription(profile, subscription)
|
|
|
|
try:
|
|
ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
|
except PaymentRequiredError:
|
|
print('\nPayment processing is currently unavailable. The subscription could not be activated.\nPlease try again later or contact support.\n')
|
|
return
|
|
|
|
elif manage_subscription_input == '2':
|
|
billing_code = input('\nEnter your billing code: ')
|
|
print()
|
|
subscription = SubscriptionController.get(billing_code, connection_observer=connection_observer)
|
|
if subscription is not None:
|
|
ProfileController.attach_subscription(profile, subscription)
|
|
try:
|
|
ProfileController.enable(profile, ignore=ignore, pristine=arguments.pristine, asynchronous=True, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer)
|
|
except PaymentRequiredError:
|
|
print('\nPayment processing is currently unavailable. The subscription could not be activated.\nPlease try again later or contact support.\n')
|
|
return
|
|
else:
|
|
print('\nThe billing code appears to be invalid.\n')
|
|
manage_subscription_input = None
|
|
|
|
elif manage_subscription_input == '3':
|
|
pass
|
|
else:
|
|
print('\nInput appears to be invalid. Please try again.\n')
|
|
|
|
|
|
# ── Argument parsers ──────────────────────────────────────────────────────────
|
|
|
|
def _build_ignore(arguments):
|
|
ignore = []
|
|
if getattr(arguments, 'without_connection_protection', False):
|
|
ignore.append(ConnectionUnprotectedError)
|
|
if getattr(arguments, 'without_endpoint_verification', False):
|
|
ignore.append(EndpointVerificationError)
|
|
if getattr(arguments, 'without_profile_state_protection', False):
|
|
ignore.append(ProfileStateConflictError)
|
|
return tuple(ignore)
|
|
|
|
|
|
def _base_parser():
|
|
import argparse
|
|
p = argparse.ArgumentParser(add_help=False)
|
|
p.add_argument('--id', '-i', type=int, required=True)
|
|
return p
|
|
|
|
def _pristine_parser():
|
|
import argparse
|
|
p = argparse.ArgumentParser(add_help=False)
|
|
p.add_argument('--pristine', '-p', action='store_true')
|
|
return p
|
|
|
|
def _connection_protection_parser():
|
|
import argparse
|
|
p = argparse.ArgumentParser(add_help=False)
|
|
p.add_argument('--without-connection-protection', action='store_true')
|
|
return p
|
|
|
|
def _endpoint_verification_parser():
|
|
import argparse
|
|
p = argparse.ArgumentParser(add_help=False)
|
|
p.add_argument('--without-endpoint-verification', action='store_true')
|
|
return p
|
|
|
|
def _profile_state_protection_parser():
|
|
import argparse
|
|
p = argparse.ArgumentParser(add_help=False)
|
|
p.add_argument('--without-profile-state-protection', action='store_true')
|
|
return p |