Module ssh2net.ssh_config
ssh2net.ssh_config
Expand source code
"""ssh2net.ssh_config"""
import os
from pathlib import Path
import re
import shlex
class SSH2NetSSHConfig:
def __init__(self, ssh_config_file=""):
"""
Initialize SSH2NetSSHConfig Object
Parse OpenSSH config file
Try to load the following data for all entries in config file:
Host
HostName
Port
User
AddressFamily
BindAddress
ConnectTimeout
IdentitiesOnly
IdentityFile
KbdInteractiveAuthentication
PasswordAuthentication
PreferredAuthentications
Args:
ssh_config_file: string path to ssh configuration file to use if not provided
will try to use users ssh config file in ~/.ssh/config first, then will try
/etc/ssh/config_file
Returns:
N/A # noqa
Raises:
N/A # noqa
"""
self.ssh_config_file = self._select_config_file(ssh_config_file)
if self.ssh_config_file:
with open(self.ssh_config_file, "r") as f:
self.ssh_config_file = f.read()
self.hosts = self._parse()
if not self.hosts:
self.hosts = None
else:
self.hosts = None
def __str__(self):
"""
Magic str method for SSH2NetSSHConfig class
Args:
N/A # noqa
Returns:
N/A # noqa
Raises:
N/A # noqa
"""
return "SSH2NetSSHConfig Object"
def __repr__(self):
"""
Magic repr method for SSH2NetSSHConfig class
Args:
N/A # noqa
Returns:
repr: repr for class object
Raises:
N/A # noqa
"""
class_dict = self.__dict__.copy()
del class_dict["ssh_config_file"]
return f"SSH2NetSSHConfig {class_dict}"
def __bool__(self):
"""
Magic bool method; return True if ssh_config_file
Args:
N/A # noqa
Returns:
bool: True/False if ssh_config_file
Raises:
N/A # noqa
"""
if self.ssh_config_file:
return True
return False
@staticmethod
def _select_config_file(ssh_config_file):
"""
Select ssh configuration file
Args:
ssh_config_file: string representation of ssh config file to try to use
Returns:
ssh_config_file: Pathlib path object or None
Raises:
N/A # noqa
"""
if Path(ssh_config_file).is_file():
return Path(ssh_config_file)
if Path(os.path.expanduser("~/.ssh/config")).is_file():
return Path(os.path.expanduser("~/.ssh/config"))
if Path("/etc/ssh/ssh_config").is_file():
return Path("/etc/ssh/ssh_config")
return None
@staticmethod
def _strip_comments(line):
"""
Strip out comments from ssh config file lines
Args:
line: to strip comments from
Returns:
line: rejoined ssh config file line after stripping comments
Raises:
N/A # noqa
"""
line = " ".join(shlex.split(line, comments=True))
return line
def _parse(self):
"""
Parse SSH configuration file
Args:
N/A # noqa
Returns:
discovered_hosts: dict of host objects discovered in ssh config file
Raises:
N/A # noqa
"""
# uncomment next line and handle global patterns (stuff before hosts) at some point
# global_config_pattern = re.compile(r"^.*?\b(?=host)", flags=re.I | re.S)
# use word boundaries with a positive lookahead to get everything between the word host
# need to do this as whitespace/formatting is not really a thing in ssh_config file
# match host\s to ensure we don't pick up hostname and split things there accidentally
host_pattern = re.compile(r"\bhost.*?\b(?=host\s|\s+$)", flags=re.I | re.S)
host_entries = re.findall(host_pattern, self.ssh_config_file)
discovered_hosts = {}
if not host_entries:
return discovered_hosts
# do we need to add whitespace between match and end of line to ensure we match correctly?
hosts_pattern = re.compile(r"^\s*host[\s=]+(.*)$", flags=re.I | re.M)
hostname_pattern = re.compile(r"^\s*hostname[\s=]+([\w.-]*)$", flags=re.I | re.M)
port_pattern = re.compile(r"^\s*port[\s=]+([\d]*)$", flags=re.I | re.M)
user_pattern = re.compile(r"^\s*user[\s=]+([\w]*)$", flags=re.I | re.M)
# address_family_pattern = None
# bind_address_pattern = None
# connect_timeout_pattern = None
identities_only_pattern = re.compile(
r"^\s*identitiesonly[\s=]+(yes|no)$", flags=re.I | re.M
)
identity_file_pattern = re.compile(
r"^\s*identityfile[\s=]+([\w.\/\@~-]*)$", flags=re.I | re.M
)
# keyboard_interactive_pattern = None
# password_authentication_pattern = None
# preferred_authentication_pattern = None
for host_entry in host_entries:
host = Host()
host_line = re.search(hosts_pattern, host_entry)
if host_line:
host.hosts = self._strip_comments(host_line.groups()[0])
hostname = re.search(hostname_pattern, host_entry)
if hostname:
host.hostname = self._strip_comments(hostname.groups()[0])
port = re.search(port_pattern, host_entry)
if port:
host.port = self._strip_comments(port.groups()[0])
user = re.search(user_pattern, host_entry)
if user:
host.user = self._strip_comments(user.groups()[0])
# address_family = re.search(user_pattern, host_entry[0])
# bind_address = re.search(user_pattern, host_entry[0])
# connect_timeout = re.search(user_pattern, host_entry[0])
identities_only = re.search(identities_only_pattern, host_entry)
if identities_only:
host.identities_only = self._strip_comments(identities_only.groups()[0])
identity_file = re.search(identity_file_pattern, host_entry)
if identity_file:
host.identity_file = self._strip_comments(identity_file.groups()[0])
# keyboard_interactive = re.search(user_pattern, host_entry[0])
# password_authentication = re.search(user_pattern, host_entry[0])
# preferred_authentication = re.search(user_pattern, host_entry[0])
discovered_hosts[host.hosts] = host
return discovered_hosts
def _lookup_fuzzy_match(self, host):
"""
Look up fuzzy matched hosts
Get the best match ssh config Host entry for a given host; this allows for using
the splat and question-mark operators in ssh config file
Args:
host: host to lookup in discovered_hosts dict
Returns:
N/A # noqa
Raises:
N/A # noqa
"""
possible_matches = []
for host_entry in self.hosts.keys():
host_list = host_entry.split()
for host_pattern in host_list:
# replace periods with literal period
# replace asterisk (match 0 or more things) with appropriate regex
# replace question mark (match one thing) with appropriate regex
host_pattern = (
host_pattern.replace(".", r"\.").replace("*", r"(.*)").replace("?", r"(.)")
)
# compile with case insensitive
host_pattern = re.compile(host_pattern, flags=re.I)
result = re.search(host_pattern, host)
# if we get a result, append it and the original pattern to the possible matches
if result:
possible_matches.append((result, host_entry))
# initialize a None best match
best_match = None
for match in possible_matches:
if best_match is None:
best_match = match
# count how many chars were replaced to get regex to work
chars_replaced = 0
for start_char, end_char in match[0].regs[1:]:
chars_replaced += end_char - start_char
# count how many chars were replaced to get regex to work on best match
best_match_chars_replaced = 0
for start_char, end_char in best_match[0].regs[1:]:
best_match_chars_replaced += end_char - start_char
# if match replaced less chars than "best_match" we have a new best match
if chars_replaced < best_match_chars_replaced:
best_match = match
return self.hosts[best_match[1]]
def lookup(self, host):
"""
Lookup a given host
Args:
host: host to lookup in discovered_hosts dict
Returns:
N/A # noqa
Raises:
N/A # noqa
"""
# return exact 1:1 match if exists
if host in self.hosts.keys():
return self.hosts[host]
# return match if given host is an exact match for a host entry
for host_entry in self.hosts.keys():
host_list = host_entry.split()
if host in host_list:
return self.hosts[host_entry]
# otherwise need to select the most correct host entry
return self._lookup_fuzzy_match(host)
class Host:
def __init__(self):
"""
Initialize SSH2Net Host Object
Create a Host object based on ssh config file information
"""
self.hosts = None
self.hostname = None
self.port = None
self.user = None
self.address_family = None
self.bind_address = None
self.connect_timeout = None
self.identities_only = None
self.identity_file = None
self.keyboard_interactive = None
self.password_authentication = None
self.preferred_authentication = None
def __str__(self):
"""
Magic str method for HostEntry class
Args:
N/A # noqa
Returns:
N/A # noqa
Raises:
N/A # noqa
"""
return f"Host: {self.hosts}"
def __repr__(self):
"""
Magic repr method for HostEntry class
Args:
N/A # noqa
Returns:
repr: repr for class object
Raises:
N/A # noqa
"""
class_dict = self.__dict__.copy()
return f"HostEntry {class_dict}"
Classes
class Host
-
Initialize SSH2Net Host Object
Create a Host object based on ssh config file information
Expand source code
class Host: def __init__(self): """ Initialize SSH2Net Host Object Create a Host object based on ssh config file information """ self.hosts = None self.hostname = None self.port = None self.user = None self.address_family = None self.bind_address = None self.connect_timeout = None self.identities_only = None self.identity_file = None self.keyboard_interactive = None self.password_authentication = None self.preferred_authentication = None def __str__(self): """ Magic str method for HostEntry class Args: N/A # noqa Returns: N/A # noqa Raises: N/A # noqa """ return f"Host: {self.hosts}" def __repr__(self): """ Magic repr method for HostEntry class Args: N/A # noqa Returns: repr: repr for class object Raises: N/A # noqa """ class_dict = self.__dict__.copy() return f"HostEntry {class_dict}"
class SSH2NetSSHConfig (ssh_config_file='')
-
Initialize SSH2NetSSHConfig Object
Parse OpenSSH config file
Try to load the following data for all entries in config file: Host HostName Port User AddressFamily BindAddress ConnectTimeout IdentitiesOnly IdentityFile KbdInteractiveAuthentication PasswordAuthentication PreferredAuthentications
Args
ssh_config_file
- string path to ssh configuration file to use if not provided will try to use users ssh config file in ~/.ssh/config first, then will try /etc/ssh/config_file
Returns
N
/A
#noqa
Raises
N
/A
#noqa
Expand source code
class SSH2NetSSHConfig: def __init__(self, ssh_config_file=""): """ Initialize SSH2NetSSHConfig Object Parse OpenSSH config file Try to load the following data for all entries in config file: Host HostName Port User AddressFamily BindAddress ConnectTimeout IdentitiesOnly IdentityFile KbdInteractiveAuthentication PasswordAuthentication PreferredAuthentications Args: ssh_config_file: string path to ssh configuration file to use if not provided will try to use users ssh config file in ~/.ssh/config first, then will try /etc/ssh/config_file Returns: N/A # noqa Raises: N/A # noqa """ self.ssh_config_file = self._select_config_file(ssh_config_file) if self.ssh_config_file: with open(self.ssh_config_file, "r") as f: self.ssh_config_file = f.read() self.hosts = self._parse() if not self.hosts: self.hosts = None else: self.hosts = None def __str__(self): """ Magic str method for SSH2NetSSHConfig class Args: N/A # noqa Returns: N/A # noqa Raises: N/A # noqa """ return "SSH2NetSSHConfig Object" def __repr__(self): """ Magic repr method for SSH2NetSSHConfig class Args: N/A # noqa Returns: repr: repr for class object Raises: N/A # noqa """ class_dict = self.__dict__.copy() del class_dict["ssh_config_file"] return f"SSH2NetSSHConfig {class_dict}" def __bool__(self): """ Magic bool method; return True if ssh_config_file Args: N/A # noqa Returns: bool: True/False if ssh_config_file Raises: N/A # noqa """ if self.ssh_config_file: return True return False @staticmethod def _select_config_file(ssh_config_file): """ Select ssh configuration file Args: ssh_config_file: string representation of ssh config file to try to use Returns: ssh_config_file: Pathlib path object or None Raises: N/A # noqa """ if Path(ssh_config_file).is_file(): return Path(ssh_config_file) if Path(os.path.expanduser("~/.ssh/config")).is_file(): return Path(os.path.expanduser("~/.ssh/config")) if Path("/etc/ssh/ssh_config").is_file(): return Path("/etc/ssh/ssh_config") return None @staticmethod def _strip_comments(line): """ Strip out comments from ssh config file lines Args: line: to strip comments from Returns: line: rejoined ssh config file line after stripping comments Raises: N/A # noqa """ line = " ".join(shlex.split(line, comments=True)) return line def _parse(self): """ Parse SSH configuration file Args: N/A # noqa Returns: discovered_hosts: dict of host objects discovered in ssh config file Raises: N/A # noqa """ # uncomment next line and handle global patterns (stuff before hosts) at some point # global_config_pattern = re.compile(r"^.*?\b(?=host)", flags=re.I | re.S) # use word boundaries with a positive lookahead to get everything between the word host # need to do this as whitespace/formatting is not really a thing in ssh_config file # match host\s to ensure we don't pick up hostname and split things there accidentally host_pattern = re.compile(r"\bhost.*?\b(?=host\s|\s+$)", flags=re.I | re.S) host_entries = re.findall(host_pattern, self.ssh_config_file) discovered_hosts = {} if not host_entries: return discovered_hosts # do we need to add whitespace between match and end of line to ensure we match correctly? hosts_pattern = re.compile(r"^\s*host[\s=]+(.*)$", flags=re.I | re.M) hostname_pattern = re.compile(r"^\s*hostname[\s=]+([\w.-]*)$", flags=re.I | re.M) port_pattern = re.compile(r"^\s*port[\s=]+([\d]*)$", flags=re.I | re.M) user_pattern = re.compile(r"^\s*user[\s=]+([\w]*)$", flags=re.I | re.M) # address_family_pattern = None # bind_address_pattern = None # connect_timeout_pattern = None identities_only_pattern = re.compile( r"^\s*identitiesonly[\s=]+(yes|no)$", flags=re.I | re.M ) identity_file_pattern = re.compile( r"^\s*identityfile[\s=]+([\w.\/\@~-]*)$", flags=re.I | re.M ) # keyboard_interactive_pattern = None # password_authentication_pattern = None # preferred_authentication_pattern = None for host_entry in host_entries: host = Host() host_line = re.search(hosts_pattern, host_entry) if host_line: host.hosts = self._strip_comments(host_line.groups()[0]) hostname = re.search(hostname_pattern, host_entry) if hostname: host.hostname = self._strip_comments(hostname.groups()[0]) port = re.search(port_pattern, host_entry) if port: host.port = self._strip_comments(port.groups()[0]) user = re.search(user_pattern, host_entry) if user: host.user = self._strip_comments(user.groups()[0]) # address_family = re.search(user_pattern, host_entry[0]) # bind_address = re.search(user_pattern, host_entry[0]) # connect_timeout = re.search(user_pattern, host_entry[0]) identities_only = re.search(identities_only_pattern, host_entry) if identities_only: host.identities_only = self._strip_comments(identities_only.groups()[0]) identity_file = re.search(identity_file_pattern, host_entry) if identity_file: host.identity_file = self._strip_comments(identity_file.groups()[0]) # keyboard_interactive = re.search(user_pattern, host_entry[0]) # password_authentication = re.search(user_pattern, host_entry[0]) # preferred_authentication = re.search(user_pattern, host_entry[0]) discovered_hosts[host.hosts] = host return discovered_hosts def _lookup_fuzzy_match(self, host): """ Look up fuzzy matched hosts Get the best match ssh config Host entry for a given host; this allows for using the splat and question-mark operators in ssh config file Args: host: host to lookup in discovered_hosts dict Returns: N/A # noqa Raises: N/A # noqa """ possible_matches = [] for host_entry in self.hosts.keys(): host_list = host_entry.split() for host_pattern in host_list: # replace periods with literal period # replace asterisk (match 0 or more things) with appropriate regex # replace question mark (match one thing) with appropriate regex host_pattern = ( host_pattern.replace(".", r"\.").replace("*", r"(.*)").replace("?", r"(.)") ) # compile with case insensitive host_pattern = re.compile(host_pattern, flags=re.I) result = re.search(host_pattern, host) # if we get a result, append it and the original pattern to the possible matches if result: possible_matches.append((result, host_entry)) # initialize a None best match best_match = None for match in possible_matches: if best_match is None: best_match = match # count how many chars were replaced to get regex to work chars_replaced = 0 for start_char, end_char in match[0].regs[1:]: chars_replaced += end_char - start_char # count how many chars were replaced to get regex to work on best match best_match_chars_replaced = 0 for start_char, end_char in best_match[0].regs[1:]: best_match_chars_replaced += end_char - start_char # if match replaced less chars than "best_match" we have a new best match if chars_replaced < best_match_chars_replaced: best_match = match return self.hosts[best_match[1]] def lookup(self, host): """ Lookup a given host Args: host: host to lookup in discovered_hosts dict Returns: N/A # noqa Raises: N/A # noqa """ # return exact 1:1 match if exists if host in self.hosts.keys(): return self.hosts[host] # return match if given host is an exact match for a host entry for host_entry in self.hosts.keys(): host_list = host_entry.split() if host in host_list: return self.hosts[host_entry] # otherwise need to select the most correct host entry return self._lookup_fuzzy_match(host)
Methods
def lookup(self, host)
-
Lookup a given host
Args
host
- host to lookup in discovered_hosts dict
Returns
N
/A
#noqa
Raises
N
/A
#noqa
Expand source code
def lookup(self, host): """ Lookup a given host Args: host: host to lookup in discovered_hosts dict Returns: N/A # noqa Raises: N/A # noqa """ # return exact 1:1 match if exists if host in self.hosts.keys(): return self.hosts[host] # return match if given host is an exact match for a host entry for host_entry in self.hosts.keys(): host_list = host_entry.split() if host in host_list: return self.hosts[host_entry] # otherwise need to select the most correct host entry return self._lookup_fuzzy_match(host)