1353 lines
54 KiB
Python
1353 lines
54 KiB
Python
|
#!/usr/bin/env python3
|
|||
|
"""
|
|||
|
* **************************************************************************
|
|||
|
* Contributions to this work were made on behalf of the GÉANT project,
|
|||
|
* a project that has received funding from the European Union’s Framework
|
|||
|
* Programme 7 under Grant Agreements No. 238875 (GN3)
|
|||
|
* and No. 605243 (GN3plus), Horizon 2020 research and innovation programme
|
|||
|
* under Grant Agreements No. 691567 (GN4-1) and No. 731122 (GN4-2).
|
|||
|
* On behalf of the aforementioned projects, GEANT Association is
|
|||
|
* the sole owner of the copyright in all material which was developed
|
|||
|
* by a member of the GÉANT project.
|
|||
|
* GÉANT Vereniging (Association) is registered with the Chamber of
|
|||
|
* Commerce in Amsterdam with registration number 40535155 and operates
|
|||
|
* in the UK as a branch of GÉANT Vereniging.
|
|||
|
*
|
|||
|
* Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands.
|
|||
|
* UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
|
|||
|
*
|
|||
|
* License: see the web/copyright.inc.php file in the file structure or
|
|||
|
* <base_url>/copyright.php after deploying the software
|
|||
|
|
|||
|
Authors:
|
|||
|
Tomasz Wolniewicz <twoln@umk.pl>
|
|||
|
Michał Gasewicz <genn@umk.pl> (Network Manager support)
|
|||
|
|
|||
|
Contributors:
|
|||
|
Steffen Klemer https://github.com/sklemer1
|
|||
|
ikreb7 https://github.com/ikreb7
|
|||
|
Dimitri Papadopoulos Orfanos https://github.com/DimitriPapadopoulos
|
|||
|
sdasda7777 https://github.com/sdasda7777
|
|||
|
Many thanks for multiple code fixes, feature ideas, styling remarks
|
|||
|
much of the code provided by them in the form of pull requests
|
|||
|
has been incorporated into the final form of this script.
|
|||
|
|
|||
|
This script is the main body of the CAT Linux installer.
|
|||
|
In the generation process configuration settings are added
|
|||
|
as well as messages which are getting translated into the language
|
|||
|
selected by the user.
|
|||
|
|
|||
|
The script runs under python3.
|
|||
|
|
|||
|
"""
|
|||
|
import argparse
|
|||
|
import base64
|
|||
|
import getpass
|
|||
|
import os
|
|||
|
import platform
|
|||
|
import re
|
|||
|
import subprocess
|
|||
|
import sys
|
|||
|
import uuid
|
|||
|
from shutil import copyfile
|
|||
|
from typing import List, Type, Union
|
|||
|
|
|||
|
NM_AVAILABLE = True
|
|||
|
NEW_CRYPTO_AVAILABLE = True
|
|||
|
OPENSSL_CRYPTO_AVAILABLE = False
|
|||
|
DEBUG_ON = False
|
|||
|
|
|||
|
parser = argparse.ArgumentParser(description='eduroam linux installer.')
|
|||
|
parser.add_argument('--debug', '-d', action='store_true', dest='debug',
|
|||
|
default=False, help='set debug flag')
|
|||
|
parser.add_argument('--username', '-u', action='store', dest='username',
|
|||
|
help='set username')
|
|||
|
parser.add_argument('--password', '-p', action='store', dest='password',
|
|||
|
help='set text_mode flag')
|
|||
|
parser.add_argument('--silent', '-s', action='store_true', dest='silent',
|
|||
|
help='set silent flag')
|
|||
|
parser.add_argument('--pfxfile', action='store', dest='pfx_file',
|
|||
|
help='set path to user certificate file')
|
|||
|
parser.add_argument("--wpa_conf", action='store_true', dest='wpa_conf',
|
|||
|
help='generate wpa_supplicant config file without configuring the system')
|
|||
|
parser.add_argument("--gui", action='store', dest='gui',
|
|||
|
help='one of: tty, tkinter, zenity, kdialog, yad - use this GUI system if present, falling back to standard choice if not')
|
|||
|
ARGS = parser.parse_args()
|
|||
|
if ARGS.debug:
|
|||
|
DEBUG_ON = True
|
|||
|
print("Running debug mode")
|
|||
|
|
|||
|
|
|||
|
def debug(msg) -> None:
|
|||
|
"""Print debugging messages to stdout"""
|
|||
|
if not DEBUG_ON:
|
|||
|
return
|
|||
|
print("DEBUG:" + str(msg))
|
|||
|
|
|||
|
|
|||
|
def byte_to_string(barray: List) -> str:
|
|||
|
"""conversion utility"""
|
|||
|
return "".join([chr(x) for x in barray])
|
|||
|
|
|||
|
|
|||
|
debug(sys.version_info.major)
|
|||
|
|
|||
|
try:
|
|||
|
import dbus
|
|||
|
except ImportError:
|
|||
|
print("WARNING: Cannot import the dbus module - please install dbus-python!")
|
|||
|
debug("Cannot import the dbus module")
|
|||
|
NM_AVAILABLE = False
|
|||
|
|
|||
|
|
|||
|
try:
|
|||
|
from cryptography.hazmat.primitives.serialization import pkcs12
|
|||
|
from cryptography.hazmat.backends import default_backend
|
|||
|
from cryptography.x509.oid import NameOID
|
|||
|
except ImportError:
|
|||
|
NEW_CRYPTO_AVAILABLE = False
|
|||
|
try:
|
|||
|
from OpenSSL import crypto
|
|||
|
crypto.load_pkcs12 # missing in newer versions
|
|||
|
OPENSSL_CRYPTO_AVAILABLE = True
|
|||
|
except (ImportError, AttributeError): # AttributeError sometimes thrown by old/broken OpenSSL versions
|
|||
|
OPENSSL_CRYPTO_AVAILABLE = False
|
|||
|
|
|||
|
|
|||
|
def detect_desktop_environment() -> str:
|
|||
|
"""
|
|||
|
Detect what desktop type is used. This method is prepared for
|
|||
|
possible future use with password encryption on supported distros
|
|||
|
|
|||
|
the function below was partially copied from
|
|||
|
https://ubuntuforums.org/showthread.php?t=1139057
|
|||
|
"""
|
|||
|
desktop_environment = 'generic'
|
|||
|
if os.environ.get('KDE_FULL_SESSION') == 'true':
|
|||
|
desktop_environment = 'kde'
|
|||
|
elif os.environ.get('GNOME_DESKTOP_SESSION_ID'):
|
|||
|
desktop_environment = 'gnome'
|
|||
|
else:
|
|||
|
try:
|
|||
|
shell_command = subprocess.Popen(['xprop', '-root',
|
|||
|
'_DT_SAVE_MODE'],
|
|||
|
stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.PIPE)
|
|||
|
out, _ = shell_command.communicate()
|
|||
|
info = out.decode('utf-8').strip()
|
|||
|
except (OSError, RuntimeError):
|
|||
|
pass
|
|||
|
else:
|
|||
|
if ' = "xfce4"' in info:
|
|||
|
desktop_environment = 'xfce'
|
|||
|
return desktop_environment
|
|||
|
|
|||
|
|
|||
|
def get_system() -> List:
|
|||
|
"""
|
|||
|
Detect Linux platform. Not used at this stage.
|
|||
|
It is meant to enable password encryption in distros
|
|||
|
that can handle this well.
|
|||
|
"""
|
|||
|
system = platform.system_alias(
|
|||
|
platform.system(),
|
|||
|
platform.release(),
|
|||
|
platform.version()
|
|||
|
)
|
|||
|
return [system, detect_desktop_environment()]
|
|||
|
|
|||
|
|
|||
|
def get_config_path() -> str:
|
|||
|
"""
|
|||
|
Return XDG_CONFIG_HOME path if exists otherwise $HOME/.config
|
|||
|
"""
|
|||
|
|
|||
|
xdg_config_home_path = os.environ.get('XDG_CONFIG_HOME')
|
|||
|
if not xdg_config_home_path:
|
|||
|
home_path = os.environ.get('HOME')
|
|||
|
return '{}/.config'.format(home_path)
|
|||
|
return xdg_config_home_path
|
|||
|
|
|||
|
|
|||
|
def run_installer() -> None:
|
|||
|
"""
|
|||
|
This is the main installer part. It tests for NM availability
|
|||
|
gets user credentials and starts a proper installer.
|
|||
|
"""
|
|||
|
global ARGS
|
|||
|
global NM_AVAILABLE
|
|||
|
username = ''
|
|||
|
password = ''
|
|||
|
silent = False
|
|||
|
pfx_file = ''
|
|||
|
gui = ''
|
|||
|
wpa_conf = False
|
|||
|
|
|||
|
if ARGS.username:
|
|||
|
username = ARGS.username
|
|||
|
if ARGS.password:
|
|||
|
password = ARGS.password
|
|||
|
if ARGS.silent:
|
|||
|
silent = ARGS.silent
|
|||
|
if ARGS.pfx_file:
|
|||
|
pfx_file = ARGS.pfx_file
|
|||
|
if ARGS.wpa_conf:
|
|||
|
wpa_conf = ARGS.wpa_conf
|
|||
|
if ARGS.gui:
|
|||
|
gui = ARGS.gui
|
|||
|
debug(get_system())
|
|||
|
debug("Calling InstallerData")
|
|||
|
installer_data = InstallerData(silent=silent, username=username,
|
|||
|
password=password, pfx_file=pfx_file, gui=gui)
|
|||
|
|
|||
|
if wpa_conf:
|
|||
|
NM_AVAILABLE = False
|
|||
|
|
|||
|
# test dbus connection
|
|||
|
if NM_AVAILABLE:
|
|||
|
config_tool = CatNMConfigTool()
|
|||
|
if config_tool.connect_to_nm() is None:
|
|||
|
NM_AVAILABLE = False
|
|||
|
if not NM_AVAILABLE and not wpa_conf:
|
|||
|
# no dbus so ask if the user will want wpa_supplicant config
|
|||
|
if installer_data.ask(Messages.save_wpa_conf, Messages.cont, 1):
|
|||
|
sys.exit(1)
|
|||
|
installer_data.get_user_cred()
|
|||
|
installer_data.save_ca()
|
|||
|
if NM_AVAILABLE:
|
|||
|
config_tool.add_connections(installer_data)
|
|||
|
else:
|
|||
|
wpa_config = WpaConf()
|
|||
|
wpa_config.create_wpa_conf(Config.ssids, installer_data)
|
|||
|
installer_data.show_info(Messages.installation_finished)
|
|||
|
|
|||
|
|
|||
|
class Messages:
|
|||
|
"""
|
|||
|
These are initial definitions of messages, but they will be
|
|||
|
overridden with translated strings.
|
|||
|
"""
|
|||
|
quit = "Really quit?"
|
|||
|
credentials_prompt = "Please, enter your credentials:"
|
|||
|
username_prompt = "enter your userid"
|
|||
|
enter_password = "enter password"
|
|||
|
enter_import_password = "enter your import password"
|
|||
|
incorrect_password = "incorrect password"
|
|||
|
repeat_password = "repeat your password"
|
|||
|
passwords_differ = "passwords do not match"
|
|||
|
installation_finished = "Installation successful"
|
|||
|
cat_dir_exists = "Directory {} exists; some of its files may be " \
|
|||
|
"overwritten."
|
|||
|
cont = "Continue?"
|
|||
|
nm_not_supported = "This NetworkManager version is not supported"
|
|||
|
cert_error = "Certificate file not found, looks like a CAT error"
|
|||
|
unknown_version = "Unknown version"
|
|||
|
dbus_error = "DBus connection problem, a sudo might help"
|
|||
|
ok = "OK"
|
|||
|
yes = "Y"
|
|||
|
nay = "N"
|
|||
|
p12_filter = "personal certificate file (p12 or pfx)"
|
|||
|
all_filter = "All files"
|
|||
|
p12_title = "personal certificate file (p12 or pfx)"
|
|||
|
save_wpa_conf = "NetworkManager configuration failed. " \
|
|||
|
"Ensure you have the dbus-python package for your distro installed on your system. " \
|
|||
|
"We may generate a wpa_supplicant configuration file " \
|
|||
|
"if you wish. Be warned that your connection password will be saved " \
|
|||
|
"in this file as clear text."
|
|||
|
save_wpa_confirm = "Write the file"
|
|||
|
wrongUsernameFormat = "Error: Your username must be of the form " \
|
|||
|
"'xxx@institutionID' e.g. 'john@example.net'!"
|
|||
|
wrong_realm = "Error: your username must be in the form of 'xxx@{}'. " \
|
|||
|
"Please enter the username in the correct format."
|
|||
|
wrong_realm_suffix = "Error: your username must be in the form of " \
|
|||
|
"'xxx@institutionID' and end with '{}'. Please enter the username " \
|
|||
|
"in the correct format."
|
|||
|
user_cert_missing = "personal certificate file not found"
|
|||
|
# "File %s exists; it will be overwritten."
|
|||
|
# "Output written to %s"
|
|||
|
|
|||
|
|
|||
|
class Config:
|
|||
|
"""
|
|||
|
This is used to prepare settings during installer generation.
|
|||
|
"""
|
|||
|
instname = ""
|
|||
|
profilename = ""
|
|||
|
url = ""
|
|||
|
email = ""
|
|||
|
title = "eduroam CAT"
|
|||
|
servers = []
|
|||
|
ssids = []
|
|||
|
del_ssids = []
|
|||
|
eap_outer = ''
|
|||
|
eap_inner = ''
|
|||
|
use_other_tls_id = False
|
|||
|
server_match = ''
|
|||
|
anonymous_identity = ''
|
|||
|
CA = ""
|
|||
|
init_info = ""
|
|||
|
init_confirmation = ""
|
|||
|
tou = ""
|
|||
|
sb_user_file = ""
|
|||
|
verify_user_realm_input = False
|
|||
|
user_realm = ""
|
|||
|
hint_user_input = False
|
|||
|
|
|||
|
|
|||
|
class InstallerData:
|
|||
|
"""
|
|||
|
General user interaction handling, supports zenity, KDialog, yad and
|
|||
|
standard command-line interface
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, silent: bool = False, username: str = '',
|
|||
|
password: str = '', pfx_file: str = '', gui: str = '') -> None:
|
|||
|
self.graphics = ''
|
|||
|
self.username = username
|
|||
|
self.password = password
|
|||
|
self.silent = silent
|
|||
|
self.pfx_file = pfx_file
|
|||
|
if gui in ('tty', 'tkinter', 'yad', 'zenity', 'kdialog'):
|
|||
|
self.gui = gui
|
|||
|
else:
|
|||
|
self.gui = ''
|
|||
|
debug("starting constructor")
|
|||
|
if silent:
|
|||
|
self.graphics = 'tty'
|
|||
|
else:
|
|||
|
self.__get_graphics_support()
|
|||
|
self.show_info(Config.init_info.format(Config.instname,
|
|||
|
Config.email, Config.url))
|
|||
|
if self.ask(Config.init_confirmation.format(Config.instname,
|
|||
|
Config.profilename),
|
|||
|
Messages.cont, 1):
|
|||
|
sys.exit(1)
|
|||
|
if Config.tou != '':
|
|||
|
if self.ask(Config.tou, Messages.cont, 1):
|
|||
|
sys.exit(1)
|
|||
|
if os.path.exists(get_config_path() + '/cat_installer'):
|
|||
|
if self.ask(Messages.cat_dir_exists.format(
|
|||
|
get_config_path() + '/cat_installer'),
|
|||
|
Messages.cont, 1):
|
|||
|
sys.exit(1)
|
|||
|
else:
|
|||
|
os.mkdir(get_config_path() + '/cat_installer', 0o700)
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def save_ca() -> None:
|
|||
|
"""
|
|||
|
Save CA certificate to cat_installer directory
|
|||
|
(create directory if needed)
|
|||
|
"""
|
|||
|
certfile = get_config_path() + '/cat_installer/ca.pem'
|
|||
|
debug("saving cert")
|
|||
|
with open(certfile, 'w') as cert:
|
|||
|
cert.write(Config.CA + "\n")
|
|||
|
|
|||
|
def ask(self, question: str, prompt: str = '', default: bool = None) -> int:
|
|||
|
"""
|
|||
|
Prompt user for a Y/N reply, possibly supplying a default answer
|
|||
|
"""
|
|||
|
if self.silent:
|
|||
|
return 0
|
|||
|
if self.graphics == 'tty':
|
|||
|
yes = Messages.yes[:1].upper()
|
|||
|
nay = Messages.nay[:1].upper()
|
|||
|
print("\n-------\n" + question + "\n")
|
|||
|
while True:
|
|||
|
tmp = prompt + " (" + Messages.yes + "/" + Messages.nay + ") "
|
|||
|
if default == 1:
|
|||
|
tmp += "[" + yes + "]"
|
|||
|
elif default == 0:
|
|||
|
tmp += "[" + nay + "]"
|
|||
|
inp = input(tmp)
|
|||
|
if inp == '':
|
|||
|
if default == 1:
|
|||
|
return 0
|
|||
|
if default == 0:
|
|||
|
return 1
|
|||
|
i = inp[:1].upper()
|
|||
|
if i == yes:
|
|||
|
return 0
|
|||
|
if i == nay:
|
|||
|
return 1
|
|||
|
elif self.graphics == 'tkinter':
|
|||
|
from tkinter import messagebox
|
|||
|
return 0 if messagebox.askyesno(Config.title, question + "\n\n" + prompt) else 1
|
|||
|
else:
|
|||
|
command = []
|
|||
|
if self.graphics == "zenity":
|
|||
|
command = ['zenity', '--title=' + Config.title, '--width=500',
|
|||
|
'--question', '--text=' + question + "\n\n" + prompt]
|
|||
|
elif self.graphics == 'kdialog':
|
|||
|
command = ['kdialog', '--yesno', question + "\n\n" + prompt,
|
|||
|
'--title=' + Config.title]
|
|||
|
elif self.graphics == 'yad':
|
|||
|
command = ['yad', '--image="dialog-question"',
|
|||
|
'--button=gtk-yes:0',
|
|||
|
'--button=gtk-no:1',
|
|||
|
'--width=500',
|
|||
|
'--wrap',
|
|||
|
'--text=' + question + "\n\n" + prompt,
|
|||
|
'--title=' + Config.title]
|
|||
|
returncode = subprocess.call(command, stderr=subprocess.DEVNULL)
|
|||
|
return returncode
|
|||
|
|
|||
|
def show_info(self, data: str) -> None:
|
|||
|
"""
|
|||
|
Show a piece of information
|
|||
|
"""
|
|||
|
if self.silent:
|
|||
|
return
|
|||
|
if self.graphics == 'tty':
|
|||
|
print(data)
|
|||
|
elif self.graphics == 'tkinter':
|
|||
|
from tkinter import messagebox
|
|||
|
messagebox.showinfo(Config.title, data)
|
|||
|
else:
|
|||
|
if self.graphics == "zenity":
|
|||
|
command = ['zenity', '--info', '--width=500', '--text=' + data]
|
|||
|
elif self.graphics == "kdialog":
|
|||
|
command = ['kdialog', '--msgbox', data, '--title=' + Config.title]
|
|||
|
elif self.graphics == "yad":
|
|||
|
command = ['yad', '--button=OK', '--width=500', '--text=' + data]
|
|||
|
else:
|
|||
|
sys.exit(1)
|
|||
|
subprocess.call(command, stderr=subprocess.DEVNULL)
|
|||
|
|
|||
|
def confirm_exit(self) -> None:
|
|||
|
"""
|
|||
|
Confirm exit from installer
|
|||
|
"""
|
|||
|
ret = self.ask(Messages.quit)
|
|||
|
if ret == 0:
|
|||
|
sys.exit(1)
|
|||
|
|
|||
|
def alert(self, text: str) -> None:
|
|||
|
"""Generate alert message"""
|
|||
|
if self.silent:
|
|||
|
return
|
|||
|
if self.graphics == 'tty':
|
|||
|
print(text)
|
|||
|
elif self.graphics == 'tkinter':
|
|||
|
from tkinter import messagebox
|
|||
|
messagebox.showwarning(Config.title, text)
|
|||
|
else:
|
|||
|
if self.graphics == 'zenity':
|
|||
|
command = ['zenity', '--warning', '--text=' + text]
|
|||
|
elif self.graphics == "kdialog":
|
|||
|
command = ['kdialog', '--sorry', text, '--title=' + Config.title]
|
|||
|
elif self.graphics == "yad":
|
|||
|
command = ['yad', '--text=' + text]
|
|||
|
else:
|
|||
|
sys.exit(1)
|
|||
|
subprocess.call(command, stderr=subprocess.DEVNULL)
|
|||
|
|
|||
|
def prompt_nonempty_string(self, show: int, prompt: str, val: str = '') -> str:
|
|||
|
"""
|
|||
|
Prompt user for input
|
|||
|
"""
|
|||
|
if self.graphics == 'tty':
|
|||
|
if show == 0:
|
|||
|
while True:
|
|||
|
inp = str(getpass.getpass(prompt + ": "))
|
|||
|
output = inp.strip()
|
|||
|
if output != '':
|
|||
|
return output
|
|||
|
while True:
|
|||
|
inp = input(prompt + ": ")
|
|||
|
output = inp.strip()
|
|||
|
if output != '':
|
|||
|
return output
|
|||
|
elif self.graphics == 'tkinter':
|
|||
|
from tkinter import simpledialog
|
|||
|
while True:
|
|||
|
output = simpledialog.askstring(Config.title, prompt,
|
|||
|
initialvalue=val,
|
|||
|
show="*" if show == 0 else "")
|
|||
|
if output:
|
|||
|
return output
|
|||
|
|
|||
|
else:
|
|||
|
command = []
|
|||
|
if self.graphics == 'zenity':
|
|||
|
if val == '':
|
|||
|
default_val = ''
|
|||
|
else:
|
|||
|
default_val = '--entry-text=' + val
|
|||
|
if show == 0:
|
|||
|
hide_text = '--hide-text'
|
|||
|
else:
|
|||
|
hide_text = ''
|
|||
|
command = ['zenity', '--entry', hide_text, default_val,
|
|||
|
'--width=500', '--text=' + prompt]
|
|||
|
elif self.graphics == 'kdialog':
|
|||
|
if show == 0:
|
|||
|
hide_text = '--password'
|
|||
|
else:
|
|||
|
hide_text = '--inputbox'
|
|||
|
command = ['kdialog', hide_text, prompt, '--title=' + Config.title]
|
|||
|
elif self.graphics == 'yad':
|
|||
|
if show == 0:
|
|||
|
hide_text = ':H'
|
|||
|
else:
|
|||
|
hide_text = ''
|
|||
|
command = ['yad', '--form', '--field=' + hide_text,
|
|||
|
'--text=' + prompt, val]
|
|||
|
|
|||
|
output = ''
|
|||
|
while not output:
|
|||
|
shell_command = subprocess.Popen(command, stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.PIPE)
|
|||
|
out, _ = shell_command.communicate()
|
|||
|
output = out.decode('utf-8')
|
|||
|
if self.graphics == 'yad':
|
|||
|
output = output[:-2]
|
|||
|
output = output.strip()
|
|||
|
if shell_command.returncode == 1:
|
|||
|
self.confirm_exit()
|
|||
|
return output
|
|||
|
|
|||
|
def __get_username_password_atomic(self) -> None:
|
|||
|
"""
|
|||
|
use single form to get username, password and password confirmation
|
|||
|
"""
|
|||
|
output_fields_separator = "\n\n\n\n\n"
|
|||
|
while True:
|
|||
|
password = "a"
|
|||
|
password1 = "b"
|
|||
|
if self.graphics == 'tkinter':
|
|||
|
import tkinter as tk
|
|||
|
|
|||
|
root = tk.Tk()
|
|||
|
root.title(Config.title)
|
|||
|
|
|||
|
desc_label = tk.Label(root, text=Messages.credentials_prompt)
|
|||
|
desc_label.grid(row=0, column=0, columnspan=2, sticky=tk.W)
|
|||
|
|
|||
|
username_label = tk.Label(root, text=Messages.username_prompt)
|
|||
|
username_label.grid(row=1, column=0, sticky=tk.W)
|
|||
|
|
|||
|
username_entry = tk.Entry(root, textvariable=tk.StringVar(root, value=self.username))
|
|||
|
username_entry.grid(row=1, column=1)
|
|||
|
|
|||
|
password_label = tk.Label(root, text=Messages.enter_password)
|
|||
|
password_label.grid(row=2, column=0, sticky=tk.W)
|
|||
|
|
|||
|
password_entry = tk.Entry(root, show="*")
|
|||
|
password_entry.grid(row=2, column=1)
|
|||
|
|
|||
|
password1_label = tk.Label(root, text=Messages.repeat_password)
|
|||
|
password1_label.grid(row=3, column=0, sticky=tk.W)
|
|||
|
|
|||
|
password1_entry = tk.Entry(root, show="*")
|
|||
|
password1_entry.grid(row=3, column=1)
|
|||
|
|
|||
|
def submit(installer):
|
|||
|
def inner():
|
|||
|
nonlocal password, password1
|
|||
|
(installer.username, password, password1) = (username_entry.get(), password_entry.get(), password1_entry.get())
|
|||
|
root.destroy()
|
|||
|
return inner
|
|||
|
|
|||
|
login_button = tk.Button(root, text=Messages.ok, command=submit(self))
|
|||
|
login_button.grid(row=4, column=0, columnspan=2)
|
|||
|
|
|||
|
root.mainloop()
|
|||
|
else:
|
|||
|
if self.graphics == 'zenity':
|
|||
|
command = ['zenity', '--forms', '--width=500',
|
|||
|
f"--title={Config.title}",
|
|||
|
f"--text={Messages.credentials_prompt}",
|
|||
|
f"--add-entry={Messages.username_prompt}",
|
|||
|
f"--add-password={Messages.enter_password}",
|
|||
|
f"--add-password={Messages.repeat_password}",
|
|||
|
"--separator", output_fields_separator]
|
|||
|
elif self.graphics == 'yad':
|
|||
|
command = ['yad', '--form',
|
|||
|
f"--title={Config.title}",
|
|||
|
f"--text={Messages.credentials_prompt}",
|
|||
|
f"--field={Messages.username_prompt}", self.username,
|
|||
|
f"--field={Messages.enter_password}:H",
|
|||
|
f"--field={Messages.repeat_password}:H",
|
|||
|
"--separator", output_fields_separator]
|
|||
|
|
|||
|
output = ''
|
|||
|
while not output:
|
|||
|
shell_command = subprocess.Popen(command, stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.PIPE)
|
|||
|
out, _ = shell_command.communicate()
|
|||
|
output = out.decode('utf-8')
|
|||
|
if self.graphics == 'yad':
|
|||
|
output = output[:-(len(output_fields_separator)+1)]
|
|||
|
output = output.strip()
|
|||
|
if shell_command.returncode == 1:
|
|||
|
self.confirm_exit()
|
|||
|
|
|||
|
if self.graphics in ('zenity', 'yad'):
|
|||
|
self.username, password, password1 = output.split(output_fields_separator)
|
|||
|
|
|||
|
if not self.__validate_user_name():
|
|||
|
continue
|
|||
|
if password != password1:
|
|||
|
self.alert(Messages.passwords_differ)
|
|||
|
continue
|
|||
|
self.password = password
|
|||
|
break
|
|||
|
|
|||
|
def get_user_cred(self) -> None:
|
|||
|
"""
|
|||
|
Get user credentials both username/password and personal certificate
|
|||
|
based
|
|||
|
"""
|
|||
|
if Config.eap_outer in ('PEAP', 'TTLS'):
|
|||
|
self.__get_username_password()
|
|||
|
elif Config.eap_outer == 'TLS':
|
|||
|
self.__get_p12_cred()
|
|||
|
|
|||
|
def __get_username_password(self) -> None:
|
|||
|
"""
|
|||
|
read user password and set the password property
|
|||
|
do nothing if silent mode is set
|
|||
|
"""
|
|||
|
if self.silent:
|
|||
|
return
|
|||
|
if self.graphics in ('tkinter', 'zenity', 'yad'):
|
|||
|
self.__get_username_password_atomic()
|
|||
|
else:
|
|||
|
password = "a"
|
|||
|
password1 = "b"
|
|||
|
if self.username:
|
|||
|
user_prompt = self.username
|
|||
|
elif Config.hint_user_input:
|
|||
|
user_prompt = '@' + Config.user_realm
|
|||
|
else:
|
|||
|
user_prompt = ''
|
|||
|
while True:
|
|||
|
self.username = self.prompt_nonempty_string(
|
|||
|
1, Messages.username_prompt, user_prompt)
|
|||
|
if self.__validate_user_name():
|
|||
|
break
|
|||
|
while password != password1:
|
|||
|
password = self.prompt_nonempty_string(
|
|||
|
0, Messages.enter_password)
|
|||
|
password1 = self.prompt_nonempty_string(
|
|||
|
0, Messages.repeat_password)
|
|||
|
if password != password1:
|
|||
|
self.alert(Messages.passwords_differ)
|
|||
|
self.password = password
|
|||
|
|
|||
|
def __check_graphics(self, command) -> bool:
|
|||
|
shell_command = subprocess.Popen(['which', command],
|
|||
|
stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.PIPE)
|
|||
|
shell_command.wait()
|
|||
|
if shell_command.returncode == 0:
|
|||
|
self.graphics = command
|
|||
|
debug("Using "+command)
|
|||
|
return True
|
|||
|
return False
|
|||
|
|
|||
|
def __get_graphics_support(self) -> None:
|
|||
|
self.graphics = 'tty'
|
|||
|
if self.gui == 'tty':
|
|||
|
return
|
|||
|
if os.environ.get('DISPLAY') is None:
|
|||
|
return
|
|||
|
if self.gui != 'tkinter':
|
|||
|
if self.__check_graphics(self.gui):
|
|||
|
return
|
|||
|
try:
|
|||
|
import tkinter
|
|||
|
self.graphics = 'tkinter'
|
|||
|
return
|
|||
|
except Exception:
|
|||
|
pass
|
|||
|
for cmd in ('yad', 'zenity', 'kdialog'):
|
|||
|
if self.__check_graphics(cmd):
|
|||
|
return
|
|||
|
|
|||
|
def __process_p12(self) -> bool:
|
|||
|
debug('process_p12')
|
|||
|
pfx_file = get_config_path() + '/cat_installer/user.p12'
|
|||
|
if NEW_CRYPTO_AVAILABLE:
|
|||
|
debug("using new crypto")
|
|||
|
try:
|
|||
|
p12 = pkcs12.load_key_and_certificates(
|
|||
|
open(pfx_file,'rb').read(),
|
|||
|
self.password, backend=default_backend())
|
|||
|
except Exception as error:
|
|||
|
debug("Incorrect password ({}).".format(error))
|
|||
|
return False
|
|||
|
else:
|
|||
|
if Config.use_other_tls_id:
|
|||
|
return True
|
|||
|
try:
|
|||
|
self.username = p12[1].subject.\
|
|||
|
get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
|||
|
except crypto.Error:
|
|||
|
self.username = p12[1].subject.\
|
|||
|
get_attributes_for_oid(NameOID.EMAIL_ADDRESS)[0].value
|
|||
|
return True
|
|||
|
if OPENSSL_CRYPTO_AVAILABLE:
|
|||
|
debug("using openssl crypto")
|
|||
|
try:
|
|||
|
p12 = crypto.load_pkcs12(open(pfx_file, 'rb').read(),
|
|||
|
self.password)
|
|||
|
except crypto.Error as error:
|
|||
|
debug("Incorrect password ({}).".format(error))
|
|||
|
return False
|
|||
|
else:
|
|||
|
if Config.use_other_tls_id:
|
|||
|
return True
|
|||
|
try:
|
|||
|
self.username = p12.get_certificate(). \
|
|||
|
get_subject().commonName
|
|||
|
except crypto.Error:
|
|||
|
self.username = p12.get_certificate().\
|
|||
|
get_subject().emailAddress
|
|||
|
return True
|
|||
|
debug("using openssl")
|
|||
|
command = ['openssl', 'pkcs12', '-in', pfx_file, '-passin',
|
|||
|
'pass:' + self.password, '-nokeys', '-clcerts']
|
|||
|
shell_command = subprocess.Popen(command, stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.PIPE)
|
|||
|
out, _ = shell_command.communicate()
|
|||
|
if shell_command.returncode != 0:
|
|||
|
debug("first password run failed")
|
|||
|
command1 = ['openssl', 'pkcs12', '-legacy', '-in', pfx_file, '-passin',
|
|||
|
'pass:' + self.password, '-nokeys', '-clcerts']
|
|||
|
shell_command1 = subprocess.Popen(command1, stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.PIPE)
|
|||
|
out, err = shell_command1.communicate()
|
|||
|
if shell_command1.returncode != 0:
|
|||
|
return False
|
|||
|
if Config.use_other_tls_id:
|
|||
|
return True
|
|||
|
out_str = out.decode('utf-8').strip()
|
|||
|
# split only on commas that are not inside double quotes
|
|||
|
subject = re.split(r'\s*[/,]\s*(?=([^"]*"[^"]*")*[^"]*$)',
|
|||
|
re.findall(r'subject=/?(.*)$',
|
|||
|
out_str, re.MULTILINE)[0])
|
|||
|
cert_prop = {}
|
|||
|
for field in subject:
|
|||
|
if field:
|
|||
|
cert_field = re.split(r'\s*=\s*', field)
|
|||
|
cert_prop[cert_field[0].lower()] = cert_field[1]
|
|||
|
if cert_prop['cn'] and re.search(r'@', cert_prop['cn']):
|
|||
|
debug('Using cn: ' + cert_prop['cn'])
|
|||
|
self.username = cert_prop['cn']
|
|||
|
elif cert_prop['emailaddress'] and \
|
|||
|
re.search(r'@', cert_prop['emailaddress']):
|
|||
|
debug('Using email: ' + cert_prop['emailaddress'])
|
|||
|
self.username = cert_prop['emailaddress']
|
|||
|
else:
|
|||
|
self.username = ''
|
|||
|
self.alert("Unable to extract username "
|
|||
|
"from the certificate")
|
|||
|
return True
|
|||
|
|
|||
|
def __select_p12_file(self) -> str:
|
|||
|
"""
|
|||
|
prompt user for the PFX file selection
|
|||
|
this method is not being called in the silent mode
|
|||
|
therefore there is no code for this case
|
|||
|
"""
|
|||
|
if self.graphics == 'tty':
|
|||
|
my_dir = os.listdir(".")
|
|||
|
p_count = 0
|
|||
|
pfx_file = ''
|
|||
|
for my_file in my_dir:
|
|||
|
if my_file.endswith(('.p12', '*.pfx', '.P12', '*.PFX')):
|
|||
|
p_count += 1
|
|||
|
pfx_file = my_file
|
|||
|
prompt = "personal certificate file (p12 or pfx)"
|
|||
|
default = ''
|
|||
|
if p_count == 1:
|
|||
|
default = '[' + pfx_file + ']'
|
|||
|
|
|||
|
while True:
|
|||
|
inp = input(prompt + default + ": ")
|
|||
|
output = inp.strip()
|
|||
|
|
|||
|
if default != '' and output == '':
|
|||
|
return pfx_file
|
|||
|
default = ''
|
|||
|
if os.path.isfile(output):
|
|||
|
return output
|
|||
|
print("file not found")
|
|||
|
|
|||
|
cert = ""
|
|||
|
if self.graphics == 'zenity':
|
|||
|
command = ['zenity', '--file-selection',
|
|||
|
'--file-filter=' + Messages.p12_filter +
|
|||
|
' | *.p12 *.P12 *.pfx *.PFX', '--file-filter=' +
|
|||
|
Messages.all_filter + ' | *',
|
|||
|
'--title=' + Messages.p12_title]
|
|||
|
shell_command = subprocess.Popen(command, stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.PIPE)
|
|||
|
cert, _ = shell_command.communicate()
|
|||
|
if self.graphics == 'kdialog':
|
|||
|
command = ['kdialog', '--getopenfilename', '.',
|
|||
|
'--title=' + Messages.p12_title]
|
|||
|
shell_command = subprocess.Popen(command, stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.DEVNULL)
|
|||
|
cert, _ = shell_command.communicate()
|
|||
|
if self.graphics == 'yad':
|
|||
|
command = ['yad', '--file',
|
|||
|
'--file-filter=*.p12 *.P12 *.pfx *.PFX',
|
|||
|
'-file-filter=*', '--title=' + Messages.p12_title]
|
|||
|
shell_command = subprocess.Popen(command, stdout=subprocess.PIPE,
|
|||
|
stderr=subprocess.DEVNULL)
|
|||
|
cert, _ = shell_command.communicate()
|
|||
|
if self.graphics == 'tkinter':
|
|||
|
from tkinter import filedialog as fd
|
|||
|
return fd.askopenfilename(title=Messages.p12_title,
|
|||
|
filetypes=(("Certificate file",
|
|||
|
("*.p12", "*.P12", "*.pfx",
|
|||
|
"*.PFX")),))
|
|||
|
return cert.decode('utf-8').strip()
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def __save_sb_pfx() -> None:
|
|||
|
"""write the user PFX file"""
|
|||
|
cert_file = get_config_path() + '/cat_installer/user.p12'
|
|||
|
with open(cert_file, 'wb') as cert:
|
|||
|
cert.write(base64.b64decode(Config.sb_user_file))
|
|||
|
|
|||
|
def __get_p12_cred(self):
|
|||
|
"""get the password for the PFX file"""
|
|||
|
if Config.eap_inner == 'SILVERBULLET':
|
|||
|
self.__save_sb_pfx()
|
|||
|
else:
|
|||
|
if not self.silent:
|
|||
|
self.pfx_file = self.__select_p12_file()
|
|||
|
try:
|
|||
|
copyfile(self.pfx_file, get_config_path() +
|
|||
|
'/cat_installer/user.p12')
|
|||
|
except (OSError, RuntimeError):
|
|||
|
print(Messages.user_cert_missing)
|
|||
|
sys.exit(1)
|
|||
|
if self.silent:
|
|||
|
username = self.username
|
|||
|
if not self.__process_p12():
|
|||
|
sys.exit(1)
|
|||
|
if username:
|
|||
|
self.username = username
|
|||
|
else:
|
|||
|
while not self.password:
|
|||
|
self.password = self.prompt_nonempty_string(
|
|||
|
0, Messages.enter_import_password).encode('utf-8')
|
|||
|
if not self.__process_p12():
|
|||
|
self.alert(Messages.incorrect_password)
|
|||
|
self.password = ''
|
|||
|
if not self.username:
|
|||
|
self.username = self.prompt_nonempty_string(
|
|||
|
1, Messages.username_prompt)
|
|||
|
|
|||
|
def __validate_user_name(self) -> bool:
|
|||
|
# locate the @ character in username
|
|||
|
pos = self.username.find('@')
|
|||
|
debug("@ position: " + str(pos))
|
|||
|
# trailing @
|
|||
|
if pos == len(self.username) - 1:
|
|||
|
debug("username ending with @")
|
|||
|
self.alert(Messages.wrongUsernameFormat)
|
|||
|
return False
|
|||
|
# no @ at all
|
|||
|
if pos == -1:
|
|||
|
if Config.verify_user_realm_input:
|
|||
|
debug("missing realm")
|
|||
|
self.alert(Messages.wrongUsernameFormat)
|
|||
|
return False
|
|||
|
debug("No realm, but possibly correct")
|
|||
|
return True
|
|||
|
# @ at the beginning
|
|||
|
if pos == 0:
|
|||
|
debug("missing user part")
|
|||
|
self.alert(Messages.wrongUsernameFormat)
|
|||
|
return False
|
|||
|
pos += 1
|
|||
|
if Config.verify_user_realm_input:
|
|||
|
if Config.hint_user_input:
|
|||
|
if self.username.endswith('@' + Config.user_realm, pos - 1):
|
|||
|
debug("realm equal to the expected value")
|
|||
|
return True
|
|||
|
debug("incorrect realm; expected:" + Config.user_realm)
|
|||
|
self.alert(Messages.wrong_realm.format(Config.user_realm))
|
|||
|
return False
|
|||
|
if self.username.endswith(Config.user_realm, pos):
|
|||
|
debug("realm ends with expected suffix")
|
|||
|
return True
|
|||
|
debug("realm suffix error; expected: " + Config.user_realm)
|
|||
|
self.alert(Messages.wrong_realm_suffix.format(
|
|||
|
Config.user_realm))
|
|||
|
return False
|
|||
|
pos1 = self.username.find('@', pos)
|
|||
|
if pos1 > -1:
|
|||
|
debug("second @ character found")
|
|||
|
self.alert(Messages.wrongUsernameFormat)
|
|||
|
return False
|
|||
|
pos1 = self.username.find('.', pos)
|
|||
|
if pos1 == pos:
|
|||
|
debug("a dot immediately after the @ character")
|
|||
|
self.alert(Messages.wrongUsernameFormat)
|
|||
|
return False
|
|||
|
debug("all passed")
|
|||
|
return True
|
|||
|
|
|||
|
|
|||
|
class WpaConf:
|
|||
|
"""
|
|||
|
Prepare and save wpa_supplicant config file
|
|||
|
"""
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def __prepare_network_block(ssid: str, user_data: Type[InstallerData]) -> str:
|
|||
|
interface = """network={
|
|||
|
ssid=\"""" + ssid + """\"
|
|||
|
key_mgmt=WPA-EAP
|
|||
|
pairwise=CCMP
|
|||
|
group=CCMP TKIP
|
|||
|
eap=""" + Config.eap_outer + """
|
|||
|
ca_cert=\"""" + get_config_path() + """/cat_installer/ca.pem\"""""""
|
|||
|
identity=\"""" + user_data.username + """\"""""""
|
|||
|
altsubject_match=\"""" + ";".join(Config.servers) + """\"
|
|||
|
"""
|
|||
|
|
|||
|
if Config.eap_outer in ('PEAP', 'TTLS'):
|
|||
|
interface += f"phase2=\"auth={Config.eap_inner}\"\n" \
|
|||
|
f"\tpassword=\"{user_data.password}\"\n"
|
|||
|
if Config.anonymous_identity != '':
|
|||
|
interface += f"\tanonymous_identity=\"{Config.anonymous_identity}\"\n"
|
|||
|
|
|||
|
elif Config.eap_outer == 'TLS':
|
|||
|
interface += f"\tprivate_key_passwd=\"{user_data.password}\"\n" \
|
|||
|
f"\tprivate_key=\"{os.environ.get('HOME')}/.cat_installer/user.p12"
|
|||
|
|
|||
|
interface += "\n}"
|
|||
|
return interface
|
|||
|
|
|||
|
def create_wpa_conf(self, ssids, user_data: Type[InstallerData]) -> None:
|
|||
|
"""Create and save the wpa_supplicant config file"""
|
|||
|
wpa_conf = get_config_path() + '/cat_installer/cat_installer.conf'
|
|||
|
with open(wpa_conf, 'w') as conf:
|
|||
|
for ssid in ssids:
|
|||
|
net = self.__prepare_network_block(ssid, user_data)
|
|||
|
conf.write(net)
|
|||
|
|
|||
|
|
|||
|
class IwdConfiguration:
|
|||
|
""" support the iNet wireless daemon by Intel """
|
|||
|
def __init__(self):
|
|||
|
self.config = ""
|
|||
|
|
|||
|
def write_config(self) -> None:
|
|||
|
for ssid in Config.ssids:
|
|||
|
with open('/var/lib/iwd/{}.8021x'.format(ssid), 'w') as config_file:
|
|||
|
config_file.write(self.config)
|
|||
|
|
|||
|
def _create_eap_pwd_config(self, ssid: str, user_data: Type[InstallerData]) -> None:
|
|||
|
""" create EAP-PWD configuration """
|
|||
|
self.conf = """
|
|||
|
[Security]
|
|||
|
EAP-Method=PWD
|
|||
|
EAP-Identity={username}
|
|||
|
EAP-Password={password}
|
|||
|
|
|||
|
[Settings]
|
|||
|
AutoConnect=True
|
|||
|
""".format(username=user_data.username,
|
|||
|
password=user_data.password)
|
|||
|
|
|||
|
def _create_eap_peap_config(self, ssid: str, user_data: Type[InstallerData]) -> None:
|
|||
|
""" create EAP-PEAP configuration """
|
|||
|
self.conf = """
|
|||
|
[Security]
|
|||
|
EAP-Method=PEAP
|
|||
|
EAP-Identity={anonymous_identity}
|
|||
|
EAP-PEAP-CACert={ca_cert}
|
|||
|
EAP-PEAP-ServerDomainMask={servers}
|
|||
|
EAP-PEAP-Phase2-Method=MSCHAPV2
|
|||
|
EAP-PEAP-Phase2-Identity={username}@{realm}
|
|||
|
EAP-PEAP-Phase2-Password={password}
|
|||
|
|
|||
|
[Settings]
|
|||
|
AutoConnect=true
|
|||
|
""".format(anonymous_identity=Config.anonymous_identity,
|
|||
|
ca_cert=Config.CA, servers=Config.servers,
|
|||
|
username=user_data.username,
|
|||
|
realm=Config.user_realm,
|
|||
|
password=user_data.password)
|
|||
|
|
|||
|
def _create_ttls_pap_config(self, ssid: str, user_data: Type[InstallerData]) -> None:
|
|||
|
""" create TTLS-PAP configuration"""
|
|||
|
self.conf = """
|
|||
|
[Security]
|
|||
|
EAP-Method=TTLS
|
|||
|
EAP-Identity={anonymous_identity}
|
|||
|
EAP-TTLS-CACert={ca_cert}
|
|||
|
EAP-TTLS-ServerDomainMask={servers}
|
|||
|
EAP-TTLS-Phase2-Method=Tunneled-PAP
|
|||
|
EAP-TTLS-Phase2-Identity={username}@{realm}
|
|||
|
EAP-TTLS-Phase2-Password={password}
|
|||
|
|
|||
|
[Settings]
|
|||
|
AutoConnect=true
|
|||
|
""".format(anonymous_identity=Config.anonymous_identity,
|
|||
|
ca_cert=Config.CA, servers=Config.servers,
|
|||
|
username=user_data.username,
|
|||
|
realm=Config.user_realm,
|
|||
|
password=user_data.password)
|
|||
|
|
|||
|
|
|||
|
class CatNMConfigTool:
|
|||
|
"""
|
|||
|
Prepare and save NetworkManager configuration
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self):
|
|||
|
self.cacert_file = None
|
|||
|
self.settings_service_name = None
|
|||
|
self.connection_interface_name = None
|
|||
|
self.system_service_name = "org.freedesktop.NetworkManager"
|
|||
|
self.nm_version = None
|
|||
|
self.pfx_file = None
|
|||
|
self.settings = None
|
|||
|
self.user_data = None
|
|||
|
self.bus = None
|
|||
|
|
|||
|
def connect_to_nm(self) -> Union[bool, None]:
|
|||
|
"""
|
|||
|
connect to DBus
|
|||
|
"""
|
|||
|
try:
|
|||
|
self.bus = dbus.SystemBus()
|
|||
|
except AttributeError:
|
|||
|
# since dbus existed but is empty we have an empty package
|
|||
|
# this gets shipped by pyqt5
|
|||
|
print("DBus not properly installed")
|
|||
|
return None
|
|||
|
except dbus.exceptions.DBusException:
|
|||
|
print("Can't connect to DBus")
|
|||
|
return None
|
|||
|
# check NM version
|
|||
|
self.__check_nm_version()
|
|||
|
debug("NM version: " + self.nm_version)
|
|||
|
if self.nm_version in ("0.9", "1.0"):
|
|||
|
self.settings_service_name = self.system_service_name
|
|||
|
self.connection_interface_name = \
|
|||
|
"org.freedesktop.NetworkManager.Settings.Connection"
|
|||
|
# settings proxy
|
|||
|
sysproxy = self.bus.get_object(
|
|||
|
self.settings_service_name,
|
|||
|
"/org/freedesktop/NetworkManager/Settings")
|
|||
|
# settings interface
|
|||
|
self.settings = dbus.Interface(sysproxy, "org.freedesktop."
|
|||
|
"NetworkManager.Settings")
|
|||
|
elif self.nm_version == "0.8":
|
|||
|
self.settings_service_name = "org.freedesktop.NetworkManager"
|
|||
|
self.connection_interface_name = "org.freedesktop.NetworkMana" \
|
|||
|
"gerSettings.Connection"
|
|||
|
# settings proxy
|
|||
|
sysproxy = self.bus.get_object(
|
|||
|
self.settings_service_name,
|
|||
|
"/org/freedesktop/NetworkManagerSettings")
|
|||
|
# settings interface
|
|||
|
self.settings = dbus.Interface(
|
|||
|
sysproxy, "org.freedesktop.NetworkManagerSettings")
|
|||
|
else:
|
|||
|
print(Messages.nm_not_supported)
|
|||
|
return None
|
|||
|
debug("NM connection worked")
|
|||
|
return True
|
|||
|
|
|||
|
def __check_opts(self) -> None:
|
|||
|
"""
|
|||
|
set certificate files paths and test for existence of the CA cert
|
|||
|
"""
|
|||
|
self.cacert_file = get_config_path() + '/cat_installer/ca.pem'
|
|||
|
self.pfx_file = get_config_path() + '/cat_installer/user.p12'
|
|||
|
if not os.path.isfile(self.cacert_file):
|
|||
|
print(Messages.cert_error)
|
|||
|
sys.exit(2)
|
|||
|
|
|||
|
def __check_nm_version(self) -> None:
|
|||
|
"""
|
|||
|
Get the NetworkManager version
|
|||
|
"""
|
|||
|
try:
|
|||
|
proxy = self.bus.get_object(
|
|||
|
self.system_service_name, "/org/freedesktop/NetworkManager")
|
|||
|
props = dbus.Interface(proxy, "org.freedesktop.DBus.Properties")
|
|||
|
version = props.Get("org.freedesktop.NetworkManager", "Version")
|
|||
|
except dbus.exceptions.DBusException:
|
|||
|
version = ""
|
|||
|
if re.match(r'^1\.', version):
|
|||
|
self.nm_version = "1.0"
|
|||
|
return
|
|||
|
if re.match(r'^0\.9', version):
|
|||
|
self.nm_version = "0.9"
|
|||
|
return
|
|||
|
if re.match(r'^0\.8', version):
|
|||
|
self.nm_version = "0.8"
|
|||
|
return
|
|||
|
self.nm_version = Messages.unknown_version
|
|||
|
|
|||
|
def __delete_existing_connection(self, ssid: str) -> None:
|
|||
|
"""
|
|||
|
checks and deletes earlier connection
|
|||
|
"""
|
|||
|
try:
|
|||
|
conns = self.settings.ListConnections()
|
|||
|
except dbus.exceptions.DBusException:
|
|||
|
print(Messages.dbus_error)
|
|||
|
exit(3)
|
|||
|
for each in conns:
|
|||
|
con_proxy = self.bus.get_object(self.system_service_name, each)
|
|||
|
connection = dbus.Interface(
|
|||
|
con_proxy,
|
|||
|
"org.freedesktop.NetworkManager.Settings.Connection")
|
|||
|
try:
|
|||
|
connection_settings = connection.GetSettings()
|
|||
|
if connection_settings['connection']['type'] == '802-11-' \
|
|||
|
'wireless':
|
|||
|
conn_ssid = byte_to_string(
|
|||
|
connection_settings['802-11-wireless']['ssid'])
|
|||
|
if conn_ssid == ssid:
|
|||
|
debug("deleting connection: " + conn_ssid)
|
|||
|
connection.Delete()
|
|||
|
except dbus.exceptions.DBusException:
|
|||
|
pass
|
|||
|
|
|||
|
def __add_connection(self, ssid: str) -> None:
|
|||
|
debug("Adding connection: " + ssid)
|
|||
|
server_alt_subject_name_list = dbus.Array(Config.servers)
|
|||
|
server_name = Config.server_match
|
|||
|
if self.nm_version in ("0.9", "1.0"):
|
|||
|
match_key = 'altsubject-matches'
|
|||
|
match_value = server_alt_subject_name_list
|
|||
|
else:
|
|||
|
match_key = 'subject-match'
|
|||
|
match_value = server_name
|
|||
|
s_8021x_data = {
|
|||
|
'eap': [Config.eap_outer.lower()],
|
|||
|
'identity': self.user_data.username,
|
|||
|
'ca-cert': dbus.ByteArray(
|
|||
|
"file://{0}\0".format(self.cacert_file).encode('utf8')),
|
|||
|
match_key: match_value}
|
|||
|
if Config.eap_outer in ('PEAP', 'TTLS'):
|
|||
|
s_8021x_data['password'] = self.user_data.password
|
|||
|
s_8021x_data['phase2-auth'] = Config.eap_inner.lower()
|
|||
|
if Config.anonymous_identity != '':
|
|||
|
s_8021x_data['anonymous-identity'] = Config.anonymous_identity
|
|||
|
s_8021x_data['password-flags'] = 0
|
|||
|
elif Config.eap_outer == 'TLS':
|
|||
|
s_8021x_data['client-cert'] = dbus.ByteArray(
|
|||
|
"file://{0}\0".format(self.pfx_file).encode('utf8'))
|
|||
|
s_8021x_data['private-key'] = dbus.ByteArray(
|
|||
|
"file://{0}\0".format(self.pfx_file).encode('utf8'))
|
|||
|
s_8021x_data['private-key-password'] = self.user_data.password
|
|||
|
s_8021x_data['private-key-password-flags'] = 0
|
|||
|
s_con = dbus.Dictionary({
|
|||
|
'type': '802-11-wireless',
|
|||
|
'uuid': str(uuid.uuid4()),
|
|||
|
'permissions': ['user:' + os.environ.get('USER')],
|
|||
|
'id': ssid
|
|||
|
})
|
|||
|
s_wifi = dbus.Dictionary({
|
|||
|
'ssid': dbus.ByteArray(ssid.encode('utf8')),
|
|||
|
'security': '802-11-wireless-security'
|
|||
|
})
|
|||
|
s_wsec = dbus.Dictionary({
|
|||
|
'key-mgmt': 'wpa-eap',
|
|||
|
'proto': ['rsn'],
|
|||
|
'pairwise': ['ccmp'],
|
|||
|
'group': ['ccmp', 'tkip']
|
|||
|
})
|
|||
|
s_8021x = dbus.Dictionary(s_8021x_data)
|
|||
|
s_ip4 = dbus.Dictionary({'method': 'auto'})
|
|||
|
s_ip6 = dbus.Dictionary({'method': 'auto'})
|
|||
|
con = dbus.Dictionary({
|
|||
|
'connection': s_con,
|
|||
|
'802-11-wireless': s_wifi,
|
|||
|
'802-11-wireless-security': s_wsec,
|
|||
|
'802-1x': s_8021x,
|
|||
|
'ipv4': s_ip4,
|
|||
|
'ipv6': s_ip6
|
|||
|
})
|
|||
|
self.settings.AddConnection(con)
|
|||
|
|
|||
|
def add_connections(self, user_data: Type[InstallerData]):
|
|||
|
"""Delete and then add connections to the system"""
|
|||
|
self.__check_opts()
|
|||
|
self.user_data = user_data
|
|||
|
for ssid in Config.ssids:
|
|||
|
self.__delete_existing_connection(ssid)
|
|||
|
self.__add_connection(ssid)
|
|||
|
for ssid in Config.del_ssids:
|
|||
|
self.__delete_existing_connection(ssid)
|
|||
|
|
|||
|
|
|||
|
Messages.quit = "Really quit?"
|
|||
|
Messages.username_prompt = "enter your userid"
|
|||
|
Messages.enter_password = "enter password"
|
|||
|
Messages.enter_import_password = "enter your import password"
|
|||
|
Messages.credentials_prompt = "Please, enter your credentials:"
|
|||
|
Messages.incorrect_password = "incorrect password"
|
|||
|
Messages.repeat_password = "repeat your password"
|
|||
|
Messages.passwords_differ = "passwords do not match"
|
|||
|
Messages.installation_finished = "Installation successful"
|
|||
|
Messages.cat_dir_exisits = "Directory {} exists; some of its files may " \
|
|||
|
"be overwritten."
|
|||
|
Messages.cont = "Continue?"
|
|||
|
Messages.nm_not_supported = "This NetworkManager version is not " \
|
|||
|
"supported"
|
|||
|
Messages.cert_error = "Certificate file not found, looks like a CAT " \
|
|||
|
"error"
|
|||
|
Messages.unknown_version = "Unknown version"
|
|||
|
Messages.dbus_error = "DBus connection problem, a sudo might help"
|
|||
|
Messages.yes = "Y"
|
|||
|
Messages.no = "N"
|
|||
|
Messages.ok = "OK"
|
|||
|
Messages.p12_filter = "personal certificate file (p12 or pfx)"
|
|||
|
Messages.all_filter = "All files"
|
|||
|
Messages.p12_title = "personal certificate file (p12 or pfx)"
|
|||
|
Messages.save_wpa_conf = "DBus module not found - please install " \
|
|||
|
"dbus-python! NetworkManager configuration failed, but we may generate " \
|
|||
|
"a wpa_supplicant configuration file if you wish. Be warned that your " \
|
|||
|
"connection password will be saved in this file as clear text."
|
|||
|
Messages.save_wpa_confirm = "Write the file"
|
|||
|
Messages.wrongUsernameFormat = "Error: Your username must be of the " \
|
|||
|
"form 'xxx@institutionID' e.g. 'john@example.net'!"
|
|||
|
Messages.wrong_realm = "Error: your username must be in the form of " \
|
|||
|
"'xxx@{}'. Please enter the username in the correct format."
|
|||
|
Messages.wrong_realm_suffix = "Error: your username must be in the " \
|
|||
|
"form of 'xxx@institutionID' and end with '{}'. Please enter the " \
|
|||
|
"username in the correct format."
|
|||
|
Messages.user_cert_missing = "personal certificate file not found"
|
|||
|
Messages.cat_dir_exists = "Directory {} exists; some of its files may " \
|
|||
|
"be overwritten"
|
|||
|
Config.instname = "Université Paris Cité"
|
|||
|
Config.profilename = "u-paris.fr TTLS-PAP"
|
|||
|
Config.url = " \
|
|||
|
""https://u-paris.fr/wi-fi-universite-de-paris-avec-eduroam/"
|
|||
|
Config.email = "your local eduroam® support"
|
|||
|
Config.title = "eduroam CAT"
|
|||
|
Config.server_match = "u-paris.fr"
|
|||
|
Config.eap_outer = "TTLS"
|
|||
|
Config.eap_inner = "PAP"
|
|||
|
Config.init_info = "This installer has been prepared for {0}\n\nMore " \
|
|||
|
"information and comments:\n\nEMAIL: {1}\nWWW: {2}\n\nInstaller created " \
|
|||
|
"with software from the GEANT project."
|
|||
|
Config.init_confirmation = "This installer will only work properly if " \
|
|||
|
"you are a member of {0}."
|
|||
|
Config.user_realm = "u-paris.fr"
|
|||
|
Config.ssids = ['eduroam']
|
|||
|
Config.del_ssids = []
|
|||
|
Config.servers = ['DNS:rad01.u-paris.fr', 'DNS:rad02.u-paris.fr', 'DNS:radcp.u-paris.fr']
|
|||
|
Config.use_other_tls_id = False
|
|||
|
Config.anonymous_identity = "anonymous@u-paris.fr"
|
|||
|
Config.tou = ""
|
|||
|
Config.CA = """-----BEGIN CERTIFICATE-----
|
|||
|
MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
|
|||
|
MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
|
|||
|
GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
|
|||
|
YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
|
|||
|
MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
|
|||
|
BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
|
|||
|
GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
|||
|
ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
|
|||
|
BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
|
|||
|
3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
|
|||
|
YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
|
|||
|
rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
|
|||
|
ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
|
|||
|
oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
|
|||
|
MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
|
|||
|
QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
|
|||
|
b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
|
|||
|
AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
|
|||
|
GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
|
|||
|
Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
|
|||
|
G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
|
|||
|
l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
|
|||
|
smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
|
|||
|
-----END CERTIFICATE-----
|
|||
|
-----BEGIN CERTIFICATE-----
|
|||
|
MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7
|
|||
|
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
|
|||
|
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
|
|||
|
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4
|
|||
|
MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
|
|||
|
MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
|
|||
|
ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0
|
|||
|
aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI
|
|||
|
s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG
|
|||
|
vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ
|
|||
|
Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb
|
|||
|
IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0
|
|||
|
tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E
|
|||
|
xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV
|
|||
|
icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5
|
|||
|
D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ
|
|||
|
WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ
|
|||
|
5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG
|
|||
|
KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg
|
|||
|
EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID
|
|||
|
ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG
|
|||
|
BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t
|
|||
|
L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr
|
|||
|
BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA
|
|||
|
A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+
|
|||
|
rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+
|
|||
|
/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA
|
|||
|
CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F
|
|||
|
zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA
|
|||
|
vGp4z7h/jnZymQyd/teRCBaho1+V
|
|||
|
-----END CERTIFICATE-----
|
|||
|
-----BEGIN CERTIFICATE-----
|
|||
|
MIIG5TCCBM2gAwIBAgIRANpDvROb0li7TdYcrMTz2+AwDQYJKoZIhvcNAQEMBQAw
|
|||
|
gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK
|
|||
|
ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD
|
|||
|
VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIw
|
|||
|
MDIxODAwMDAwMFoXDTMzMDUwMTIzNTk1OVowRDELMAkGA1UEBhMCTkwxGTAXBgNV
|
|||
|
BAoTEEdFQU5UIFZlcmVuaWdpbmcxGjAYBgNVBAMTEUdFQU5UIE9WIFJTQSBDQSA0
|
|||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApYhi1aEiPsg9ZKRMAw9Q
|
|||
|
r8Mthsr6R20VSfFeh7TgwtLQi6RSRLOh4or4EMG/1th8lijv7xnBMVZkTysFiPmT
|
|||
|
PiLOfvz+QwO1NwjvgY+Jrs7fSoVA/TQkXzcxu4Tl3WHi+qJmKLJVu/JOuHud6mOp
|
|||
|
LWkIbhODSzOxANJ24IGPx9h4OXDyy6/342eE6UPXCtJ8AzeumTG6Dfv5KVx24lCF
|
|||
|
TGUzHUB+j+g0lSKg/Sf1OzgCajJV9enmZ/84ydh48wPp6vbWf1H0O3Rd3LhpMSVn
|
|||
|
TqFTLKZSbQeLcx/l9DOKZfBCC9ghWxsgTqW9gQ7v3T3aIfSaVC9rnwVxO0VjmDdP
|
|||
|
FNbdoxnh0zYwf45nV1QQgpRwZJ93yWedhp4ch1a6Ajwqs+wv4mZzmBSjovtV0mKw
|
|||
|
d+CQbSToalEUP4QeJq4Udz5WNmNMI4OYP6cgrnlJ50aa0DZPlJqrKQPGL69KQQz1
|
|||
|
2WgxvhCuVU70y6ZWAPopBa1ykbsttpLxADZre5cH573lIuLHdjx7NjpYIXRx2+QJ
|
|||
|
URnX2qx37eZIxYXz8ggM+wXH6RDbU3V2o5DP67hXPHSAbA+p0orjAocpk2osxHKo
|
|||
|
NSE3LCjNx8WVdxnXvuQ28tKdaK69knfm3bB7xpdfsNNTPH9ElcjscWZxpeZ5Iij8
|
|||
|
lyrCG1z0vSWtSBsgSnUyG/sCAwEAAaOCAYswggGHMB8GA1UdIwQYMBaAFFN5v1qq
|
|||
|
K0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBRvHTVJEGwy+lmgnryK6B+VvnF6DDAO
|
|||
|
BgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUEFjAUBggr
|
|||
|
BgEFBQcDAQYIKwYBBQUHAwIwOAYDVR0gBDEwLzAtBgRVHSAAMCUwIwYIKwYBBQUH
|
|||
|
AgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMFAGA1UdHwRJMEcwRaBDoEGGP2h0
|
|||
|
dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9u
|
|||
|
QXV0aG9yaXR5LmNybDB2BggrBgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6
|
|||
|
Ly9jcnQudXNlcnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAl
|
|||
|
BggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0B
|
|||
|
AQwFAAOCAgEAUtlC3e0xj/1BMfPhdQhUXeLjb0xp8UE28kzWE5xDzGKbfGgnrT2R
|
|||
|
lw5gLIx+/cNVrad//+MrpTppMlxq59AsXYZW3xRasrvkjGfNR3vt/1RAl8iI31lG
|
|||
|
hIg6dfIX5N4esLkrQeN8HiyHKH6khm4966IkVVtnxz5CgUPqEYn4eQ+4eeESrWBh
|
|||
|
AqXaiv7HRvpsdwLYekAhnrlGpioZ/CJIT2PTTxf+GHM6cuUnNqdUzfvrQgA8kt1/
|
|||
|
ASXx2od/M+c8nlJqrGz29lrJveJOSEMX0c/ts02WhsfMhkYa6XujUZLmvR1Eq08r
|
|||
|
48/EZ4l+t5L4wt0DV8VaPbsEBF1EOFpz/YS2H6mSwcFaNJbnYqqJHIvm3PLJHkFm
|
|||
|
EoLXRVrQXdCT+3wgBfgU6heCV5CYBz/YkrdWES7tiiT8sVUDqXmVlTsbiRNiyLs2
|
|||
|
bmEWWFUl76jViIJog5fongEqN3jLIGTG/mXrJT1UyymIcobnIGrbwwRVz/mpFQo0
|
|||
|
vBYIi1k2ThVh0Dx88BbF9YiP84dd8Fkn5wbE6FxXYJ287qfRTgmhePecPc73Yrzt
|
|||
|
apdRcsKVGkOpaTIJP/l+lAHRLZxk/dUtyN95G++bOSQqnOCpVPabUGl2E/OEyFrp
|
|||
|
Ipwgu2L/WJclvd6g+ZA/iWkLSMcpnFb+uX6QBqvD6+RNxul1FaB5iHY=
|
|||
|
-----END CERTIFICATE-----
|
|||
|
"""
|
|||
|
|
|||
|
|
|||
|
if __name__ == '__main__':
|
|||
|
run_installer()
|