From f8fbb2dce274ee92464e42cb134e5a8f5ee41e96 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Sun, 22 Jan 2023 10:36:29 -0800 Subject: [PATCH] use derivative of sshd_conf parser instead of paramiko --- CHANGELOG | 5 +- jc/parsers/paramiko/LICENSE | 504 -------------------- jc/parsers/paramiko/__init__.py | 0 jc/parsers/paramiko/config.py | 681 --------------------------- jc/parsers/paramiko/ssh_exception.py | 235 --------- jc/parsers/ssh_conf.py | 591 ++++++++++++++++++++++- jc/parsers/sshd_conf.py | 8 +- 7 files changed, 578 insertions(+), 1446 deletions(-) delete mode 100644 jc/parsers/paramiko/LICENSE delete mode 100644 jc/parsers/paramiko/__init__.py delete mode 100644 jc/parsers/paramiko/config.py delete mode 100644 jc/parsers/paramiko/ssh_exception.py diff --git a/CHANGELOG b/CHANGELOG index 82e2fb4c..e9152163 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,9 @@ jc changelog -20230121 v1.23.0 -- Add ssh_conf file parser +20230122 v1.23.0 +- Add `ssh` configuration file parser - Add input slicing +- Fix `acpi` command parser for "will never fully discharge" battery state 20230111 v1.22.5 - Add TOML file parser diff --git a/jc/parsers/paramiko/LICENSE b/jc/parsers/paramiko/LICENSE deleted file mode 100644 index d12bef0c..00000000 --- a/jc/parsers/paramiko/LICENSE +++ /dev/null @@ -1,504 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! - - diff --git a/jc/parsers/paramiko/__init__.py b/jc/parsers/paramiko/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/jc/parsers/paramiko/config.py b/jc/parsers/paramiko/config.py deleted file mode 100644 index e5e8f582..00000000 --- a/jc/parsers/paramiko/config.py +++ /dev/null @@ -1,681 +0,0 @@ -# Copyright (C) 2006-2007 Robey Pointer -# Copyright (C) 2012 Olle Lundberg -# -# This file is part of paramiko. -# -# Paramiko is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Configuration file (aka ``ssh_config``) support. -""" - -# search jc_change for jc fixes - -import fnmatch -import getpass -import os -import re -import shlex -import socket -from hashlib import sha1 -from io import StringIO -from functools import partial - -invoke, invoke_import_error = None, None -try: - import invoke -except ImportError as e: - invoke_import_error = e - -from .ssh_exception import CouldNotCanonicalize, ConfigParseError - - -SSH_PORT = 22 - - -class SSHConfig: - """ - Representation of config information as stored in the format used by - OpenSSH. Queries can be made via `lookup`. The format is described in - OpenSSH's ``ssh_config`` man page. This class is provided primarily as a - convenience to posix users (since the OpenSSH format is a de-facto - standard on posix) but should work fine on Windows too. - - .. versionadded:: 1.6 - """ - - SETTINGS_REGEX = re.compile(r"(\w+)(?:\s*=\s*|\s+)(.+)") - - # TODO: do a full scan of ssh.c & friends to make sure we're fully - # compatible across the board, e.g. OpenSSH 8.1 added %n to ProxyCommand. - TOKENS_BY_CONFIG_KEY = { - "controlpath": ["%C", "%h", "%l", "%L", "%n", "%p", "%r", "%u"], - "hostname": ["%h"], - "identityfile": ["%C", "~", "%d", "%h", "%l", "%u", "%r"], - "proxycommand": ["~", "%h", "%p", "%r"], - "proxyjump": ["%h", "%p", "%r"], - # Doesn't seem worth making this 'special' for now, it will fit well - # enough (no actual match-exec config key to be confused with). - "match-exec": ["%C", "%d", "%h", "%L", "%l", "%n", "%p", "%r", "%u"], - } - - def __init__(self): - """ - Create a new OpenSSH config object. - - Note: the newer alternate constructors `from_path`, `from_file` and - `from_text` are simpler to use, as they parse on instantiation. For - example, instead of:: - - config = SSHConfig() - config.parse(open("some-path.config") - - you could:: - - config = SSHConfig.from_file(open("some-path.config")) - # Or more directly: - config = SSHConfig.from_path("some-path.config") - # Or if you have arbitrary ssh_config text from some other source: - config = SSHConfig.from_text("Host foo\\n\\tUser bar") - """ - self._config = [] - - @classmethod - def from_text(cls, text): - """ - Create a new, parsed `SSHConfig` from ``text`` string. - - .. versionadded:: 2.7 - """ - return cls.from_file(StringIO(text)) - - @classmethod - def from_path(cls, path): - """ - Create a new, parsed `SSHConfig` from the file found at ``path``. - - .. versionadded:: 2.7 - """ - with open(path) as flo: - return cls.from_file(flo) - - @classmethod - def from_file(cls, flo): - """ - Create a new, parsed `SSHConfig` from file-like object ``flo``. - - .. versionadded:: 2.7 - """ - obj = cls() - obj.parse(flo) - return obj - - def parse(self, file_obj): - """ - Read an OpenSSH config from the given file object. - - :param file_obj: a file-like object to read the config file from - """ - # Start out w/ implicit/anonymous global host-like block to hold - # anything not contained by an explicit one. - context = {"host": ["*"], "config": {}} - for line in file_obj: - # Strip any leading or trailing whitespace from the line. - # Refer to https://github.com/paramiko/paramiko/issues/499 - line = line.strip() - # Skip blanks, comments - if not line or line.startswith("#"): - continue - - # Parse line into key, value - match = re.match(self.SETTINGS_REGEX, line) - if not match: - raise ConfigParseError("Unparsable line {}".format(line)) - key = match.group(1).lower() - value = match.group(2) - - # Host keyword triggers switch to new block/context - if key in ("host", "match"): - self._config.append(context) - context = {"config": {}} - if key == "host": - # TODO 4.0: make these real objects or at least name this - # "hosts" to acknowledge it's an iterable. (Doing so prior - # to 3.0, despite it being a private API, feels bad - - # surely such an old codebase has folks actually relying on - # these keys.) - context["host"] = self._get_hosts(value) - else: - context["matches"] = self._get_matches(value) - # Special-case for noop ProxyCommands - elif key == "proxycommand" and value.lower() == "none": - # Store 'none' as None - not as a string implying that the - # proxycommand is the literal shell command "none"! - context["config"][key] = None - # All other keywords get stored, directly or via append - else: - if value.startswith('"') and value.endswith('"'): - value = value[1:-1] - - # identityfile, localforward, remoteforward keys are special - # cases, since they are allowed to be specified multiple times - # and they should be tried in order of specification. - if key in ["identityfile", "localforward", "remoteforward"]: - if key in context["config"]: - context["config"][key].append(value) - else: - context["config"][key] = [value] - elif key not in context["config"]: - context["config"][key] = value - # Store last 'open' block and we're done - self._config.append(context) - - def lookup(self, hostname): - """ - Return a dict (`SSHConfigDict`) of config options for a given hostname. - - The host-matching rules of OpenSSH's ``ssh_config`` man page are used: - For each parameter, the first obtained value will be used. The - configuration files contain sections separated by ``Host`` and/or - ``Match`` specifications, and that section is only applied for hosts - which match the given patterns or keywords - - Since the first obtained value for each parameter is used, more host- - specific declarations should be given near the beginning of the file, - and general defaults at the end. - - The keys in the returned dict are all normalized to lowercase (look for - ``"port"``, not ``"Port"``. The values are processed according to the - rules for substitution variable expansion in ``ssh_config``. - - Finally, please see the docs for `SSHConfigDict` for deeper info on - features such as optional type conversion methods, e.g.:: - - conf = my_config.lookup('myhost') - assert conf['passwordauthentication'] == 'yes' - assert conf.as_bool('passwordauthentication') is True - - .. note:: - If there is no explicitly configured ``HostName`` value, it will be - set to the being-looked-up hostname, which is as close as we can - get to OpenSSH's behavior around that particular option. - - :param str hostname: the hostname to lookup - - .. versionchanged:: 2.5 - Returns `SSHConfigDict` objects instead of dict literals. - .. versionchanged:: 2.7 - Added canonicalization support. - .. versionchanged:: 2.7 - Added ``Match`` support. - """ - # First pass - options = self._lookup(hostname=hostname) - # Inject HostName if it was not set (this used to be done incidentally - # during tokenization, for some reason). - if "hostname" not in options: - options["hostname"] = hostname - # Handle canonicalization - canon = options.get("canonicalizehostname", None) in ("yes", "always") - maxdots = int(options.get("canonicalizemaxdots", 1)) - if canon and hostname.count(".") <= maxdots: - # NOTE: OpenSSH manpage does not explicitly state this, but its - # implementation for CanonicalDomains is 'split on any whitespace'. - domains = options["canonicaldomains"].split() - hostname = self.canonicalize(hostname, options, domains) - # Overwrite HostName again here (this is also what OpenSSH does) - options["hostname"] = hostname - options = self._lookup(hostname, options, canonical=True) - return options - - def _lookup(self, hostname, options=None, canonical=False): - # Init - if options is None: - options = SSHConfigDict() - # Iterate all stanzas, applying any that match, in turn (so that things - # like Match can reference currently understood state) - for context in self._config: - if not ( - self._pattern_matches(context.get("host", []), hostname) - or self._does_match( - context.get("matches", []), hostname, canonical, options - ) - ): - continue - for key, value in context["config"].items(): - if key not in options: - # Create a copy of the original value, - # else it will reference the original list - # in self._config and update that value too - # when the extend() is being called. - options[key] = value[:] if value is not None else value - elif key == "identityfile": - options[key].extend( - x for x in value if x not in options[key] - ) - # Expand variables in resulting values (besides 'Match exec' which was - # already handled above) - options = self._expand_variables(options, hostname) - return options - - def canonicalize(self, hostname, options, domains): - """ - Return canonicalized version of ``hostname``. - - :param str hostname: Target hostname. - :param options: An `SSHConfigDict` from a previous lookup pass. - :param domains: List of domains (e.g. ``["paramiko.org"]``). - - :returns: A canonicalized hostname if one was found, else ``None``. - - .. versionadded:: 2.7 - """ - found = False - for domain in domains: - candidate = "{}.{}".format(hostname, domain) - family_specific = _addressfamily_host_lookup(candidate, options) - if family_specific is not None: - # TODO: would we want to dig deeper into other results? e.g. to - # find something that satisfies PermittedCNAMEs when that is - # implemented? - found = family_specific[0] - else: - # TODO: what does ssh use here and is there a reason to use - # that instead of gethostbyname? - try: - found = socket.gethostbyname(candidate) - except socket.gaierror: - pass - if found: - # TODO: follow CNAME (implied by found != candidate?) if - # CanonicalizePermittedCNAMEs allows it - return candidate - # If we got here, it means canonicalization failed. - # When CanonicalizeFallbackLocal is undefined or 'yes', we just spit - # back the original hostname. - if options.get("canonicalizefallbacklocal", "yes") == "yes": - return hostname - # And here, we failed AND fallback was set to a non-yes value, so we - # need to get mad. - raise CouldNotCanonicalize(hostname) - - def get_hostnames(self): - """ - Return the set of literal hostnames defined in the SSH config (both - explicit hostnames and wildcard entries). - """ - hosts = set() - for entry in self._config: - hosts.update(entry["host"]) - return hosts - - def _pattern_matches(self, patterns, target): - # Convenience auto-splitter if not already a list - if hasattr(patterns, "split"): - patterns = patterns.split(",") - match = False - for pattern in patterns: - # Short-circuit if target matches a negated pattern - if pattern.startswith("!") and fnmatch.fnmatch( - target, pattern[1:] - ): - return False - # Flag a match, but continue (in case of later negation) if regular - # match occurs - elif fnmatch.fnmatch(target, pattern): - match = True - return match - - def _does_match(self, match_list, target_hostname, canonical, options): - matched = [] - candidates = match_list[:] - local_username = getpass.getuser() - while candidates: - candidate = candidates.pop(0) - passed = None - # Obtain latest host/user value every loop, so later Match may - # reference values assigned within a prior Match. - configured_host = options.get("hostname", None) - configured_user = options.get("user", None) - type_, param = candidate["type"], candidate["param"] - # Canonical is a hard pass/fail based on whether this is a - # canonicalized re-lookup. - if type_ == "canonical": - if self._should_fail(canonical, candidate): - return False - # The parse step ensures we only see this by itself or after - # canonical, so it's also an easy hard pass. (No negation here as - # that would be uh, pretty weird?) - elif type_ == "all": - return True - # From here, we are testing various non-hard criteria, - # short-circuiting only on fail - elif type_ == "host": - hostval = configured_host or target_hostname - passed = self._pattern_matches(param, hostval) - elif type_ == "originalhost": - passed = self._pattern_matches(param, target_hostname) - elif type_ == "user": - user = configured_user or local_username - passed = self._pattern_matches(param, user) - elif type_ == "localuser": - passed = self._pattern_matches(param, local_username) - elif type_ == "exec": - exec_cmd = self._tokenize( - options, target_hostname, "match-exec", param - ) - # This is the laziest spot in which we can get mad about an - # inability to import Invoke. - if invoke is None: - raise invoke_import_error - # Like OpenSSH, we 'redirect' stdout but let stderr bubble up - passed = invoke.run(exec_cmd, hide="stdout", warn=True).ok - # Tackle any 'passed, but was negated' results from above - if passed is not None and self._should_fail(passed, candidate): - return False - # Made it all the way here? Everything matched! - matched.append(candidate) - # Did anything match? (To be treated as bool, usually.) - return matched - - def _should_fail(self, would_pass, candidate): - return would_pass if candidate["negate"] else not would_pass - - def _tokenize(self, config, target_hostname, key, value): - """ - Tokenize a string based on current config/hostname data. - - :param config: Current config data. - :param target_hostname: Original target connection hostname. - :param key: Config key being tokenized (used to filter token list). - :param value: Config value being tokenized. - - :returns: The tokenized version of the input ``value`` string. - """ - allowed_tokens = self._allowed_tokens(key) - # Short-circuit if no tokenization possible - if not allowed_tokens: - return value - # Obtain potentially configured hostname, for use with %h. - # Special-case where we are tokenizing the hostname itself, to avoid - # replacing %h with a %h-bearing value, etc. - configured_hostname = target_hostname - if key != "hostname": - configured_hostname = config.get("hostname", configured_hostname) - # Ditto the rest of the source values - if "port" in config: - port = config["port"] - else: - port = SSH_PORT - user = getpass.getuser() - if "user" in config: - remoteuser = config["user"] - else: - remoteuser = user - local_hostname = socket.gethostname().split(".")[0] - local_fqdn = LazyFqdn(config, local_hostname) - homedir = os.path.expanduser("~") - tohash = local_hostname + target_hostname + repr(port) + remoteuser - # The actual tokens! - replacements = { - # TODO: %%??? - # "%C": sha1(tohash.encode()).hexdigest(), # jc_change - # "%d": homedir, # jc_change - "%h": configured_hostname, - # TODO: %i? - # "%L": local_hostname, # jc_change - # "%l": local_fqdn, # jc_change - # also this is pseudo buggy when not in Match exec mode so document - # that. also WHY is that the case?? don't we do all of this late? - "%n": target_hostname, - "%p": port, - "%r": remoteuser, - # TODO: %T? don't believe this is possible however - # "%u": user, # jc_change - # "~": homedir, # jc_change - } - # Do the thing with the stuff - tokenized = value - for find, replace in replacements.items(): - if find not in allowed_tokens: - continue - tokenized = tokenized.replace(find, str(replace)) - # TODO: log? eg that value -> tokenized - return tokenized - - def _allowed_tokens(self, key): - """ - Given config ``key``, return list of token strings to tokenize. - - .. note:: - This feels like it wants to eventually go away, but is used to - preserve as-strict-as-possible compatibility with OpenSSH, which - for whatever reason only applies some tokens to some config keys. - """ - return self.TOKENS_BY_CONFIG_KEY.get(key, []) - - def _expand_variables(self, config, target_hostname): - """ - Return a dict of config options with expanded substitutions - for a given original & current target hostname. - - Please refer to :doc:`/api/config` for details. - - :param dict config: the currently parsed config - :param str hostname: the hostname whose config is being looked up - """ - for k in config: - if config[k] is None: - continue - tokenizer = partial(self._tokenize, config, target_hostname, k) - if isinstance(config[k], list): - for i, value in enumerate(config[k]): - config[k][i] = tokenizer(value) - else: - config[k] = tokenizer(config[k]) - return config - - def _get_hosts(self, host): - """ - Return a list of host_names from host value. - """ - try: - return shlex.split(host) - except ValueError: - raise ConfigParseError("Unparsable host {}".format(host)) - - def _get_matches(self, match): - """ - Parse a specific Match config line into a list-of-dicts for its values. - - Performs some parse-time validation as well. - """ - matches = [] - tokens = shlex.split(match) - while tokens: - match = {"type": None, "param": None, "negate": False} - type_ = tokens.pop(0) - # Handle per-keyword negation - if type_.startswith("!"): - match["negate"] = True - type_ = type_[1:] - match["type"] = type_ - # all/canonical have no params (everything else does) - if type_ in ("all", "canonical"): - matches.append(match) - continue - if not tokens: - raise ConfigParseError( - "Missing parameter to Match '{}' keyword".format(type_) - ) - match["param"] = tokens.pop(0) - matches.append(match) - # Perform some (easier to do now than in the middle) validation that is - # better handled here than at lookup time. - keywords = [x["type"] for x in matches] - if "all" in keywords: - allowable = ("all", "canonical") - ok, bad = ( - list(filter(lambda x: x in allowable, keywords)), - list(filter(lambda x: x not in allowable, keywords)), - ) - err = None - if any(bad): - err = "Match does not allow 'all' mixed with anything but 'canonical'" # noqa - elif "canonical" in ok and ok.index("canonical") > ok.index("all"): - err = "Match does not allow 'all' before 'canonical'" - if err is not None: - raise ConfigParseError(err) - return matches - - -def _addressfamily_host_lookup(hostname, options): - """ - Try looking up ``hostname`` in an IPv4 or IPv6 specific manner. - - This is an odd duck due to needing use in two divergent use cases. It looks - up ``AddressFamily`` in ``options`` and if it is ``inet`` or ``inet6``, - this function uses `socket.getaddrinfo` to perform a family-specific - lookup, returning the result if successful. - - In any other situation -- lookup failure, or ``AddressFamily`` being - unspecified or ``any`` -- ``None`` is returned instead and the caller is - expected to do something situation-appropriate like calling - `socket.gethostbyname`. - - :param str hostname: Hostname to look up. - :param options: `SSHConfigDict` instance w/ parsed options. - :returns: ``getaddrinfo``-style tuples, or ``None``, depending. - """ - address_family = options.get("addressfamily", "any").lower() - if address_family == "any": - return - try: - family = socket.AF_INET6 - if address_family == "inet": - family = socket.AF_INET - return socket.getaddrinfo( - hostname, - None, - family, - socket.SOCK_DGRAM, - socket.IPPROTO_IP, - socket.AI_CANONNAME, - ) - except socket.gaierror: - pass - - -class LazyFqdn: - """ - Returns the host's fqdn on request as string. - """ - - def __init__(self, config, host=None): - self.fqdn = None - self.config = config - self.host = host - - def __str__(self): - if self.fqdn is None: - # - # If the SSH config contains AddressFamily, use that when - # determining the local host's FQDN. Using socket.getfqdn() from - # the standard library is the most general solution, but can - # result in noticeable delays on some platforms when IPv6 is - # misconfigured or not available, as it calls getaddrinfo with no - # address family specified, so both IPv4 and IPv6 are checked. - # - - # Handle specific option - fqdn = None - results = _addressfamily_host_lookup(self.host, self.config) - if results is not None: - for res in results: - af, socktype, proto, canonname, sa = res - if canonname and "." in canonname: - fqdn = canonname - break - # Handle 'any' / unspecified / lookup failure - if fqdn is None: - fqdn = socket.getfqdn() - # Cache - self.fqdn = fqdn - return self.fqdn - - -class SSHConfigDict(dict): - """ - A dictionary wrapper/subclass for per-host configuration structures. - - This class introduces some usage niceties for consumers of `SSHConfig`, - specifically around the issue of variable type conversions: normal value - access yields strings, but there are now methods such as `as_bool` and - `as_int` that yield casted values instead. - - For example, given the following ``ssh_config`` file snippet:: - - Host foo.example.com - PasswordAuthentication no - Compression yes - ServerAliveInterval 60 - - the following code highlights how you can access the raw strings as well as - usefully Python type-casted versions (recalling that keys are all - normalized to lowercase first):: - - my_config = SSHConfig() - my_config.parse(open('~/.ssh/config')) - conf = my_config.lookup('foo.example.com') - - assert conf['passwordauthentication'] == 'no' - assert conf.as_bool('passwordauthentication') is False - assert conf['compression'] == 'yes' - assert conf.as_bool('compression') is True - assert conf['serveraliveinterval'] == '60' - assert conf.as_int('serveraliveinterval') == 60 - - .. versionadded:: 2.5 - """ - - def as_bool(self, key): - """ - Express given key's value as a boolean type. - - Typically, this is used for ``ssh_config``'s pseudo-boolean values - which are either ``"yes"`` or ``"no"``. In such cases, ``"yes"`` yields - ``True`` and any other value becomes ``False``. - - .. note:: - If (for whatever reason) the stored value is already boolean in - nature, it's simply returned. - - .. versionadded:: 2.5 - """ - val = self[key] - if isinstance(val, bool): - return val - return val.lower() == "yes" - - def as_int(self, key): - """ - Express given key's value as an integer, if possible. - - This method will raise ``ValueError`` or similar if the value is not - int-appropriate, same as the builtin `int` type. - - .. versionadded:: 2.5 - """ - return int(self[key]) diff --git a/jc/parsers/paramiko/ssh_exception.py b/jc/parsers/paramiko/ssh_exception.py deleted file mode 100644 index 9b1b44c3..00000000 --- a/jc/parsers/paramiko/ssh_exception.py +++ /dev/null @@ -1,235 +0,0 @@ -# Copyright (C) 2003-2007 Robey Pointer -# -# This file is part of paramiko. -# -# Paramiko is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -import socket - - -class SSHException(Exception): - """ - Exception raised by failures in SSH2 protocol negotiation or logic errors. - """ - - pass - - -class AuthenticationException(SSHException): - """ - Exception raised when authentication failed for some reason. It may be - possible to retry with different credentials. (Other classes specify more - specific reasons.) - - .. versionadded:: 1.6 - """ - - pass - - -class PasswordRequiredException(AuthenticationException): - """ - Exception raised when a password is needed to unlock a private key file. - """ - - pass - - -class BadAuthenticationType(AuthenticationException): - """ - Exception raised when an authentication type (like password) is used, but - the server isn't allowing that type. (It may only allow public-key, for - example.) - - .. versionadded:: 1.1 - """ - - allowed_types = [] - - # TODO 4.0: remove explanation kwarg - def __init__(self, explanation, types): - # TODO 4.0: remove this supercall unless it's actually required for - # pickling (after fixing pickling) - AuthenticationException.__init__(self, explanation, types) - self.explanation = explanation - self.allowed_types = types - - def __str__(self): - return "{}; allowed types: {!r}".format( - self.explanation, self.allowed_types - ) - - -class PartialAuthentication(AuthenticationException): - """ - An internal exception thrown in the case of partial authentication. - """ - - allowed_types = [] - - def __init__(self, types): - AuthenticationException.__init__(self, types) - self.allowed_types = types - - def __str__(self): - return "Partial authentication; allowed types: {!r}".format( - self.allowed_types - ) - - -class ChannelException(SSHException): - """ - Exception raised when an attempt to open a new `.Channel` fails. - - :param int code: the error code returned by the server - - .. versionadded:: 1.6 - """ - - def __init__(self, code, text): - SSHException.__init__(self, code, text) - self.code = code - self.text = text - - def __str__(self): - return "ChannelException({!r}, {!r})".format(self.code, self.text) - - -class BadHostKeyException(SSHException): - """ - The host key given by the SSH server did not match what we were expecting. - - :param str hostname: the hostname of the SSH server - :param PKey got_key: the host key presented by the server - :param PKey expected_key: the host key expected - - .. versionadded:: 1.6 - """ - - def __init__(self, hostname, got_key, expected_key): - SSHException.__init__(self, hostname, got_key, expected_key) - self.hostname = hostname - self.key = got_key - self.expected_key = expected_key - - def __str__(self): - msg = "Host key for server '{}' does not match: got '{}', expected '{}'" # noqa - return msg.format( - self.hostname, - self.key.get_base64(), - self.expected_key.get_base64(), - ) - - -class IncompatiblePeer(SSHException): - """ - A disagreement arose regarding an algorithm required for key exchange. - - .. versionadded:: 2.9 - """ - - # TODO 4.0: consider making this annotate w/ 1..N 'missing' algorithms, - # either just the first one that would halt kex, or even updating the - # Transport logic so we record /all/ that /could/ halt kex. - # TODO: update docstrings where this may end up raised so they are more - # specific. - pass - - -class ProxyCommandFailure(SSHException): - """ - The "ProxyCommand" found in the .ssh/config file returned an error. - - :param str command: The command line that is generating this exception. - :param str error: The error captured from the proxy command output. - """ - - def __init__(self, command, error): - SSHException.__init__(self, command, error) - self.command = command - self.error = error - - def __str__(self): - return 'ProxyCommand("{}") returned nonzero exit status: {}'.format( - self.command, self.error - ) - - -class NoValidConnectionsError(socket.error): - """ - Multiple connection attempts were made and no families succeeded. - - This exception class wraps multiple "real" underlying connection errors, - all of which represent failed connection attempts. Because these errors are - not guaranteed to all be of the same error type (i.e. different errno, - `socket.error` subclass, message, etc) we expose a single unified error - message and a ``None`` errno so that instances of this class match most - normal handling of `socket.error` objects. - - To see the wrapped exception objects, access the ``errors`` attribute. - ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', - 22)``) and whose values are the exception encountered trying to connect to - that address. - - It is implied/assumed that all the errors given to a single instance of - this class are from connecting to the same hostname + port (and thus that - the differences are in the resolution of the hostname - e.g. IPv4 vs v6). - - .. versionadded:: 1.16 - """ - - def __init__(self, errors): - """ - :param dict errors: - The errors dict to store, as described by class docstring. - """ - addrs = sorted(errors.keys()) - body = ", ".join([x[0] for x in addrs[:-1]]) - tail = addrs[-1][0] - if body: - msg = "Unable to connect to port {0} on {1} or {2}" - else: - msg = "Unable to connect to port {0} on {2}" - super().__init__( - None, msg.format(addrs[0][1], body, tail) # stand-in for errno - ) - self.errors = errors - - def __reduce__(self): - return (self.__class__, (self.errors,)) - - -class CouldNotCanonicalize(SSHException): - """ - Raised when hostname canonicalization fails & fallback is disabled. - - .. versionadded:: 2.7 - """ - - pass - - -class ConfigParseError(SSHException): - """ - A fatal error was encountered trying to parse SSH config data. - - Typically this means a config file violated the ``ssh_config`` - specification in a manner that requires exiting immediately, such as not - matching ``key = value`` syntax or misusing certain ``Match`` keywords. - - .. versionadded:: 2.7 - """ - - pass diff --git a/jc/parsers/ssh_conf.py b/jc/parsers/ssh_conf.py index 80b5c727..1e9883f5 100644 --- a/jc/parsers/ssh_conf.py +++ b/jc/parsers/ssh_conf.py @@ -1,47 +1,493 @@ -"""jc - JSON Convert ssh configuration file parser +"""jc - JSON Convert ssh configuration file and `ssh -G` command output parser + +This parser will work with `ssh` configuration files or the output of +`ssh -G`. Any `Match` blocks in the `ssh` configuration file will be +ignored. Usage (cli): - $ cat ssh_conf | jc --ssh-conf + $ ssh -G hostname | jc --ssh-conf + +or + + $ jc ssh -G hostname + +or + + $ cat ~/.ssh/config | jc --ssh-conf Usage (module): import jc - result = jc.parse('ssh_conf', ssh_conf_file_output) + result = jc.parse('ssh_conf', ssh_conf_output) Schema: [ { - "ssh_conf": string, - "bar": boolean, - "baz": integer + "host": string, + "addkeystoagent": string, + "addressfamily": string, + "batchmode": string, + "bindaddress": string, + "bindinterface": string, + "canonicaldomains": [ + string + ], + "canonicalizefallbacklocal": string, + "canonicalizehostname": string, + "canonicalizemaxdots": integer, + "canonicalizepermittedcnames": [ + string + ], + "casignaturealgorithms": [ + string + ], + "certificatefile": [ + string + ], + "checkhostip": string, + "ciphers": [ + string + ], + "clearallforwardings": string, + "compression": string, + "connectionattempts": integer, + "connecttimeout": integer, + "controlmaster": string, + "controlpath": string, + "controlpersist": string, + "dynamicforward": string, + "enableescapecommandline": string, + "enablesshkeysign": string, + "escapechar": string, + "exitonforwardfailure": string, + "fingerprinthash": string, + "forkafterauthentication": string, + "forwardagent": string, + "forwardx11": string, + "forwardx11timeout": integer, + "forwardx11trusted": string, + "gatewayports": string, + "globalknownhostsfile": [ + string + ], + "gssapiauthentication": string, + "gssapidelegatecredentials": string, + "hashknownhosts": string, + "hostbasedacceptedalgorithms": [ + string + ], + "hostbasedauthentication": string, + "hostkeyalgorithms": [ + string + ], + "hostkeyalias": string, + "hostname": string, + "identitiesonly": string, + "identityagent": string, + "identityfile": [ + string + ], + "ignoreunknown": string, + "include": [ + string + ], + "ipqos": [ + string + ], + "kbdinteractiveauthentication": string, + "kbdinteractivedevices": [ + string + ], + "kexalgorithms": [ + string + ], + "kexalgorithms_strategy": string, + "knownhostscommand": string, + "localcommand": string, + "localforward": [ + string + ], + "loglevel": string, + "logverbose": [ + string + ], + "macs": [ + string + ], + "macs_strategy": string, + "nohostauthenticationforlocalhost": string, + "numberofpasswordprompts": integer, + "passwordauthentication": string, + "permitlocalcommand": string, + "permitremoteopen": [ + string + ], + "pkcs11provider": string, + "port": integer, + "preferredauthentications": [ + string + ], + "protocol": integer, + "proxycommand": string, + "proxyjump": [ + string + ], + "proxyusefdpass": string, + "pubkeyacceptedalgorithms": [ + string + ], + "pubkeyacceptedalgorithms_strategy": string, + "pubkeyauthentication": string, + "rekeylimit": string, + "remotecommand": string, + "remoteforward": string, + "requesttty": string, + "requiredrsasize": integer, + "revokedhostkeys": string, + "securitykeyprovider": string, + "sendenv": [ + string + ], + "serveralivecountmax": integer, + "serveraliveinterval": integer, + "sessiontype": string, + "setenv": [ + string + ], + "stdinnull": string, + "streamlocalbindmask": string, + "streamlocalbindunlink": string, + "stricthostkeychecking": string, + "syslogfacility": string, + "tcpkeepalive": string, + "tunnel": string, + "tunneldevice": string, + "updatehostkeys": string, + "user": string, + "userknownhostsfile": [ + string + ], + "verifyhostkeydns": string, + "visualhostkey": string, + "xauthlocation": string } ] Examples: - $ cat ssh_conf | jc --ssh-conf -p - [] + $ ssh -G - | jc --ssh-conf -p + [ + { + "user": "foo", + "hostname": "-", + "port": 22, + "addressfamily": "any", + "batchmode": "no", + "canonicalizefallbacklocal": "yes", + "canonicalizehostname": "false", + "checkhostip": "no", + "compression": "no", + "controlmaster": "false", + "enablesshkeysign": "no", + "clearallforwardings": "no", + "exitonforwardfailure": "no", + "fingerprinthash": "SHA256", + "forwardx11": "no", + "forwardx11trusted": "no", + "gatewayports": "no", + "gssapiauthentication": "no", + "gssapidelegatecredentials": "no", + "hashknownhosts": "no", + "hostbasedauthentication": "no", + "identitiesonly": "no", + "kbdinteractiveauthentication": "yes", + "nohostauthenticationforlocalhost": "no", + "passwordauthentication": "yes", + "permitlocalcommand": "no", + "proxyusefdpass": "no", + "pubkeyauthentication": "true", + "requesttty": "auto", + "sessiontype": "default", + "stdinnull": "no", + "forkafterauthentication": "no", + "streamlocalbindunlink": "no", + "stricthostkeychecking": "ask", + "tcpkeepalive": "yes", + "tunnel": "false", + "verifyhostkeydns": "false", + "visualhostkey": "no", + "updatehostkeys": "true", + "applemultipath": "no", + "canonicalizemaxdots": 1, + "connectionattempts": 1, + "forwardx11timeout": 1200, + "numberofpasswordprompts": 3, + "serveralivecountmax": 3, + "serveraliveinterval": 0, + "ciphers": [ + "chacha20-poly1305@openssh.com", + "aes128-ctr", + "aes192-ctr", + "aes256-ctr", + "aes128-gcm@openssh.com", + "aes256-gcm@openssh.com" + ], + "hostkeyalgorithms": [ + "ssh-ed25519-cert-v01@openssh.com", + "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521-cert-v01@openssh.com", + "rsa-sha2-512-cert-v01@openssh.com", + "rsa-sha2-256-cert-v01@openssh.com", + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "rsa-sha2-512", + "rsa-sha2-256" + ], + "hostbasedacceptedalgorithms": [ + "ssh-ed25519-cert-v01@openssh.com", + "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521-cert-v01@openssh.com", + "rsa-sha2-512-cert-v01@openssh.com", + "rsa-sha2-256-cert-v01@openssh.com", + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "rsa-sha2-512", + "rsa-sha2-256" + ], + "kexalgorithms": [ + "sntrup761x25519-sha512@openssh.com", + "curve25519-sha256", + "curve25519-sha256@libssh.org", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group16-sha512", + "diffie-hellman-group18-sha512", + "diffie-hellman-group14-sha256" + ], + "casignaturealgorithms": [ + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "rsa-sha2-512", + "rsa-sha2-256" + ], + "loglevel": "INFO", + "macs": [ + "umac-64-etm@openssh.com", + "umac-128-etm@openssh.com", + "hmac-sha2-256-etm@openssh.com", + "hmac-sha2-512-etm@openssh.com", + "hmac-sha1-etm@openssh.com", + "umac-64@openssh.com", + "umac-128@openssh.com", + "hmac-sha2-256", + "hmac-sha2-512", + "hmac-sha1" + ], + "securitykeyprovider": "$SSH_SK_PROVIDER", + "pubkeyacceptedalgorithms": [ + "ssh-ed25519-cert-v01@openssh.com", + "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521-cert-v01@openssh.com", + "rsa-sha2-512-cert-v01@openssh.com", + "rsa-sha2-256-cert-v01@openssh.com", + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "rsa-sha2-512", + "rsa-sha2-256" + ], + "xauthlocation": "/usr/X11R6/bin/xauth", + "identityfile": [ + "~/.ssh/id_rsa", + "~/.ssh/id_ecdsa", + "~/.ssh/id_ecdsa_sk", + "~/.ssh/id_ed25519", + "~/.ssh/id_ed25519_sk", + "~/.ssh/id_xmss", + "~/.ssh/id_dsa" + ], + "canonicaldomains": [ + "none" + ], + "globalknownhostsfile": [ + "/etc/ssh/ssh_known_hosts", + "/etc/ssh/ssh_known_hosts2" + ], + "userknownhostsfile": [ + "/Users/foo/.ssh/known_hosts", + "/Users/foo/.ssh/known_hosts2" + ], + "sendenv": [ + "LANG", + "LC_*" + ], + "logverbose": [ + "none" + ], + "permitremoteopen": [ + "any" + ], + "addkeystoagent": "false", + "forwardagent": "no", + "connecttimeout": null, + "tunneldevice": "any:any", + "canonicalizepermittedcnames": [ + "none" + ], + "controlpersist": "no", + "escapechar": "~", + "ipqos": [ + "af21", + "cs1" + ], + "rekeylimit": "0 0", + "streamlocalbindmask": "0177", + "syslogfacility": "USER" + } + ] - $ cat ssh_conf | jc --ssh-conf -p -r - [] + $ cat ~/.ssh/config | jc --ssh-conf -p + [ + { + "host": "server1", + "hostname": "server1.cyberciti.biz", + "user": "nixcraft", + "port": 4242, + "identityfile": [ + "/nfs/shared/users/nixcraft/keys/server1/id_rsa" + ] + }, + { + "host": "nas01", + "hostname": "192.168.1.100", + "user": "root", + "identityfile": [ + "~/.ssh/nas01.key" + ] + }, + { + "host": "aws.apache", + "hostname": "1.2.3.4", + "user": "wwwdata", + "identityfile": [ + "~/.ssh/aws.apache.key" + ] + }, + { + "host": "uk.gw.lan uk.lan", + "hostname": "192.168.0.251", + "user": "nixcraft", + "proxycommand": "ssh nixcraft@gateway.uk.cyberciti.biz nc %h %p 2> /dev/null" + }, + { + "host": "proxyus", + "hostname": "vps1.cyberciti.biz", + "user": "breakfree", + "identityfile": [ + "~/.ssh/vps1.cyberciti.biz.key" + ], + "localforward": [ + "3128 127.0.0.1:3128" + ] + }, + { + "host": "*", + "forwardagent": "no", + "forwardx11": "no", + "forwardx11trusted": "yes", + "user": "nixcraft", + "port": 22, + "protocol": 2, + "serveraliveinterval": 60, + "serveralivecountmax": 30 + } + ] + + $ cat ~/.ssh/config | jc --ssh-conf -p -r + [ + { + "host": "server1", + "hostname": "server1.cyberciti.biz", + "user": "nixcraft", + "port": "4242", + "identityfile": [ + "/nfs/shared/users/nixcraft/keys/server1/id_rsa" + ] + }, + { + "host": "nas01", + "hostname": "192.168.1.100", + "user": "root", + "identityfile": [ + "~/.ssh/nas01.key" + ] + }, + { + "host": "aws.apache", + "hostname": "1.2.3.4", + "user": "wwwdata", + "identityfile": [ + "~/.ssh/aws.apache.key" + ] + }, + { + "host": "uk.gw.lan uk.lan", + "hostname": "192.168.0.251", + "user": "nixcraft", + "proxycommand": "ssh nixcraft@gateway.uk.cyberciti.biz nc %h %p 2> /dev/null" + }, + { + "host": "proxyus", + "hostname": "vps1.cyberciti.biz", + "user": "breakfree", + "identityfile": [ + "~/.ssh/vps1.cyberciti.biz.key" + ], + "localforward": [ + "3128 127.0.0.1:3128" + ] + }, + { + "host": "*", + "forwardagent": "no", + "forwardx11": "no", + "forwardx11trusted": "yes", + "user": "nixcraft", + "port": "22", + "protocol": "2", + "serveraliveinterval": "60", + "serveralivecountmax": "30" + } + ] """ -from typing import List, Dict +from typing import Set, List, Dict from jc.jc_types import JSONDictType import jc.utils -from .paramiko.config import SSHConfig as sshconfig class info(): """Provides parser metadata (version, author, etc.)""" version = '1.0' - description = 'ssh config file parser' + description = 'ssh config file and `ssh -G` command parser' author = 'Kelly Brazil' author_email = 'kellyjonbrazil@gmail.com' - details = 'Using Paramiko library at https://github.com/paramiko/paramiko.' - compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd'] - tags = ['file'] + compatible = ['linux', 'darwin', 'freebsd'] + magic_commands = ['ssh -G'] + tags = ['command', 'file'] __version__ = info.version @@ -59,6 +505,47 @@ def _process(proc_data: List[JSONDictType]) -> List[JSONDictType]: List of Dictionaries. Structured to conform to the schema. """ + split_fields_space: Set[str] = { + 'canonicaldomains', 'globalknownhostsfile', 'include', 'ipqos', + 'permitremoteopen', 'sendenv', 'setenv', 'userknownhostsfile' + } + + split_fields_comma: Set[str] = { + 'canonicalizepermittedcnames', 'casignaturealgorithms', 'ciphers', + 'hostbasedacceptedalgorithms', 'hostkeyalgorithms', + 'kbdinteractivedevices', 'kexalgorithms', 'logverbose', 'macs', + 'preferredauthentications', 'proxyjump', 'pubkeyacceptedalgorithms' + } + + int_list: Set[str] = { + 'canonicalizemaxdots', 'connectionattempts', 'connecttimeout', + 'forwardx11timeout', 'numberofpasswordprompts', 'port', 'protocol', + 'requiredrsasize', 'serveralivecountmax', 'serveraliveinterval' + } + + for host in proc_data: + dict_copy = host.copy() + for key, val in dict_copy.items(): + # these are list values + if key == 'sendenv' or key == 'setenv' or key == 'include': + new_list: List[str] = [] + for item in val: + new_list.extend(item.split()) + host[key] = new_list + continue + + if key in split_fields_space: + host[key] = val.split() + continue + + if key in split_fields_comma: + host[key] = val.split(',') + continue + + for key, val in host.items(): + if key in int_list: + host[key] = jc.utils.convert_to_int(val) + return proc_data @@ -83,13 +570,73 @@ def parse( jc.utils.compatibility(__name__, info.compatible, quiet) jc.utils.input_type_check(data) - raw_output: List[Dict] = [] + raw_output: List = [] + host: Dict = {} + + multi_fields: Set[str] = { + 'certificatefile', 'identityfile', 'include', 'localforward', + 'sendenv', 'setenv' + } + + modified_fields: Set[str] = { + 'casignaturealgorithms', 'ciphers', 'hostbasedacceptedalgorithms', + 'HostKeyAlgorithms', 'kexalgorithms', 'macs', + 'pubkeyacceptedalgorithms' + } + + modifiers: Set[str] = {'+', '-', '^'} + + match_block_found = False if jc.utils.has_data(data): - myconfig = sshconfig.from_text(data) - hostnames = sorted(myconfig.get_hostnames()) - raw_output = [myconfig.lookup(x) for x in hostnames] - for host, obj in zip(hostnames, raw_output.copy()): - obj.update({'host': host}) + + for line in filter(None, data.splitlines()): + # skip any lines with only whitespace + if not line.strip(): + continue + + # support configuration file by skipping commented lines + if line.strip().startswith('#'): + continue + + if line.strip().startswith('Host '): + if host: + raw_output.append(host) + host = {'host': line.split()[1]} + + # support configuration file by ignoring all lines between + # Match xxx and Match any + if line.strip().startswith('Match all'): + match_block_found = False + continue + + if line.strip().startswith('Match'): + match_block_found = True + continue + + if match_block_found: + continue + + key, val = line.split(maxsplit=1) + + # support configuration file by converting to lower case + key = key.lower() + + if key in multi_fields: + if key not in host: + host[key] = [] + host[key].append(val) + continue + + if key in modified_fields and val[0] in modifiers: + host[key] = val[1:] + host[key + '_strategy'] = val[0] + continue + + host[key] = val + continue + + if host: + raw_output.append(host) return raw_output if raw else _process(raw_output) diff --git a/jc/parsers/sshd_conf.py b/jc/parsers/sshd_conf.py index b6a19e64..62782ade 100644 --- a/jc/parsers/sshd_conf.py +++ b/jc/parsers/sshd_conf.py @@ -483,13 +483,13 @@ import jc.utils class info(): """Provides parser metadata (version, author, etc.)""" - version = '1.0' + version = '1.1' description = 'sshd config file and `sshd -T` command parser' author = 'Kelly Brazil' author_email = 'kellyjonbrazil@gmail.com' compatible = ['linux', 'darwin', 'freebsd'] magic_commands = ['sshd -T'] - tags = ['file'] + tags = ['command', 'file'] __version__ = info.version @@ -622,6 +622,10 @@ def parse( if jc.utils.has_data(data): for line in filter(None, data.splitlines()): + # skip any lines with only whitespace + if not line.strip(): + continue + # support configuration file by skipping commented lines if line.strip().startswith('#'): continue