Skip to content

Commit 92c3901

Browse files
Merge pull request #6 from TabulateJarl8/qol-fixes
QOL Fixes
2 parents 1c411e8 + 979b662 commit 92c3901

File tree

8 files changed

+273
-226
lines changed

8 files changed

+273
-226
lines changed

README.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ This is a wrapper script around openconnect to help with authentication for the
1717
This script can easily be installed with pip or [pipx](https://pipx.pypa.io/stable/) with the following commands:
1818

1919
```console
20-
$ pip3 install jmu-openconnect
21-
$ # OR
2220
$ pipx install jmu-openconnect
21+
$ # OR
22+
$ pip3 install jmu-openconnect
2323
```
2424

25-
This script can also be used as a standalone script by downloading the `main.py` file and ensuring that selenium is installed with `pip3 install selenium`, or by cloning the repository and running `poetry install`.
26-
2725
## Usage
2826
Once the script is installed, you can run the following command in your terminal:
2927

@@ -37,7 +35,7 @@ You can also specify a username and password to be automatically typed in, howev
3735
$ jmu-openconnect -u <EID> -p <PASSWORD>
3836
```
3937

40-
You can alternatively specify the `--prompt-password` option instead of using `-p`, which will prompt the user for a password without echoing, much like sudo. This is more secure as your password won't be saved in your command line history.
38+
You can alternatively specify the `--prompt-password` (or `-P`) option instead of using `-p`, which will prompt the user for a password without echoing, much like sudo. This is more secure as your password won't be saved in your command line history.
4139

4240
JMU OpenConnect defaults to using firefox, but you can easily change which browser you're using by specifying `--browser`, which accepts `firefox`, `chrome`, or `edge`.
4341

@@ -52,12 +50,9 @@ To see all of the available options, run `jmu-openconnect --help`.
5250
## Dependencies
5351
This script just requires openconnect and [selenium](https://pypi.org/project/selenium/). If you are having problems, check the [Selenium Python Documentation](https://selenium-python.readthedocs.io/installation.html#drivers0).
5452

55-
## Why is this all in one script?
56-
I heavily considered splitting this up into multiple files, but I really wanted to preserve the ability to just have this script up on a website somewhere where people could just download this script, install selenium, and run it with Python. This may change in the future but this is what I've gone with for now.
57-
5853
## DSID Cookie was not found
5954
If you get the error that the DSID cookie was not found, then you may be logged on in multiple places at once. Navigate to https://vpn.jmu.edu and after signing in, you should see a screen like this:
6055

6156
![Maximum number of open user sessions screenshot](img/multi_sign_in.png)
6257

63-
If this is the case, just select the box to remove that sign in and retry the script after verifying that you are signed out of all browser sessions. If this is not the problem, try running the script with `jmu-openconnect --debug-auth-error` to see the error for a longer period of time.
58+
If this is the case, just select the box to remove that sign in and press "Close Selected Sessions and Log in". After this, you will need to press the log out button in the upper right corner of the VPN website, and then you can retry the script. If this is not the problem, try running the script with `jmu-openconnect --debug-auth-error` to see the error for a longer period of time.

jmu_openconnect/__init__.py

Whitespace-only changes.

jmu_openconnect/argument_parser.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Module for parsing command line arguments."""
2+
3+
import argparse
4+
5+
6+
def parse_args() -> argparse.Namespace:
7+
"""Parse command line arguments.
8+
9+
Returns:
10+
argparse.Namespace: an argparse Namespace containing the parsed arguments
11+
"""
12+
parser = argparse.ArgumentParser(
13+
description='OpenConnect helper for logging into the JMU Ivanti VPN through Duo',
14+
)
15+
16+
parser.add_argument(
17+
'--browser',
18+
'-b',
19+
choices=['firefox', 'chrome', 'edge'],
20+
default='firefox',
21+
nargs='?',
22+
help='Which web browser to use in Duo authentication. Default firefox',
23+
)
24+
25+
parser.add_argument(
26+
'--browser-binary',
27+
'-B',
28+
help="Specify a path to your browser's binary. Useful if you use a derivative of a supported browser, like WaterFox.",
29+
)
30+
31+
parser.add_argument(
32+
'--username',
33+
'-u',
34+
default='',
35+
help='Automatically type in a username',
36+
)
37+
38+
parser.add_argument(
39+
'--password',
40+
'-p',
41+
default='',
42+
help='Automatically type in a password',
43+
)
44+
45+
parser.add_argument(
46+
'-P',
47+
'--prompt-password',
48+
action='store_true',
49+
help='Prompt for the password without echoing as to not show it in your command history',
50+
)
51+
52+
parser.add_argument(
53+
'--timeout',
54+
type=int,
55+
default=300,
56+
help='Number of seconds it takes before the webdriver times out waiting for authentication. Default 300 seconds',
57+
)
58+
59+
parser.add_argument(
60+
'--debug-auth-error',
61+
action='store_true',
62+
help='Pause for 10 seconds after authentication. Useful for debugging errors',
63+
)
64+
65+
parser.add_argument(
66+
'-A',
67+
'--only-authenticate',
68+
action='store_true',
69+
help="Only authenticate and don't start openconnect. Prints the DSID cookie to STDOUT",
70+
)
71+
72+
return parser.parse_args()
Lines changed: 7 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
#!/usr/bin/env python3
1+
"""Module for user authenticate with selenium.
2+
3+
This module contains utilities related to authenticating the user with selenium,
4+
such as the Browser enum as well as the function for fetching the DSID cookie.
5+
"""
26

3-
import argparse
4-
import getpass
57
import json
68
import logging
7-
import os
8-
import shutil
9-
import subprocess
10-
import sys
119
import time
1210
from enum import Enum
1311
from typing import Optional
@@ -23,6 +21,8 @@
2321
from selenium.webdriver.support import expected_conditions as EC # noqa: N812
2422
from selenium.webdriver.support.wait import WebDriverWait
2523

24+
from jmu_openconnect.exceptions import MissingDSIDError, TimedOutError
25+
2626

2727
class Browser(Enum):
2828
"""Specify a browser to use"""
@@ -32,42 +32,6 @@ class Browser(Enum):
3232
EDGE = {'driver': webdriver.Edge, 'options': webdriver.EdgeOptions}
3333

3434

35-
class MissingDSIDError(Exception):
36-
"""Raised if DSID cooke was not found."""
37-
38-
def __init__(
39-
self,
40-
msg='DSID Cookie was not found',
41-
*args,
42-
**kwargs,
43-
):
44-
super().__init__(msg, *args, **kwargs)
45-
46-
47-
class TimedOutError(Exception):
48-
"""Raised if the webdriver timed out while waiting for authentication."""
49-
50-
def __init__(
51-
self,
52-
msg='webdriver timed out while waiting for authentication',
53-
*args,
54-
**kwargs,
55-
):
56-
super().__init__(msg, *args, **kwargs)
57-
58-
59-
class MissingOpenConnectError(Exception):
60-
"""If openconnect could not be found on the PATH."""
61-
62-
def __init__(
63-
self,
64-
msg="openconnect binary could not be found. make sure it's installed and on your PATH",
65-
*args,
66-
**kwargs,
67-
):
68-
super().__init__(msg, *args, **kwargs)
69-
70-
7135
def fetch_latest_browser_version(browser: Browser) -> str:
7236
"""Fetches the latest firefox/chromium version from mozilla's/google's API.
7337
@@ -257,166 +221,3 @@ def get_dsid_cookie(
257221
driver.quit()
258222

259223
return dsid_cookie['value']
260-
261-
262-
def start_openconnect(dsid_cookie: str) -> int:
263-
"""Start openconnect with the specified DSID cookie.
264-
265-
Args:
266-
dsid_cookie (str): The DSID cookie to authenticate with
267-
268-
Raises:
269-
MissingOpenConnectError: If openconnect couldn't be found on the PATH
270-
PermissionError: If the user isn't root and sudo/doas couldn't be found
271-
272-
Returns:
273-
int: the openconnect return code
274-
"""
275-
logging.info('Starting openconnect')
276-
277-
logging.debug('Checking if openconnect is installed')
278-
if shutil.which('openconnect') is None:
279-
raise MissingOpenConnectError
280-
281-
as_root = next(([prog] for prog in ('doas', 'sudo') if shutil.which(prog)), [])
282-
logging.debug(f'Root program identified: {as_root}')
283-
284-
# check if the script is running as root or if sudo/doas were not found
285-
# os.geteuid() will be 0 if root
286-
if os.geteuid() and not as_root:
287-
raise PermissionError('sudo/doas were not found')
288-
289-
oc_command = as_root + [
290-
'openconnect',
291-
'--protocol',
292-
'pulse',
293-
'--cookie',
294-
dsid_cookie,
295-
'https://vpn.jmu.edu',
296-
]
297-
298-
logging.debug(f'OC Command: {oc_command}')
299-
300-
# we use subprocess.call to prompt the user for root if needed, and return the return code
301-
return subprocess.call(oc_command)
302-
303-
304-
def main():
305-
logging.basicConfig(
306-
format='%(asctime)s | %(levelname)s | %(module)s:%(module)s:%(lineno)d - %(message)s',
307-
level=os.environ.get('LOGLEVEL', 'INFO').upper(),
308-
)
309-
310-
parser = argparse.ArgumentParser(
311-
description='OpenConnect helper for logging into the JMU Ivanti VPN through Duo'
312-
)
313-
314-
parser.add_argument(
315-
'--browser',
316-
'-b',
317-
choices=['firefox', 'chrome', 'edge'],
318-
default='firefox',
319-
nargs='?',
320-
help='Which web browser to use in Duo authentication. Default firefox',
321-
)
322-
323-
parser.add_argument(
324-
'--browser-binary',
325-
'-B',
326-
help="Specify a path to your browser's binary. Useful if you use a derivative of a supported browser, like WaterFox.",
327-
)
328-
329-
parser.add_argument(
330-
'--username',
331-
'-u',
332-
default='',
333-
help='Automatically type in a username',
334-
)
335-
336-
parser.add_argument(
337-
'--password',
338-
'-p',
339-
default='',
340-
help='Automatically type in a password',
341-
)
342-
343-
parser.add_argument(
344-
'--prompt-password',
345-
action='store_true',
346-
help='Prompt for the password without echoing as to not show it in your command history',
347-
)
348-
349-
parser.add_argument(
350-
'--timeout',
351-
type=int,
352-
default=300,
353-
help='Number of seconds it takes before the webdriver times out waiting for authentication. Default 300 seconds',
354-
)
355-
356-
parser.add_argument(
357-
'--debug-auth-error',
358-
action='store_true',
359-
help='Pause for 10 seconds after authentication. Useful for debugging errors',
360-
)
361-
362-
parser.add_argument(
363-
'--only-authenticate',
364-
action='store_true',
365-
help="Only authenticate and don't start openconnect. Prints the DSID cookie to STDOUT",
366-
)
367-
368-
args = parser.parse_args()
369-
370-
# check if we need to prompt the user for a password
371-
password = args.password
372-
if args.prompt_password:
373-
password = getpass.getpass('Password: ')
374-
375-
# assign the browser to use. argparse guarentees that it will be one of these three
376-
browser = {
377-
'firefox': Browser.FIREFOX,
378-
'chrome': Browser.CHROME,
379-
'edge': Browser.EDGE,
380-
}[args.browser]
381-
382-
try:
383-
dsid_cookie = get_dsid_cookie(
384-
username=args.username,
385-
password=password,
386-
browser=browser,
387-
browser_binary=args.browser_binary,
388-
webdriver_timeout=args.timeout,
389-
debug_auth_error=args.debug_auth_error,
390-
)
391-
except TimedOutError:
392-
print('webdriver timed out while waiting for authentication')
393-
sys.exit(1)
394-
except MissingDSIDError:
395-
print(
396-
'DSID Cookie was not found. Ensure that you are not logged in multiple times. See README for details.'
397-
)
398-
sys.exit(1)
399-
400-
if args.only_authenticate:
401-
print(dsid_cookie)
402-
sys.exit(0)
403-
404-
exit_code = 0
405-
try:
406-
exit_code = start_openconnect(dsid_cookie=dsid_cookie)
407-
except MissingOpenConnectError:
408-
print(
409-
'openconnect was not found on your PATH. Please ensure that openconnect is installed.'
410-
)
411-
exit_code = 1
412-
except PermissionError:
413-
print(
414-
'sudo was not found on your PATH. Please install sudo or run this script as root.'
415-
)
416-
exit_code = 1
417-
418-
sys.exit(exit_code)
419-
420-
421-
if __name__ == '__main__':
422-
main()

jmu_openconnect/exceptions.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""This module contains custom exceptions that are used throughout the program."""
2+
3+
4+
class MissingDSIDError(Exception):
5+
"""Raised if DSID cooke was not found."""
6+
7+
def __init__(
8+
self,
9+
msg='DSID Cookie was not found',
10+
*args,
11+
**kwargs,
12+
):
13+
super().__init__(msg, *args, **kwargs)
14+
15+
16+
class TimedOutError(Exception):
17+
"""Raised if the webdriver timed out while waiting for authentication."""
18+
19+
def __init__(
20+
self,
21+
msg='webdriver timed out while waiting for authentication',
22+
*args,
23+
**kwargs,
24+
):
25+
super().__init__(msg, *args, **kwargs)
26+
27+
28+
class MissingOpenConnectError(Exception):
29+
"""If openconnect could not be found on the PATH."""
30+
31+
def __init__(
32+
self,
33+
msg="openconnect binary could not be found. make sure it's installed and on your PATH",
34+
*args,
35+
**kwargs,
36+
):
37+
super().__init__(msg, *args, **kwargs)

0 commit comments

Comments
 (0)