Skip to content

Commit

Permalink
Merge pull request #8 from Dhi13man/feature/global-protect
Browse files Browse the repository at this point in the history
Release v0.0.2 | Global Protect Integration, data model decoupling and Zope Interfaces removal
  • Loading branch information
Dhi13man authored Jun 16, 2024
2 parents 6629c76 + 21cee58 commit ec204d8
Show file tree
Hide file tree
Showing 25 changed files with 726 additions and 316 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Releases

## [0.0.2] - 16th June 2024

1. Integrated one-step [Palo Alto Global Protect](https://docs.paloaltonetworks.com/globalprotect) VPN connection/disconnection.
2. Decoupled VPN Config and VPN Data Models for future flexibility.
3. Removed unnecessary Zope Interfaces dependency as it does not seem worth the maintenance effort.
4. Upgraded `pyinstaller` dependency to leave vulnerable version.

## [0.0.1] - 25th March 2023

Initial implementation of the base features of the auto_vpn_connect script:

1. Connect/Disconnect and set up Auto-Connect to Pritunl VPNs.
2. Save PINs, Tokens and auto fetch TOTPs using pyotp by providing the TOTP URL to minimise the effort to connect to VPNs, after a one-time setup.
3. Set up customisable JSON VPN profiles and configs to customise where various CLIs and dependencies might be located
4. Extensibility to add other VPN clients with ease.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
[![Language](http://ForTheBadge.com/images/badges/made-with-python.svg)](https://www.python.org/)
[!["Buy Me A Coffee"](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20an%20Ego%20boost&emoji=%F0%9F%98%B3&slug=dhi13man&button_colour=FF5F5F&font_colour=ffffff&font_family=Lato&outline_colour=000000&coffee_colour=FFDD00****)](https://www.buymeacoffee.com/dhi13man)

A Python script that allows users to automatically connect to VPNs with minimal effort. As of now, only [Pritunl VPN Client](https://docs.pritunl.com/docs/command-line-interface) is supported.
This is a Python script that allows users to automatically connect to VPNs with minimal effort. VPNs supported as of now:

1. [Pritunl VPN Client](https://docs.pritunl.com/docs/command-line-interface)
2. [Palo Alto Global Protect](https://docs.paloaltonetworks.com/globalprotect)

## Usage

Expand All @@ -24,7 +27,14 @@ A Python script that allows users to automatically connect to VPNs with minimal
{
"config": {
"PRITUNL": {
"vpn_type": "PRITUNL",
"cli_path": "/Applications/Pritunl.app/Contents/Resources/pritunl-client"
},
"GLOBAL_PROTECT": {
"vpn_type": "GLOBAL_PROTECT",
"service_load_command": "launchctl load /Library/LaunchAgents/com.paloaltonetworks.gp.pangpa.plist",
"service_unload_command": "launchctl unload /Library/LaunchAgents/com.paloaltonetworks.gp.pangpa.plist",
"process_kill_command": "pkill -9 -f GlobalProtect"
}
},
"vpn_list": [
Expand All @@ -44,6 +54,10 @@ A Python script that allows users to automatically connect to VPNs with minimal
"vpn_type": "PRITUNL",
"pin": "<vpn_pin_3>",
"token": "<vpn_token>"
},
{
"vpn_id": "GlobalProtect",
"vpn_type": "GLOBAL_PROTECT"
}
]
}
Expand Down Expand Up @@ -161,7 +175,6 @@ cd <path_to_script>

- [Python 3.10+](https://www.python.org/downloads/): Used for developing the script
- [pyotp](https://pypi.org/project/pyotp/): Used for generating OTPs
- [zope.interface](https://pypi.org/project/zope.interface/): Used for type hinting

#### External Dependencies

Expand Down
4 changes: 2 additions & 2 deletions __main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from time import sleep

from src.models.user_switches import UserSwitches
from src.models.vpn_data import abstract_vpn_data
from src.models.vpn_model import abstract_vpn_model
from src.services.vpn_parser_service import VpnDataParserService

PROMPT: str = 'Run in Connect (c), Disconnect (d), or be in Always-Connected mode (w)'
Expand Down Expand Up @@ -57,7 +57,7 @@ def get_user_switches() -> UserSwitches:
raise ValueError(f'Invalid action switch. {PROMPT}!')

# List all VPNs
vpn_data_list: list[abstract_vpn_data.AbstractVpnData] = []
vpn_data_list: list[abstract_vpn_model.AbstractVpnModel] = []
with open(user_switches.vpn_data_json_path, 'r', encoding='utf-8') as f:
vpn_parser_service: VpnDataParserService = VpnDataParserService()
vpn_data_list = vpn_parser_service.parse_vpn_data(f.read())
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file added release/v0.0.2/auto_vpn_connect-0.0.2-macos-apple
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
pyotp==2.8.0
zope.interface==5.5.2
pyinstaller==5.11.0
pyinstaller==6.8.0
18 changes: 10 additions & 8 deletions src/enums/vpn_data/vpn_type.py → src/enums/vpn_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from enum import Enum
from typing import TypeVar
from zope.interface import interfacemethod, Interface

T = TypeVar('T')

Expand All @@ -18,6 +17,7 @@ class VpnType(Enum):
PRITUNL: str = 'PRITUNL'
WIREGUARD: str = 'WIREGUARD'
OPEN_VPN: str = 'OPEN_VPN'
GLOBAL_PROTECT: str = 'GLOBAL_PROTECT'

def visit(self, visitor: 'VpnTypeVisitor[T]') -> T:
'''
Expand All @@ -34,31 +34,33 @@ def visit(self, visitor: 'VpnTypeVisitor[T]') -> T:
return visitor.visit_wireguard()
if self == VpnType.OPEN_VPN:
return visitor.visit_open_vpn()
if self == VpnType.GLOBAL_PROTECT:
return visitor.visit_global_protect()
raise ValueError('VPN Type visit not implemented')

# pylint: disable-next=inherit-non-class
class VpnTypeVisitor(Interface):
class VpnTypeVisitor:
'''
Visitor for VPN types. This is used to visit the VPN type and return the
appropriate data.
'''

@interfacemethod
def visit_none(self) -> T:
'''Visit None VPN data.'''
raise NotImplementedError

def visit_pritunl(self) -> T:
'''Visit Pritunl VPN data.'''
raise NotImplementedError

@interfacemethod
def visit_wireguard(self) -> T:
'''Visit Wireguard VPN data.'''
raise NotImplementedError

@interfacemethod
def visit_open_vpn(self) -> T:
'''Visit OpenVPN VPN data.'''
raise NotImplementedError

@interfacemethod
def visit_none(self) -> T:
'''Visit None VPN data.'''
def visit_global_protect(self) -> T:
'''Visit GlobalProtect VPN data.'''
raise NotImplementedError
56 changes: 56 additions & 0 deletions src/models/vpn_config/abstract_vpn_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'''
Abstract class for VPN data.
'''

from abc import ABC

from src.enums.vpn_type import VpnType, VpnTypeVisitor, T


class AbstractVpnConfig(ABC):
'''
Abstract class for VPN data.
'''

_vpn_type_key: str = 'vpn_type'
_vpn_type: VpnType = VpnType.NONE

def get_vpn_type(self) -> VpnType:
'''
Get the type of the VPN.
Returns:
VpnType: Type of the VPN
'''
return AbstractVpnConfig._vpn_type

def visit(self, visitor: 'VpnTypeVisitor[T]') -> T:
'''
Visit the VPN with a VpnTypeVisitor.
Args:
visitor (VpnTypeVisitor): Visitor to visit the Pritunl VPN with
'''
return visitor.visit_none()

def to_json(self) -> dict:
'''
Convert the VPN data to a JSON string.
Returns:
str: JSON string of the VPN data
'''
return {AbstractVpnConfig._vpn_type_key: self.get_vpn_type().value}

@staticmethod
def from_json(json: dict) -> 'AbstractVpnConfig':
'''
Create a VPN data object from a JSON string.
Args:
json (dict): JSON string of the VPN data
'''
vpn_type: VpnType = VpnType(json.get(AbstractVpnConfig._vpn_type_key, VpnType.NONE))
if vpn_type != AbstractVpnConfig._vpn_type:
raise ValueError(f'Invalid VPN type {vpn_type}')
return AbstractVpnConfig()
87 changes: 87 additions & 0 deletions src/models/vpn_config/global_protect_vpn_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'''
Abstract class for VPN data.
'''

from src.enums.vpn_type import VpnType, VpnTypeVisitor, T
from src.models.vpn_config.abstract_vpn_config import AbstractVpnConfig

class GlobalProtectVpnConfig(AbstractVpnConfig):
'''
Abstract class for VPN data.
Attributes:
vpn_id (str): ID of the VPN
'''

_vpn_type: VpnType = VpnType.GLOBAL_PROTECT
_service_load_command_key: str = "service_load_command"
_service_unload_command_key: str = "service_unload_command"
_process_kill_command_key: str = "process_kill_command"
_default_service_load_command: str = (
"launchctl load /Library/LaunchAgents/com.paloaltonetworks.gp.pangpa.plist"
)
_default_service_unload_command: str = (
"launchctl unload /Library/LaunchAgents/com.paloaltonetworks.gp.pangpa.plist"
)
_default_process_kill_command: str = "pkill -9 -f GlobalProtect"

def __init__(
self,
service_load_command: str = _default_service_load_command,
service_unload_command: str = _default_service_unload_command,
process_kill_command: str = _default_process_kill_command
):
self.service_load_command: str = service_load_command
self.service_unload_command: str = service_unload_command
self.process_kill_command: str = process_kill_command

def get_vpn_type(self) -> VpnType:
'''
Get the type of the VPN.
Returns:
VpnType: Type of the VPN
'''
return GlobalProtectVpnConfig._vpn_type

def visit(self, visitor: 'VpnTypeVisitor[T]') -> T:
'''
Visit the VPN with a VpnTypeVisitor.
Args:
visitor (VpnTypeVisitor): Visitor to visit the Pritunl VPN with
'''
return visitor.visit_global_protect()

def to_json(self) -> dict:
'''
Convert the VPN data to a JSON string.
Returns:
str: JSON string of the VPN data
'''
return {
GlobalProtectVpnConfig._vpn_type_key: self.get_vpn_type().value,
GlobalProtectVpnConfig._service_load_command_key: self.service_load_command,
GlobalProtectVpnConfig._service_unload_command_key: self.service_unload_command,
GlobalProtectVpnConfig._process_kill_command_key: self.process_kill_command
}

@staticmethod
def from_json(json: dict) -> 'GlobalProtectVpnConfig':
'''
Create a VPN data object from a JSON string.
Args:
json (dict): JSON string of the VPN data
'''
vpn_type: VpnType = VpnType(
json.get(GlobalProtectVpnConfig._vpn_type_key, VpnType.GLOBAL_PROTECT)
)
if vpn_type != GlobalProtectVpnConfig._vpn_type:
raise ValueError(f'Invalid VPN type {vpn_type}')
return GlobalProtectVpnConfig(
service_load_command=json.get(GlobalProtectVpnConfig._service_load_command_key),
service_unload_command=json.get(GlobalProtectVpnConfig._service_unload_command_key),
process_kill_command=json.get(GlobalProtectVpnConfig._process_kill_command_key)
)
64 changes: 64 additions & 0 deletions src/models/vpn_config/pritunl_vpn_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'''
Abstract class for VPN data.
'''

from src.enums.vpn_type import VpnType, VpnTypeVisitor, T
from src.models.vpn_config.abstract_vpn_config import AbstractVpnConfig

class PritunlVpnConfig(AbstractVpnConfig):
'''
Abstract class for VPN data.
Attributes:
vpn_id (str): ID of the VPN
'''

_vpn_type: VpnType = VpnType.PRITUNL
_cli_path_key: str = "cli_path"
_default_cli_path: str = "/Applications/Pritunl.app/Contents/Resources/pritunl-client"

def __init__(self, cli_path: str=_default_cli_path) -> None:
self.cli_path = cli_path

def get_vpn_type(self) -> VpnType:
'''
Get the type of the VPN.
Returns:
VpnType: Type of the VPN
'''
return PritunlVpnConfig._vpn_type

def visit(self, visitor: 'VpnTypeVisitor[T]') -> T:
'''
Visit the VPN with a VpnTypeVisitor.
Args:
visitor (VpnTypeVisitor): Visitor to visit the Pritunl VPN with
'''
return visitor.visit_pritunl()

def to_json(self) -> dict:
'''
Convert the VPN data to a JSON string.
Returns:
str: JSON string of the VPN data
'''
return {
PritunlVpnConfig._vpn_type_key: self.get_vpn_type().value,
PritunlVpnConfig._cli_path_key: self.cli_path
}

@staticmethod
def from_json(json: dict) -> 'PritunlVpnConfig':
'''
Create a VPN data object from a JSON string.
Args:
json (dict): JSON string of the VPN data
'''
vpn_type: VpnType = VpnType(json.get(PritunlVpnConfig._vpn_type_key, VpnType.PRITUNL))
if vpn_type != PritunlVpnConfig._vpn_type:
raise ValueError(f'Invalid VPN type {vpn_type}')
return PritunlVpnConfig(cli_path=json.get(PritunlVpnConfig._cli_path_key))
Loading

0 comments on commit ec204d8

Please sign in to comment.