[project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--base-0]
initial import (automatically generated log message)
This commit is contained in:
commit
51607386c7
|
@ -0,0 +1,504 @@
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
59 Temple Place, Suite 330, Boston, MA 02111-1307 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.
|
||||||
|
|
||||||
|
<one line to give the library's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1990
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
That's all there is to it!
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
README
|
||||||
|
ber.py
|
||||||
|
channel.py
|
||||||
|
dsskey.py
|
||||||
|
kex_gex.py
|
||||||
|
kex_group1.py
|
||||||
|
message.py
|
||||||
|
rsakey.py
|
||||||
|
secsh.py
|
||||||
|
setup.py
|
||||||
|
transport.py
|
||||||
|
util.py
|
||||||
|
demo.py
|
|
@ -0,0 +1,22 @@
|
||||||
|
# releases:
|
||||||
|
# aerodactyl (13sep03)
|
||||||
|
# bulbasaur
|
||||||
|
# charmander
|
||||||
|
|
||||||
|
RELEASE=bulbasaur
|
||||||
|
|
||||||
|
release:
|
||||||
|
mkdir ../secsh-$(RELEASE)
|
||||||
|
cp README ../secsh-$(RELEASE)
|
||||||
|
cp *.py ../secsh-$(RELEASE)
|
||||||
|
cd .. && zip -r secsh-$(RELEASE).zip secsh-$(RELEASE)
|
||||||
|
echo rm -rf ../secsh-$(RELEASE)
|
||||||
|
|
||||||
|
py:
|
||||||
|
python ./setup.py sdist --formats=zip
|
||||||
|
|
||||||
|
# places where the version number is stored:
|
||||||
|
#
|
||||||
|
# setup.py
|
||||||
|
# secsh.py
|
||||||
|
# README
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
+-------------------+ +-----------------+
|
||||||
|
(Socket)InputStream ---> | secsh transport | <===> | secsh channel |
|
||||||
|
(Socket)OutputStream --> | (auth, pipe) | N | (buffer) |
|
||||||
|
+-------------------+ +-----------------+
|
||||||
|
@ feeder thread | |
|
||||||
|
- read InputStream | +-> InputStream
|
||||||
|
- feed into channel +---> OutputStream
|
||||||
|
buffers
|
||||||
|
|
||||||
|
SIS <-- @ --> (parse, find chan) --> secsh chan: buffer <-- SSHInputStream
|
||||||
|
SSHOutputStream --> secsh chan --> secsh transport --> SOS [no thread]
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
secsh 0.1
|
||||||
|
"bulbasaur" release, 18 sep 2003
|
||||||
|
|
||||||
|
(c) 2003 Robey Pointer <robey@lag.net>
|
||||||
|
|
||||||
|
http://www.lag.net/~robey/secsh/
|
||||||
|
|
||||||
|
|
||||||
|
*** WHAT
|
||||||
|
|
||||||
|
secsh is a module for python 2.3 that implements the SSH2 protocol for secure
|
||||||
|
(encrypted and authenticated) connections to remote machines. unlike SSL (aka
|
||||||
|
TLS), SSH2 protocol does not require heirarchical certificates signed by a
|
||||||
|
powerful central authority. you may know SSH2 as the protocol that replaced
|
||||||
|
telnet and rsh for secure access to remote shells, but the protocol also
|
||||||
|
includes the ability to open arbitrary channels to remote services across the
|
||||||
|
encrypted tunnel (this is how sftp works, for example).
|
||||||
|
|
||||||
|
the module works by taking a socket-like object that you pass in, negotiating
|
||||||
|
with the remote server, authenticating (using a password or a given private
|
||||||
|
key), and opening flow-controled "channels" to the server, which are returned
|
||||||
|
as socket-like objects. you are responsible for verifying that the server's
|
||||||
|
host key is the one you expected to see, and you have control over which kinds
|
||||||
|
of encryption or hashing you prefer (if you care), but all of the heavy lifting
|
||||||
|
is done by the secsh module.
|
||||||
|
|
||||||
|
it is written entirely in python (no C or platform-dependent code) and is
|
||||||
|
released under the GNU LGPL (lesser GPL).
|
||||||
|
|
||||||
|
|
||||||
|
*** REQUIREMENTS
|
||||||
|
|
||||||
|
python 2.3 <http://www.python.org/>
|
||||||
|
pyCrypto <http://www.amk.ca/python/code/crypto.html>
|
||||||
|
|
||||||
|
|
||||||
|
*** PORTABILITY
|
||||||
|
|
||||||
|
i code and test this library on Linux and MacOS X. for that reason, i'm
|
||||||
|
pretty sure that it works for all posix platforms, including MacOS. i also
|
||||||
|
think it will work on Windows, though i've never tested it there. if you
|
||||||
|
run into Windows problems, send me a patch: portability is important to me.
|
||||||
|
|
||||||
|
the Channel object supports a "fileno()" call so that it can be passed into
|
||||||
|
select or poll, for polling on posix. once you call "fileno()" on a Channel,
|
||||||
|
it changes behavior in some fundamental ways, and these ways require posix.
|
||||||
|
so don't call "fileno()" on a Channel on Windows. (the problem is that pipes
|
||||||
|
are used to simulate an open socket, so that the ssh "socket" has an OS-level
|
||||||
|
file descriptor. i haven't figured out how to make pipes on Windows go into
|
||||||
|
non-blocking mode yet. [if you don't understand this last sentence, don't
|
||||||
|
be afraid. the point is to make the API simple enough that you don't HAVE to
|
||||||
|
know these screwy steps. i just don't understand windows enough.])
|
||||||
|
|
||||||
|
|
||||||
|
*** DEMO
|
||||||
|
|
||||||
|
the demo app (demo.py) is a raw implementation of the normal 'ssh' CLI tool.
|
||||||
|
while the secsh library should work on all platforms, the demo app will only
|
||||||
|
run on posix, because it uses select.
|
||||||
|
|
||||||
|
you can run demo.py with no arguments, or you can give a hostname (or
|
||||||
|
username@hostname) on the command line. if you don't, it'll prompt you for
|
||||||
|
a hostname and username. if you have an ".ssh/" folder, it will try to read
|
||||||
|
the host keys from there, though it's easily confused. you can choose to
|
||||||
|
authenticate with a password, or with an RSA or DSS key, but it can only
|
||||||
|
read your private key file(s) if they're not password-protected.
|
||||||
|
|
||||||
|
the demo app leaves a logfile called "demo.log" so you can see what secsh
|
||||||
|
logs as it works. but the most interesting part is probably the code itself,
|
||||||
|
which hopefully demonstrates how you can use the secsh library.
|
||||||
|
|
||||||
|
|
||||||
|
*** USE
|
||||||
|
|
||||||
|
(this section could probably be improved a lot.)
|
||||||
|
|
||||||
|
first, create a Transport by passing in an existing socket (connected to the
|
||||||
|
desired server). call "start_client(event)", passing in an event which will
|
||||||
|
be triggered when the negotiation is finished (either successfully or not).
|
||||||
|
the event is required because each new Transport creates a new worker thread
|
||||||
|
to handle incoming data asynchronously.
|
||||||
|
|
||||||
|
after the event triggers, use "is_active()" to determine if the Transport was
|
||||||
|
successfully connected. if so, you should check the server's host key to make
|
||||||
|
sure it's what you expected. don't worry, i don't mean "check" in any crypto
|
||||||
|
sense: i mean compare the key, byte for byte, with what you saw last time, to
|
||||||
|
make sure it's the same key. Transport will handle verifying that the server's
|
||||||
|
key works.
|
||||||
|
|
||||||
|
next, authenticate, using either "auth_key" or "auth_password". in the future,
|
||||||
|
this API may change to accomodate servers that require both forms of auth.
|
||||||
|
pass another event in so you can determine when the authentication dance is
|
||||||
|
over. if it was successful, "is_authenticated()" will return true.
|
||||||
|
|
||||||
|
once authentication is successful, the Transport is ready to use. call
|
||||||
|
"open_channel" or "open_session" to create new channels over the Transport
|
||||||
|
(SSH2 supports many different channels over the same connection). these calls
|
||||||
|
block until they succeed or fail, and return a Channel object on success, or
|
||||||
|
None on failure. Channel objects can be treated as "socket-like objects": they
|
||||||
|
implement:
|
||||||
|
recv(nbytes)
|
||||||
|
send(data)
|
||||||
|
settimeout(timeout_in_seconds)
|
||||||
|
close()
|
||||||
|
fileno() [* see note below]
|
||||||
|
because SSH2 has a windowing kind of flow control, if you stop reading data
|
||||||
|
from a Channel and its buffer fills up, the server will be unable to send you
|
||||||
|
any more data until you read some of it. (this won't affect other channels on
|
||||||
|
the Transport, though.)
|
||||||
|
|
||||||
|
* NOTE that if you use "fileno()", the behavior of the Channel will change
|
||||||
|
slightly, underneath. this shouldn't be noticable outside the library, but
|
||||||
|
this alternate implementation will not work on non-posix systems. so don't
|
||||||
|
try calling "fileno()" on Windows! this has the side effect that you can't
|
||||||
|
pass a Channel to "select" or "poll" on Windows (which should be fine, since
|
||||||
|
those calls don't exist on Windows). calling "fileno()" creates an OS-level
|
||||||
|
pipe and generates a real file descriptor which can be used for polling, BUT
|
||||||
|
should not be used for reading data from the channel: use "recv" instead.
|
||||||
|
|
||||||
|
because each Transport has a worker thread running in the background, you
|
||||||
|
must call "close()" on the Transport to kill this thread. on many platforms,
|
||||||
|
the python interpreter will refuse to exit cleanly if any of these threads
|
||||||
|
are still running (and you'll have to kill -9 from another shell window).
|
||||||
|
|
||||||
|
|
||||||
|
*** CHANGELOG
|
||||||
|
|
||||||
|
2003-08-24:
|
||||||
|
* implemented the other hashes: all 4 from the draft are working now
|
||||||
|
* added 'aes128-cbc' and '3des-cbc' cipher support
|
||||||
|
* fixed channel eof/close semantics
|
||||||
|
2003-09-12: version "aerodactyl"
|
||||||
|
* implemented group-exchange kex ("kex-gex")
|
||||||
|
* implemented RSA/DSA private key auth
|
||||||
|
2003-09-13:
|
||||||
|
* fixed inflate_long and deflate_long to handle negatives, even though
|
||||||
|
they're never used in the current ssh protocol
|
||||||
|
2003-09-14:
|
||||||
|
* fixed session_id handling: re-keying works now
|
||||||
|
* added the ability for a Channel to have a fileno() for select/poll
|
||||||
|
purposes, although this will cause worse window performance if the
|
||||||
|
client app isn't careful
|
||||||
|
2003-09-16: version "bulbasaur"
|
||||||
|
* fixed pipe (fileno) method to be nonblocking and it seems to work now
|
||||||
|
* fixed silly bug that caused large blocks to be truncated
|
||||||
|
2003-10-08:
|
||||||
|
* patch to fix Channel.invoke_subsystem and add Channel.exec_command
|
||||||
|
[vaclav dvorak]
|
||||||
|
* patch to add Channel.sendall [vaclav dvorak]
|
||||||
|
* patch to add Channel.shutdown [vaclav dvorak]
|
||||||
|
* patch to add Channel.makefile and a ChannelFile class which emulates
|
||||||
|
a python file object [vaclav dvorak]
|
||||||
|
2003-10-26:
|
||||||
|
* thread creation no longer happens during construction -- use the new
|
||||||
|
method "start_client(event)" to get things rolling
|
||||||
|
* re-keying now takes place after 1GB of data or 1 billion packets
|
||||||
|
(these limits can be easily changed per-session if needed)
|
||||||
|
|
||||||
|
|
||||||
|
*** MISSING LINKS
|
||||||
|
|
||||||
|
* ctr forms of ciphers are missing (blowfish-ctr, aes128-ctr, aes256-ctr)
|
||||||
|
* can't handle password-protected private key files
|
||||||
|
* multi-part auth not supported (ie, need username AND pk)
|
||||||
|
* should have a simple synchronous method that handles all auth & events,
|
||||||
|
by pre-seeding the password or key info, and the expected key
|
|
@ -0,0 +1,224 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
from transport import BaseTransport
|
||||||
|
from transport import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, \
|
||||||
|
MSG_USERAUTH_SUCCESS, MSG_USERAUTH_BANNER
|
||||||
|
from message import Message
|
||||||
|
from secsh import SSHException
|
||||||
|
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
|
||||||
|
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
|
||||||
|
DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
|
||||||
|
|
||||||
|
AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3)
|
||||||
|
|
||||||
|
|
||||||
|
class Transport(BaseTransport):
|
||||||
|
"BaseTransport with the auth framework hooked up"
|
||||||
|
def __init__(self, sock):
|
||||||
|
BaseTransport.__init__(self, sock)
|
||||||
|
self.auth_event = None
|
||||||
|
# for server mode:
|
||||||
|
self.auth_username = None
|
||||||
|
self.auth_fail_count = 0
|
||||||
|
self.auth_complete = 0
|
||||||
|
|
||||||
|
def request_auth(self):
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_SERVICE_REQUEST))
|
||||||
|
m.add_string('ssh-userauth')
|
||||||
|
self.send_message(m)
|
||||||
|
|
||||||
|
def auth_key(self, username, key, event):
|
||||||
|
if (not self.active) or (not self.initial_kex_done):
|
||||||
|
# we should never try to send the password unless we're on a secure link
|
||||||
|
raise SSHException('No existing session')
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
self.auth_event = event
|
||||||
|
self.auth_method = 'publickey'
|
||||||
|
self.username = username
|
||||||
|
self.private_key = key
|
||||||
|
self.request_auth()
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def auth_password(self, username, password, event):
|
||||||
|
'authenticate using a password; event is triggered on success or fail'
|
||||||
|
if (not self.active) or (not self.initial_kex_done):
|
||||||
|
# we should never try to send the password unless we're on a secure link
|
||||||
|
raise SSHException('No existing session')
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
self.auth_event = event
|
||||||
|
self.auth_method = 'password'
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.request_auth()
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def disconnect_service_not_available(self):
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_DISCONNECT))
|
||||||
|
m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE)
|
||||||
|
m.add_string('Service not available')
|
||||||
|
m.add_string('en')
|
||||||
|
self.send_message(m)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def disconnect_no_more_auth(self):
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_DISCONNECT))
|
||||||
|
m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE)
|
||||||
|
m.add_string('No more auth methods available')
|
||||||
|
m.add_string('en')
|
||||||
|
self.send_message(m)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def parse_service_request(self, m):
|
||||||
|
service = m.get_string()
|
||||||
|
if self.server_mode and (service == 'ssh-userauth'):
|
||||||
|
# accepted
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_SERVICE_ACCEPT))
|
||||||
|
m.add_string(service)
|
||||||
|
self.send_message(m)
|
||||||
|
return
|
||||||
|
# dunno this one
|
||||||
|
self.disconnect_service_not_available()
|
||||||
|
|
||||||
|
def parse_service_accept(self, m):
|
||||||
|
service = m.get_string()
|
||||||
|
if service == 'ssh-userauth':
|
||||||
|
self.log(DEBUG, 'userauth is OK')
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_USERAUTH_REQUEST))
|
||||||
|
m.add_string(self.username)
|
||||||
|
m.add_string('ssh-connection')
|
||||||
|
m.add_string(self.auth_method)
|
||||||
|
if self.auth_method == 'password':
|
||||||
|
m.add_boolean(0)
|
||||||
|
m.add_string(self.password.encode('UTF-8'))
|
||||||
|
elif self.auth_method == 'publickey':
|
||||||
|
m.add_boolean(1)
|
||||||
|
m.add_string(self.private_key.get_name())
|
||||||
|
m.add_string(str(self.private_key))
|
||||||
|
m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username))
|
||||||
|
else:
|
||||||
|
raise SSHException('Unknown auth method "%s"' % self.auth_method)
|
||||||
|
self.send_message(m)
|
||||||
|
else:
|
||||||
|
self.log(DEBUG, 'Service request "%s" accepted (?)' % service)
|
||||||
|
|
||||||
|
def get_allowed_auths(self):
|
||||||
|
"override me!"
|
||||||
|
return 'password'
|
||||||
|
|
||||||
|
def check_auth_none(self, username):
|
||||||
|
"override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
|
||||||
|
return (AUTH_FAILED, self.get_allowed_auths())
|
||||||
|
|
||||||
|
def check_auth_password(self, username, password):
|
||||||
|
"override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
|
||||||
|
return (AUTH_FAILED, self.get_allowed_auths())
|
||||||
|
|
||||||
|
def check_auth_publickey(self, username, key):
|
||||||
|
"override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
|
||||||
|
return (AUTH_FAILED, self.get_allowed_auths())
|
||||||
|
|
||||||
|
def parse_userauth_request(self, m):
|
||||||
|
if not self.server_mode:
|
||||||
|
# er, uh... what?
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_USERAUTH_FAILURE))
|
||||||
|
m.add_string('none')
|
||||||
|
m.add_boolean(0)
|
||||||
|
self.send_message(m)
|
||||||
|
return
|
||||||
|
if self.auth_complete:
|
||||||
|
# ignore
|
||||||
|
return
|
||||||
|
username = m.get_string()
|
||||||
|
service = m.get_string()
|
||||||
|
method = m.get_string()
|
||||||
|
if service != 'ssh-connection':
|
||||||
|
self.disconnect_service_not_available()
|
||||||
|
return
|
||||||
|
if (self.auth_username is not None) and (self.auth_username != username):
|
||||||
|
# trying to change username in mid-flight!
|
||||||
|
self.disconnect_no_more_auth()
|
||||||
|
return
|
||||||
|
if method == 'none':
|
||||||
|
result = self.check_auth_none(username)
|
||||||
|
elif method == 'password':
|
||||||
|
changereq = m.get_boolean()
|
||||||
|
password = m.get_string().decode('UTF-8')
|
||||||
|
if changereq:
|
||||||
|
# always treated as failure, since we don't support changing passwords, but collect
|
||||||
|
# the list of valid auth types from the callback anyway
|
||||||
|
newpassword = m.get_string().decode('UTF-8')
|
||||||
|
result = self.check_auth_password(username, password)
|
||||||
|
result = (AUTH_FAILED, result[1])
|
||||||
|
else:
|
||||||
|
result = self.check_auth_password(username, password)
|
||||||
|
elif method == 'publickey':
|
||||||
|
# FIXME
|
||||||
|
result = self.check_auth_none(username)
|
||||||
|
result = (AUTH_FAILED, result[1])
|
||||||
|
else:
|
||||||
|
result = self.check_auth_none(username)
|
||||||
|
result = (AUTH_FAILED, result[1])
|
||||||
|
# okay, send result
|
||||||
|
m = Message()
|
||||||
|
if result[0] == AUTH_SUCCESSFUL:
|
||||||
|
m.add_byte(chr(MSG_USERAUTH_SUCCESSFUL))
|
||||||
|
self.auth_complete = 1
|
||||||
|
else:
|
||||||
|
m.add_byte(chr(MSG_USERAUTH_FAILURE))
|
||||||
|
m.add_string(result[1])
|
||||||
|
if result[0] == AUTH_PARTIALLY_SUCCESSFUL:
|
||||||
|
m.add_boolean(1)
|
||||||
|
else:
|
||||||
|
m.add_boolean(0)
|
||||||
|
self.auth_fail_count += 1
|
||||||
|
self.send_message(m)
|
||||||
|
if self.auth_fail_count >= 10:
|
||||||
|
self.disconnect_no_more_auth()
|
||||||
|
|
||||||
|
def parse_userauth_success(self, m):
|
||||||
|
self.log(INFO, 'Authentication successful!')
|
||||||
|
self.authenticated = 1
|
||||||
|
if self.auth_event != None:
|
||||||
|
self.auth_event.set()
|
||||||
|
|
||||||
|
def parse_userauth_failure(self, m):
|
||||||
|
authlist = m.get_list()
|
||||||
|
partial = m.get_boolean()
|
||||||
|
if partial:
|
||||||
|
self.log(INFO, 'Authentication continues...')
|
||||||
|
self.log(DEBUG, 'Methods: ' + str(partial))
|
||||||
|
# FIXME - do something
|
||||||
|
pass
|
||||||
|
self.log(INFO, 'Authentication failed.')
|
||||||
|
self.authenticated = 0
|
||||||
|
self.close()
|
||||||
|
if self.auth_event != None:
|
||||||
|
self.auth_event.set()
|
||||||
|
|
||||||
|
def parse_userauth_banner(self, m):
|
||||||
|
banner = m.get_string()
|
||||||
|
lang = m.get_string()
|
||||||
|
self.log(INFO, 'Auth banner: ' + banner)
|
||||||
|
# who cares.
|
||||||
|
|
||||||
|
handler_table = BaseTransport.handler_table.copy()
|
||||||
|
handler_table.update({
|
||||||
|
MSG_SERVICE_REQUEST: parse_service_request,
|
||||||
|
MSG_SERVICE_ACCEPT: parse_service_accept,
|
||||||
|
MSG_USERAUTH_REQUEST: parse_userauth_request,
|
||||||
|
MSG_USERAUTH_SUCCESS: parse_userauth_success,
|
||||||
|
MSG_USERAUTH_FAILURE: parse_userauth_failure,
|
||||||
|
MSG_USERAUTH_BANNER: parse_userauth_banner,
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
def inflate_long(s, always_positive=0):
|
||||||
|
"turns a normalized byte string into a long-int (adapted from Crypto.Util.number)"
|
||||||
|
out = 0L
|
||||||
|
if len(s) % 4:
|
||||||
|
filler = '\x00'
|
||||||
|
if not always_positive and (ord(s[0]) >= 0x80):
|
||||||
|
# negative
|
||||||
|
filler = '\xff'
|
||||||
|
s = filler * (4 - len(s) % 4) + s
|
||||||
|
# FIXME: this doesn't actually handle negative.
|
||||||
|
# luckily ssh never uses negative bignums.
|
||||||
|
for i in range(0, len(s), 4):
|
||||||
|
out = (out << 32) + struct.unpack('>I', s[i:i+4])[0]
|
||||||
|
return out
|
||||||
|
|
||||||
|
def deflate_long(n, add_sign_padding=1):
|
||||||
|
"turns a long-int into a normalized byte string (adapted from Crypto.Util.number)"
|
||||||
|
# after much testing, this algorithm was deemed to be the fastest
|
||||||
|
s = ''
|
||||||
|
n = long(n)
|
||||||
|
while n > 0:
|
||||||
|
s = struct.pack('>I', n & 0xffffffffL) + s
|
||||||
|
n = n >> 32
|
||||||
|
# strip off leading zeros
|
||||||
|
for i in enumerate(s):
|
||||||
|
if i[1] != '\000':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# only happens when n == 0
|
||||||
|
s = '\000'
|
||||||
|
i = (0,)
|
||||||
|
s = s[i[0]:]
|
||||||
|
if (ord(s[0]) >= 0x80) and add_sign_padding:
|
||||||
|
s = '\x00' + s
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
class BER(object):
|
||||||
|
|
||||||
|
def __init__(self, content=''):
|
||||||
|
self.content = content
|
||||||
|
self.idx = 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.content
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'BER(' + repr(self.content) + ')'
|
||||||
|
|
||||||
|
def decode(self):
|
||||||
|
return self.decode_next()
|
||||||
|
|
||||||
|
def decode_next(self):
|
||||||
|
if self.idx >= len(self.content):
|
||||||
|
return None
|
||||||
|
id = ord(self.content[self.idx])
|
||||||
|
self.idx += 1
|
||||||
|
if (id & 31) == 31:
|
||||||
|
# identifier > 30
|
||||||
|
id = 0
|
||||||
|
while self.idx < len(self.content):
|
||||||
|
t = ord(self.content[self.idx])
|
||||||
|
if not (t & 0x80):
|
||||||
|
break
|
||||||
|
id = (id << 7) | (t & 0x7f)
|
||||||
|
self.idx += 1
|
||||||
|
if self.idx >= len(self.content):
|
||||||
|
return None
|
||||||
|
# now fetch length
|
||||||
|
size = ord(self.content[self.idx])
|
||||||
|
self.idx += 1
|
||||||
|
if size & 0x80:
|
||||||
|
# more complimicated...
|
||||||
|
# FIXME: theoretically should handle indefinite-length (0x80)
|
||||||
|
t = size & 0x7f
|
||||||
|
if self.idx + t > len(self.content):
|
||||||
|
return None
|
||||||
|
size = 0
|
||||||
|
while t > 0:
|
||||||
|
size = (size << 8) | ord(self.content[self.idx])
|
||||||
|
self.idx += 1
|
||||||
|
t -= 1
|
||||||
|
if self.idx + size > len(self.content):
|
||||||
|
# can't fit
|
||||||
|
return None
|
||||||
|
data = self.content[self.idx : self.idx + size]
|
||||||
|
self.idx += size
|
||||||
|
# now switch on id
|
||||||
|
if id == 0x30:
|
||||||
|
# sequence
|
||||||
|
return self.decode_sequence(data)
|
||||||
|
elif id == 2:
|
||||||
|
# int
|
||||||
|
return inflate_long(data)
|
||||||
|
else:
|
||||||
|
# 1: boolean (00 false, otherwise true)
|
||||||
|
raise Exception('Unknown ber encoding type %d (robey is lazy)' % id)
|
||||||
|
|
||||||
|
def decode_sequence(data):
|
||||||
|
out = []
|
||||||
|
b = BER(data)
|
||||||
|
while 1:
|
||||||
|
x = b.decode_next()
|
||||||
|
if x == None:
|
||||||
|
return out
|
||||||
|
out.append(x)
|
||||||
|
decode_sequence = staticmethod(decode_sequence)
|
||||||
|
|
|
@ -0,0 +1,608 @@
|
||||||
|
from message import Message
|
||||||
|
from secsh import SSHException
|
||||||
|
from transport import MSG_CHANNEL_REQUEST, MSG_CHANNEL_CLOSE, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, \
|
||||||
|
MSG_CHANNEL_EOF
|
||||||
|
|
||||||
|
import time, threading, logging, socket, os
|
||||||
|
from logging import DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
# this is ugly, and won't work on windows
|
||||||
|
def set_nonblocking(fd):
|
||||||
|
import fcntl
|
||||||
|
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||||
|
|
||||||
|
|
||||||
|
class Channel(object):
|
||||||
|
"""
|
||||||
|
Abstraction for a secsh channel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, chanid, transport):
|
||||||
|
self.chanid = chanid
|
||||||
|
self.transport = transport
|
||||||
|
self.active = 0
|
||||||
|
self.eof_received = 0
|
||||||
|
self.eof_sent = 0
|
||||||
|
self.in_buffer = ''
|
||||||
|
self.timeout = None
|
||||||
|
self.closed = 0
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.in_buffer_cv = threading.Condition(self.lock)
|
||||||
|
self.out_buffer_cv = threading.Condition(self.lock)
|
||||||
|
self.name = str(chanid)
|
||||||
|
self.logger = logging.getLogger('secsh.chan.' + str(chanid))
|
||||||
|
self.pipe_rfd = self.pipe_wfd = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
out = '<secsh.Channel %d' % self.chanid
|
||||||
|
if self.closed:
|
||||||
|
out += ' (closed)'
|
||||||
|
elif self.active:
|
||||||
|
if self.eof_received:
|
||||||
|
out += ' (EOF received)'
|
||||||
|
if self.eof_sent:
|
||||||
|
out += ' (EOF sent)'
|
||||||
|
out += ' (open) window=%d' % (self.out_window_size)
|
||||||
|
if len(self.in_buffer) > 0:
|
||||||
|
out += ' in-buffer=%d' % (len(self.in_buffer),)
|
||||||
|
out += ' -> ' + repr(self.transport)
|
||||||
|
out += '>'
|
||||||
|
return out
|
||||||
|
|
||||||
|
def log(self, level, msg):
|
||||||
|
self.logger.log(level, msg)
|
||||||
|
|
||||||
|
def set_window(self, window_size, max_packet_size):
|
||||||
|
self.in_window_size = window_size
|
||||||
|
self.in_max_packet_size = max_packet_size
|
||||||
|
# threshold of bytes we receive before we bother to send a window update
|
||||||
|
self.in_window_threshold = window_size // 10
|
||||||
|
self.in_window_sofar = 0
|
||||||
|
|
||||||
|
def set_server_channel(self, chanid, window_size, max_packet_size):
|
||||||
|
self.server_chanid = chanid
|
||||||
|
self.out_window_size = window_size
|
||||||
|
self.out_max_packet_size = max_packet_size
|
||||||
|
self.active = 1
|
||||||
|
|
||||||
|
def request_success(self, m):
|
||||||
|
self.log(DEBUG, 'Sesch channel %d request ok' % self.chanid)
|
||||||
|
return
|
||||||
|
|
||||||
|
def request_failed(self, m):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def feed(self, m):
|
||||||
|
s = m.get_string()
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
self.log(DEBUG, 'fed %d bytes' % len(s))
|
||||||
|
if self.pipe_wfd != None:
|
||||||
|
self.feed_pipe(s)
|
||||||
|
else:
|
||||||
|
self.in_buffer += s
|
||||||
|
self.in_buffer_cv.notifyAll()
|
||||||
|
self.log(DEBUG, '(out from feed)')
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def window_adjust(self, m):
|
||||||
|
nbytes = m.get_int()
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
self.log(DEBUG, 'window up %d' % nbytes)
|
||||||
|
self.out_window_size += nbytes
|
||||||
|
self.out_buffer_cv.notifyAll()
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def handle_request(self, m):
|
||||||
|
key = m.get_string()
|
||||||
|
if key == 'exit-status':
|
||||||
|
self.exit_status = m.get_int()
|
||||||
|
return
|
||||||
|
elif key == 'xon-xoff':
|
||||||
|
# ignore
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.log(DEBUG, 'Unhandled channel request "%s"' % key)
|
||||||
|
|
||||||
|
def handle_eof(self, m):
|
||||||
|
self.eof_received = 1
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
self.in_buffer_cv.notifyAll()
|
||||||
|
if self.pipe_wfd != None:
|
||||||
|
os.close(self.pipe_wfd)
|
||||||
|
self.pipe_wfd = None
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
self.log(DEBUG, 'EOF received')
|
||||||
|
|
||||||
|
def handle_close(self, m):
|
||||||
|
self.close()
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
self.in_buffer_cv.notifyAll()
|
||||||
|
self.out_buffer_cv.notifyAll()
|
||||||
|
if self.pipe_wfd != None:
|
||||||
|
os.close(self.pipe_wfd)
|
||||||
|
self.pipe_wfd = None
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
# API for external use
|
||||||
|
|
||||||
|
def get_pty(self, term='vt100', width=80, height=24):
|
||||||
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
|
raise SSHException('Channel is not open')
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
m.add_string('pty-req')
|
||||||
|
m.add_boolean(0)
|
||||||
|
m.add_string(term)
|
||||||
|
m.add_int(width)
|
||||||
|
m.add_int(height)
|
||||||
|
# pixel height, width (usually useless)
|
||||||
|
m.add_int(0).add_int(0)
|
||||||
|
m.add_string('')
|
||||||
|
self.transport.send_message(m)
|
||||||
|
|
||||||
|
def invoke_shell(self):
|
||||||
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
|
raise SSHException('Channel is not open')
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
m.add_string('shell')
|
||||||
|
m.add_boolean(1)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
|
||||||
|
def exec_command(self, command):
|
||||||
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
|
raise SSHException('Channel is not open')
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
m.add_string('exec')
|
||||||
|
m.add_boolean(1)
|
||||||
|
m.add_string(command)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
|
||||||
|
def invoke_subsystem(self, subsystem):
|
||||||
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
|
raise SSHException('Channel is not open')
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
m.add_string('subsystem')
|
||||||
|
m.add_boolean(1)
|
||||||
|
m.add_string(subsystem)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
|
||||||
|
def resize_pty(self, width=80, height=24):
|
||||||
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
|
raise SSHException('Channel is not open')
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
m.add_string('window-change')
|
||||||
|
m.add_boolean(0)
|
||||||
|
m.add_int(width)
|
||||||
|
m.add_int(height)
|
||||||
|
m.add_int(0).add_int(0)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
|
||||||
|
def get_transport(self):
|
||||||
|
return self.transport
|
||||||
|
|
||||||
|
def set_name(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.logger = logging.getLogger('secsh.chan.' + name)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def send_eof(self):
|
||||||
|
if self.eof_sent:
|
||||||
|
return
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_EOF))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.eof_sent = 1
|
||||||
|
self.log(DEBUG, 'EOF sent')
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# socket equivalency methods...
|
||||||
|
|
||||||
|
def settimeout(self, timeout):
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def gettimeout(self):
|
||||||
|
return self.timeout
|
||||||
|
|
||||||
|
def setblocking(self, blocking):
|
||||||
|
if blocking:
|
||||||
|
self.settimeout(None)
|
||||||
|
else:
|
||||||
|
self.settimeout(0.0)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.closed or not self.active:
|
||||||
|
return
|
||||||
|
self.send_eof()
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_CLOSE))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.closed = 1
|
||||||
|
self.transport.unlink_channel(self.chanid)
|
||||||
|
|
||||||
|
def recv_ready(self):
|
||||||
|
"doesn't work if you've called fileno()"
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
if len(self.in_buffer) == 0:
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def recv(self, nbytes):
|
||||||
|
out = ''
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
if self.pipe_rfd != None:
|
||||||
|
# use the pipe
|
||||||
|
return self.read_pipe(nbytes)
|
||||||
|
if len(self.in_buffer) == 0:
|
||||||
|
if self.closed or self.eof_received:
|
||||||
|
return out
|
||||||
|
# should we block?
|
||||||
|
if self.timeout == 0.0:
|
||||||
|
raise socket.timeout()
|
||||||
|
# loop here in case we get woken up but a different thread has grabbed everything in the buffer
|
||||||
|
timeout = self.timeout
|
||||||
|
while (len(self.in_buffer) == 0) and not self.closed and not self.eof_received:
|
||||||
|
then = time.time()
|
||||||
|
self.in_buffer_cv.wait(timeout)
|
||||||
|
if timeout != None:
|
||||||
|
timeout -= time.time() - then
|
||||||
|
if timeout <= 0.0:
|
||||||
|
raise socket.timeout()
|
||||||
|
# something in the buffer and we have the lock
|
||||||
|
if len(self.in_buffer) <= nbytes:
|
||||||
|
out = self.in_buffer
|
||||||
|
self.in_buffer = ''
|
||||||
|
else:
|
||||||
|
out = self.in_buffer[:nbytes]
|
||||||
|
self.in_buffer = self.in_buffer[nbytes:]
|
||||||
|
self.check_add_window(len(out))
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def send(self, s):
|
||||||
|
size = 0
|
||||||
|
if self.closed or self.eof_sent:
|
||||||
|
return size
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
if self.out_window_size == 0:
|
||||||
|
# should we block?
|
||||||
|
if self.timeout == 0.0:
|
||||||
|
raise socket.timeout()
|
||||||
|
# loop here in case we get woken up but a different thread has filled the buffer
|
||||||
|
timeout = self.timeout
|
||||||
|
while self.out_window_size == 0:
|
||||||
|
then = time.time()
|
||||||
|
self.out_buffer_cv.wait(timeout)
|
||||||
|
if timeout != None:
|
||||||
|
timeout -= time.time() - then
|
||||||
|
if timeout <= 0.0:
|
||||||
|
raise socket.timeout()
|
||||||
|
# we have some window to squeeze into
|
||||||
|
if self.closed:
|
||||||
|
return 0
|
||||||
|
size = len(s)
|
||||||
|
if self.out_window_size < size:
|
||||||
|
size = self.out_window_size
|
||||||
|
if self.out_max_packet_size < size:
|
||||||
|
size = self.out_max_packet_size
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_DATA))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
m.add_string(s[:size])
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.out_window_size -= size
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
return size
|
||||||
|
|
||||||
|
def sendall(self, s):
|
||||||
|
while s:
|
||||||
|
if self.closed:
|
||||||
|
# this doesn't seem useful, but it is the documented behavior of Socket
|
||||||
|
raise socket.error('Socket is closed')
|
||||||
|
sent = self.send(s)
|
||||||
|
s = s[sent:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def makefile(self, *params):
|
||||||
|
return ChannelFile(*([self] + list(params)))
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
"""
|
||||||
|
returns an OS-level fd which can be used for polling and reading (but
|
||||||
|
NOT for writing). this is primarily to allow python's \"select\" module
|
||||||
|
to work. the first time this function is called, a pipe is created to
|
||||||
|
simulate real OS-level fd behavior. because of this, two actual fds are
|
||||||
|
created: one to return and one to feed. this may be inefficient if you
|
||||||
|
plan to use many fds.
|
||||||
|
|
||||||
|
the channel's receive window will be updated as data comes in, not as
|
||||||
|
you read it, so if you fail to poll the channel often enough, it may
|
||||||
|
block ALL channels across the transport.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
if self.pipe_rfd != None:
|
||||||
|
return self.pipe_rfd
|
||||||
|
# create the pipe and feed in any existing data
|
||||||
|
self.pipe_rfd, self.pipe_wfd = os.pipe()
|
||||||
|
set_nonblocking(self.pipe_wfd)
|
||||||
|
set_nonblocking(self.pipe_rfd)
|
||||||
|
if len(self.in_buffer) > 0:
|
||||||
|
x = self.in_buffer
|
||||||
|
self.in_buffer = ''
|
||||||
|
self.feed_pipe(x)
|
||||||
|
return self.pipe_rfd
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def shutdown(self, how):
|
||||||
|
if (how == 0) or (how == 2):
|
||||||
|
# feign "read" shutdown
|
||||||
|
self.eof_received = 1
|
||||||
|
if (how == 1) or (how == 2):
|
||||||
|
self.send_eof()
|
||||||
|
|
||||||
|
|
||||||
|
# internal use...
|
||||||
|
|
||||||
|
def feed_pipe(self, data):
|
||||||
|
"you are already holding the lock"
|
||||||
|
if len(self.in_buffer) > 0:
|
||||||
|
self.in_buffer += data
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
n = os.write(self.pipe_wfd, data)
|
||||||
|
if n < len(data):
|
||||||
|
# at least on linux, this will never happen, as the writes are
|
||||||
|
# considered atomic... but just in case.
|
||||||
|
self.in_buffer = data[n:]
|
||||||
|
self.check_add_window(n)
|
||||||
|
self.in_buffer_cv.notifyAll()
|
||||||
|
return
|
||||||
|
except OSError, e:
|
||||||
|
pass
|
||||||
|
if len(data) > 1:
|
||||||
|
# try writing just one byte then
|
||||||
|
x = data[0]
|
||||||
|
data = data[1:]
|
||||||
|
try:
|
||||||
|
os.write(self.pipe_wfd, x)
|
||||||
|
self.in_buffer = data
|
||||||
|
self.check_add_window(1)
|
||||||
|
self.in_buffer_cv.notifyAll()
|
||||||
|
return
|
||||||
|
except OSError, e:
|
||||||
|
data = x + data
|
||||||
|
# pipe is very full
|
||||||
|
self.in_buffer = data
|
||||||
|
self.in_buffer_cv.notifyAll()
|
||||||
|
|
||||||
|
def read_pipe(self, nbytes):
|
||||||
|
"you are already holding the lock"
|
||||||
|
try:
|
||||||
|
x = os.read(self.pipe_rfd, nbytes)
|
||||||
|
if len(x) > 0:
|
||||||
|
self.push_pipe(len(x))
|
||||||
|
return x
|
||||||
|
except OSError, e:
|
||||||
|
pass
|
||||||
|
# nothing in the pipe
|
||||||
|
if self.closed or self.eof_received:
|
||||||
|
return ''
|
||||||
|
# should we block?
|
||||||
|
if self.timeout == 0.0:
|
||||||
|
raise socket.timeout()
|
||||||
|
# loop here in case we get woken up but a different thread has grabbed everything in the buffer
|
||||||
|
timeout = self.timeout
|
||||||
|
while not self.closed and not self.eof_received:
|
||||||
|
then = time.time()
|
||||||
|
self.in_buffer_cv.wait(timeout)
|
||||||
|
if timeout != None:
|
||||||
|
timeout -= time.time() - then
|
||||||
|
if timeout <= 0.0:
|
||||||
|
raise socket.timeout()
|
||||||
|
try:
|
||||||
|
x = os.read(self.pipe_rfd, nbytes)
|
||||||
|
if len(x) > 0:
|
||||||
|
self.push_pipe(len(x))
|
||||||
|
return x
|
||||||
|
except OSError, e:
|
||||||
|
pass
|
||||||
|
pass
|
||||||
|
|
||||||
|
def push_pipe(self, nbytes):
|
||||||
|
# successfully read N bytes from the pipe, now re-feed the pipe if necessary
|
||||||
|
# (assumption: the pipe can hold as many bytes as were read out)
|
||||||
|
if len(self.in_buffer) == 0:
|
||||||
|
return
|
||||||
|
if len(self.in_buffer) <= nbytes:
|
||||||
|
os.write(self.pipe_wfd, self.in_buffer)
|
||||||
|
self.in_buffer = ''
|
||||||
|
return
|
||||||
|
x = self.in_buffer[:nbytes]
|
||||||
|
self.in_buffer = self.in_buffer[nbytes:]
|
||||||
|
os.write(self.pipd_wfd, x)
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
if self.closed or not self.active:
|
||||||
|
return
|
||||||
|
self.closed = 1
|
||||||
|
self.transport.unlink_channel(self.chanid)
|
||||||
|
|
||||||
|
def check_add_window(self, n):
|
||||||
|
# already holding the lock!
|
||||||
|
if self.closed or self.eof_received or not self.active:
|
||||||
|
return
|
||||||
|
self.log(DEBUG, 'addwindow %d' % n)
|
||||||
|
self.in_window_sofar += n
|
||||||
|
if self.in_window_sofar > self.in_window_threshold:
|
||||||
|
self.log(DEBUG, 'addwindow send %d' % self.in_window_sofar)
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_WINDOW_ADJUST))
|
||||||
|
m.add_int(self.server_chanid)
|
||||||
|
m.add_int(self.in_window_sofar)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.in_window_sofar = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelFile(object):
|
||||||
|
"""
|
||||||
|
A file-like wrapper around Channel.
|
||||||
|
Doesn't have the non-portable side effect of Channel.fileno().
|
||||||
|
XXX Todo: the channel and its file-wrappers should be able to be closed or
|
||||||
|
garbage-collected independently, for compatibility with real sockets and
|
||||||
|
their file-wrappers. Currently, closing does nothing but flush the buffer.
|
||||||
|
XXX Todo: translation of the various forms of newline is not implemented,
|
||||||
|
let alone the universal newline. Line buffering (for writing) is
|
||||||
|
implemented, though, which makes little sense without text mode support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel, mode = "r", buf_size = -1):
|
||||||
|
self.channel = channel
|
||||||
|
self.mode = mode
|
||||||
|
if buf_size < 0:
|
||||||
|
self.buf_size = 1024
|
||||||
|
self.line_buffered = 0
|
||||||
|
elif buf_size == 1:
|
||||||
|
self.buf_size = 1
|
||||||
|
self.line_buffered = 1
|
||||||
|
else:
|
||||||
|
self.buf_size = buf_size
|
||||||
|
self.line_buffered = 0
|
||||||
|
self.wbuffer = ""
|
||||||
|
self.rbuffer = ""
|
||||||
|
self.readable = ("r" in mode)
|
||||||
|
self.writable = ("w" in mode) or ("+" in mode) or ("a" in mode)
|
||||||
|
self.binary = ("b" in mode)
|
||||||
|
if not self.binary:
|
||||||
|
raise NotImplementedError("text mode not supported")
|
||||||
|
self.softspace = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
line = self.readline()
|
||||||
|
if not line:
|
||||||
|
raise StopIteration
|
||||||
|
return line
|
||||||
|
|
||||||
|
def write(self, str):
|
||||||
|
if not self.writable:
|
||||||
|
raise IOError("file not open for writing")
|
||||||
|
if self.buf_size == 0 and not self.line_buffered:
|
||||||
|
self.channel.sendall(str)
|
||||||
|
return
|
||||||
|
self.wbuffer += str
|
||||||
|
if self.line_buffered:
|
||||||
|
last_newline_pos = self.wbuffer.rfind("\n")
|
||||||
|
if last_newline_pos >= 0:
|
||||||
|
self.channel.sendall(self.wbuffer[:last_newline_pos+1])
|
||||||
|
self.wbuffer = self.wbuffer[last_newline_pos+1:]
|
||||||
|
else:
|
||||||
|
if len(self.wbuffer) >= self.buf_size:
|
||||||
|
self.channel.sendall(self.wbuffer)
|
||||||
|
self.wbuffer = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
def writelines(self, sequence):
|
||||||
|
for s in sequence:
|
||||||
|
self.write(s)
|
||||||
|
return
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.channel.sendall(self.wbuffer)
|
||||||
|
self.wbuffer = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
def read(self, size = None):
|
||||||
|
if not self.readable:
|
||||||
|
raise IOError("file not open for reading")
|
||||||
|
if size is None or size < 0:
|
||||||
|
result = self.rbuffer
|
||||||
|
self.rbuffer = ""
|
||||||
|
while not self.channel.eof_received:
|
||||||
|
new_data = self.channel.recv(65536)
|
||||||
|
if not new_data:
|
||||||
|
break
|
||||||
|
result += new_data
|
||||||
|
return result
|
||||||
|
if size <= len(self.rbuffer):
|
||||||
|
result = self.rbuffer[:size]
|
||||||
|
self.rbuffer = self.rbuffer[size:]
|
||||||
|
return result
|
||||||
|
while len(self.rbuffer) < size and not self.channel.eof_received:
|
||||||
|
new_data = self.channel.recv(max(self.buf_size, size-len(self.rbuffer)))
|
||||||
|
if not new_data:
|
||||||
|
break
|
||||||
|
self.rbuffer += new_data
|
||||||
|
result = self.rbuffer[:size]
|
||||||
|
self.rbuffer[size:]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def readline(self, size = None):
|
||||||
|
line = ""
|
||||||
|
while "\n" not in line:
|
||||||
|
if size >= 0:
|
||||||
|
new_data = self.read(size - len(line))
|
||||||
|
else:
|
||||||
|
new_data = self.read(64)
|
||||||
|
if not new_data:
|
||||||
|
break
|
||||||
|
line += new_data
|
||||||
|
newline_pos = line.find("\n")
|
||||||
|
if newline_pos >= 0:
|
||||||
|
self.rbuffer = line[newline_pos+1:] + self.rbuffer
|
||||||
|
return line[:newline_pos+1]
|
||||||
|
elif len(line) > size:
|
||||||
|
self.rbuffer = line[size:] + self.rbuffer
|
||||||
|
return line[:size]
|
||||||
|
return line
|
||||||
|
|
||||||
|
def readlines(self, sizehint = None):
|
||||||
|
lines = []
|
||||||
|
while 1:
|
||||||
|
line = self.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
lines.append(line)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def xreadlines(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# vim: set shiftwidth=4 expandtab :
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import sys, os, socket, threading, logging, traceback
|
||||||
|
import secsh
|
||||||
|
|
||||||
|
# setup logging
|
||||||
|
l = logging.getLogger("secsh")
|
||||||
|
l.setLevel(logging.DEBUG)
|
||||||
|
if len(l.handlers) == 0:
|
||||||
|
f = open('demo-server.log', 'w')
|
||||||
|
lh = logging.StreamHandler(f)
|
||||||
|
lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S'))
|
||||||
|
l.addHandler(lh)
|
||||||
|
|
||||||
|
host_key = secsh.RSAKey()
|
||||||
|
host_key.read_private_key_file('/home/robey/sshkey/ssh_host_rsa_key')
|
||||||
|
|
||||||
|
# now connect
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.bind(('', 2200))
|
||||||
|
except Exception, e:
|
||||||
|
print '*** Bind failed: ' + str(e)
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.listen(100)
|
||||||
|
client, addr = sock.accept()
|
||||||
|
except Exception, e:
|
||||||
|
print '*** Listen/accept failed: ' + str(e)
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = threading.Event()
|
||||||
|
t = secsh.Transport(client)
|
||||||
|
t.add_server_key(host_key)
|
||||||
|
t.ultra_debug = 1
|
||||||
|
t.start_server(event)
|
||||||
|
# print repr(t)
|
||||||
|
event.wait(10)
|
||||||
|
if not t.is_active():
|
||||||
|
print '*** SSH negotiation failed.'
|
||||||
|
sys.exit(1)
|
||||||
|
# print repr(t)
|
||||||
|
except Exception, e:
|
||||||
|
print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e)
|
||||||
|
traceback.print_exc()
|
||||||
|
try:
|
||||||
|
t.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
sys.exit(1)
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import sys, os, socket, threading, getpass, logging, time, base64, select, termios, tty, traceback
|
||||||
|
import secsh
|
||||||
|
|
||||||
|
|
||||||
|
##### utility functions
|
||||||
|
|
||||||
|
def load_host_keys():
|
||||||
|
filename = os.environ['HOME'] + '/.ssh/known_hosts'
|
||||||
|
keys = {}
|
||||||
|
try:
|
||||||
|
f = open(filename, 'r')
|
||||||
|
except Exception, e:
|
||||||
|
print '*** Unable to open host keys file (%s)' % filename
|
||||||
|
return
|
||||||
|
for line in f:
|
||||||
|
keylist = line.split(' ')
|
||||||
|
if len(keylist) != 3:
|
||||||
|
continue
|
||||||
|
hostlist, keytype, key = keylist
|
||||||
|
hosts = hostlist.split(',')
|
||||||
|
for host in hosts:
|
||||||
|
if not keys.has_key(host):
|
||||||
|
keys[host] = {}
|
||||||
|
keys[host][keytype] = base64.decodestring(key)
|
||||||
|
f.close()
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
##### main demo
|
||||||
|
|
||||||
|
# setup logging
|
||||||
|
l = logging.getLogger("secsh")
|
||||||
|
l.setLevel(logging.DEBUG)
|
||||||
|
if len(l.handlers) == 0:
|
||||||
|
f = open('demo.log', 'w')
|
||||||
|
lh = logging.StreamHandler(f)
|
||||||
|
lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S'))
|
||||||
|
l.addHandler(lh)
|
||||||
|
|
||||||
|
username = ''
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
hostname = sys.argv[1]
|
||||||
|
if hostname.find('@') >= 0:
|
||||||
|
username, hostname = hostname.split('@')
|
||||||
|
else:
|
||||||
|
hostname = raw_input('Hostname: ')
|
||||||
|
if len(hostname) == 0:
|
||||||
|
print '*** Hostname required.'
|
||||||
|
sys.exit(1)
|
||||||
|
port = 22
|
||||||
|
if hostname.find(':') >= 0:
|
||||||
|
hostname, portstr = hostname.split(':')
|
||||||
|
port = int(portstr)
|
||||||
|
|
||||||
|
# now connect
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.connect((hostname, port))
|
||||||
|
except Exception, e:
|
||||||
|
print '*** Connect failed: ' + str(e)
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = threading.Event()
|
||||||
|
t = secsh.Transport(sock)
|
||||||
|
t.ultra_debug = 1
|
||||||
|
t.start_client(event)
|
||||||
|
# print repr(t)
|
||||||
|
event.wait(10)
|
||||||
|
if not t.is_active():
|
||||||
|
print '*** SSH negotiation failed.'
|
||||||
|
sys.exit(1)
|
||||||
|
# print repr(t)
|
||||||
|
|
||||||
|
keys = load_host_keys()
|
||||||
|
keytype, hostkey = t.get_host_key()
|
||||||
|
if not keys.has_key(hostname):
|
||||||
|
print '*** WARNING: Unknown host key!'
|
||||||
|
elif not keys[hostname].has_key(keytype):
|
||||||
|
print '*** WARNING: Unknown host key!'
|
||||||
|
elif keys[hostname][keytype] != hostkey:
|
||||||
|
print '*** WARNING: Host key has changed!!!'
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print '*** Host key OK.'
|
||||||
|
|
||||||
|
event.clear()
|
||||||
|
|
||||||
|
# get username
|
||||||
|
if username == '':
|
||||||
|
default_username = getpass.getuser()
|
||||||
|
username = raw_input('Username [%s]: ' % default_username)
|
||||||
|
if len(username) == 0:
|
||||||
|
username = default_username
|
||||||
|
|
||||||
|
# ask for what kind of authentication to try
|
||||||
|
default_auth = 'p'
|
||||||
|
auth = raw_input('Auth by (p)assword, (r)sa key, or (d)ss key? [%s] ' % default_auth)
|
||||||
|
if len(auth) == 0:
|
||||||
|
auth = default_auth
|
||||||
|
|
||||||
|
if auth == 'r':
|
||||||
|
key = secsh.RSAKey()
|
||||||
|
default_path = os.environ['HOME'] + '/.ssh/id_rsa'
|
||||||
|
path = raw_input('RSA key [%s]: ' % default_path)
|
||||||
|
if len(path) == 0:
|
||||||
|
path = default_path
|
||||||
|
key.read_private_key_file(path)
|
||||||
|
t.auth_key(username, key, event)
|
||||||
|
elif auth == 'd':
|
||||||
|
key = secsh.DSSKey()
|
||||||
|
default_path = os.environ['HOME'] + '/.ssh/id_dsa'
|
||||||
|
path = raw_input('DSS key [%s]: ' % default_path)
|
||||||
|
if len(path) == 0:
|
||||||
|
path = default_path
|
||||||
|
key.read_private_key_file(path)
|
||||||
|
t.auth_key(username, key, event)
|
||||||
|
else:
|
||||||
|
pw = getpass.getpass('Password for %s@%s: ' % (username, hostname))
|
||||||
|
t.auth_password(username, pw, event)
|
||||||
|
|
||||||
|
event.wait(10)
|
||||||
|
# print repr(t)
|
||||||
|
if not t.is_authenticated():
|
||||||
|
print '*** Authentication failed. :('
|
||||||
|
t.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
chan = t.open_session()
|
||||||
|
chan.get_pty()
|
||||||
|
chan.invoke_shell()
|
||||||
|
print '*** Here we go!'
|
||||||
|
print
|
||||||
|
|
||||||
|
try:
|
||||||
|
oldtty = termios.tcgetattr(sys.stdin)
|
||||||
|
tty.setraw(sys.stdin.fileno())
|
||||||
|
tty.setcbreak(sys.stdin.fileno())
|
||||||
|
chan.settimeout(0.0)
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
r, w, e = select.select([chan, sys.stdin], [], [])
|
||||||
|
if chan in r:
|
||||||
|
try:
|
||||||
|
x = chan.recv(1024)
|
||||||
|
if len(x) == 0:
|
||||||
|
print
|
||||||
|
print '*** EOF\r\n',
|
||||||
|
break
|
||||||
|
sys.stdout.write(x)
|
||||||
|
sys.stdout.flush()
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
if sys.stdin in r:
|
||||||
|
# FIXME: reading 1 byte at a time is incredibly dumb.
|
||||||
|
x = sys.stdin.read(1)
|
||||||
|
if len(x) == 0:
|
||||||
|
print
|
||||||
|
print '*** Bye.\r\n',
|
||||||
|
break
|
||||||
|
chan.send(x)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
|
||||||
|
|
||||||
|
chan.close()
|
||||||
|
t.close()
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e)
|
||||||
|
traceback.print_exc()
|
||||||
|
try:
|
||||||
|
t.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
sys.exit(1)
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from message import Message
|
||||||
|
from transport import MSG_USERAUTH_REQUEST
|
||||||
|
from util import inflate_long, deflate_long
|
||||||
|
from Crypto.PublicKey import DSA
|
||||||
|
from Crypto.Hash import SHA
|
||||||
|
from ber import BER
|
||||||
|
|
||||||
|
from util import format_binary
|
||||||
|
|
||||||
|
|
||||||
|
class DSSKey(object):
|
||||||
|
|
||||||
|
def __init__(self, msg=None):
|
||||||
|
self.valid = 0
|
||||||
|
if (msg == None) or (msg.get_string() != 'ssh-dss'):
|
||||||
|
return
|
||||||
|
self.p = msg.get_mpint()
|
||||||
|
self.q = msg.get_mpint()
|
||||||
|
self.g = msg.get_mpint()
|
||||||
|
self.y = msg.get_mpint()
|
||||||
|
self.size = len(deflate_long(self.p, 0))
|
||||||
|
self.valid = 1
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if not self.valid:
|
||||||
|
return ''
|
||||||
|
m = Message()
|
||||||
|
m.add_string('ssh-dss')
|
||||||
|
m.add_mpint(self.p)
|
||||||
|
m.add_mpint(self.q)
|
||||||
|
m.add_mpint(self.g)
|
||||||
|
m.add_mpint(self.y)
|
||||||
|
return str(m)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return 'ssh-dss'
|
||||||
|
|
||||||
|
def verify_ssh_sig(self, data, msg):
|
||||||
|
if not self.valid:
|
||||||
|
return 0
|
||||||
|
if len(str(msg)) == 40:
|
||||||
|
# spies.com bug: signature has no header
|
||||||
|
sig = str(msg)
|
||||||
|
else:
|
||||||
|
kind = msg.get_string()
|
||||||
|
if kind != 'ssh-dss':
|
||||||
|
return 0
|
||||||
|
sig = msg.get_string()
|
||||||
|
|
||||||
|
# pull out (r, s) which are NOT encoded as mpints
|
||||||
|
sigR = inflate_long(sig[:20], 1)
|
||||||
|
sigS = inflate_long(sig[20:], 1)
|
||||||
|
sigM = inflate_long(SHA.new(data).digest(), 1)
|
||||||
|
|
||||||
|
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q)))
|
||||||
|
return dss.verify(sigM, (sigR, sigS))
|
||||||
|
|
||||||
|
def sign_ssh_data(self, data):
|
||||||
|
hash = SHA.new(data).digest()
|
||||||
|
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x)))
|
||||||
|
# generate a suitable k
|
||||||
|
qsize = len(deflate_long(self.q, 0))
|
||||||
|
while 1:
|
||||||
|
k = inflate_long(randpool.get_bytes(qsize), 1)
|
||||||
|
if (k > 2) and (k < self.q):
|
||||||
|
break
|
||||||
|
r, s = dss.sign(inflate_long(hash, 1), k)
|
||||||
|
m = Message()
|
||||||
|
m.add_string('ssh-dss')
|
||||||
|
m.add_string(deflate_long(r, 0) + deflate_long(s, 0))
|
||||||
|
return str(m)
|
||||||
|
|
||||||
|
|
||||||
|
rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
|
||||||
|
sig = deflate_long(rsa.sign(self.pkcs1imify(hash), '')[0], 0)
|
||||||
|
m = Message()
|
||||||
|
m.add_string('ssh-rsa')
|
||||||
|
m.add_string(sig)
|
||||||
|
return str(m)
|
||||||
|
|
||||||
|
def read_private_key_file(self, filename):
|
||||||
|
# private key file contains:
|
||||||
|
# DSAPrivateKey = { version = 0, p, q, g, y, x }
|
||||||
|
self.valid = 0
|
||||||
|
try:
|
||||||
|
f = open(filename, 'r')
|
||||||
|
lines = f.readlines()
|
||||||
|
f.close()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
if lines[0].strip() != '-----BEGIN DSA PRIVATE KEY-----':
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = base64.decodestring(''.join(lines[1:-1]))
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
keylist = BER(data).decode()
|
||||||
|
if (type(keylist) != type([])) or (len(keylist) < 6) or (keylist[0] != 0):
|
||||||
|
return
|
||||||
|
self.p = keylist[1]
|
||||||
|
self.q = keylist[2]
|
||||||
|
self.g = keylist[3]
|
||||||
|
self.y = keylist[4]
|
||||||
|
self.x = keylist[5]
|
||||||
|
self.size = len(deflate_long(self.p, 0))
|
||||||
|
self.valid = 1
|
||||||
|
|
||||||
|
def sign_ssh_session(self, randpool, sid, username):
|
||||||
|
m = Message()
|
||||||
|
m.add_string(sid)
|
||||||
|
m.add_byte(chr(MSG_USERAUTH_REQUEST))
|
||||||
|
m.add_string(username)
|
||||||
|
m.add_string('ssh-connection')
|
||||||
|
m.add_string('publickey')
|
||||||
|
m.add_boolean(1)
|
||||||
|
m.add_string('ssh-dss')
|
||||||
|
m.add_string(str(self))
|
||||||
|
return self.sign_ssh_data(str(m))
|
|
@ -0,0 +1,180 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# variant on group1 (see kex_group1.py) where the prime "p" and generator "g"
|
||||||
|
# are provided by the server. a bit more work is required on our side (and a
|
||||||
|
# LOT more on the server side).
|
||||||
|
|
||||||
|
from message import Message, inflate_long, deflate_long
|
||||||
|
from secsh import SSHException
|
||||||
|
from transport import MSG_NEWKEYS
|
||||||
|
from Crypto.Hash import SHA
|
||||||
|
from Crypto.Util import number
|
||||||
|
from logging import DEBUG
|
||||||
|
|
||||||
|
MSG_KEXDH_GEX_GROUP, MSG_KEXDH_GEX_INIT, MSG_KEXDH_GEX_REPLY, MSG_KEXDH_GEX_REQUEST = range(31, 35)
|
||||||
|
|
||||||
|
|
||||||
|
class KexGex(object):
|
||||||
|
|
||||||
|
name = 'diffie-hellman-group-exchange-sha1'
|
||||||
|
min_bits = 1024
|
||||||
|
max_bits = 8192
|
||||||
|
preferred_bits = 2048
|
||||||
|
|
||||||
|
def __init__(self, transport):
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def start_kex(self):
|
||||||
|
if self.transport.server_mode:
|
||||||
|
self.transport.expected_packet = MSG_KEXDH_GEX_REQUEST
|
||||||
|
return
|
||||||
|
# request a bit range: we accept (min_bits) to (max_bits), but prefer
|
||||||
|
# (preferred_bits). according to the spec, we shouldn't pull the
|
||||||
|
# minimum up above 1024.
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_KEXDH_GEX_REQUEST))
|
||||||
|
m.add_int(self.min_bits)
|
||||||
|
m.add_int(self.preferred_bits)
|
||||||
|
m.add_int(self.max_bits)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.transport.expected_packet = MSG_KEXDH_GEX_GROUP
|
||||||
|
|
||||||
|
def parse_next(self, ptype, m):
|
||||||
|
if ptype == MSG_KEXDH_GEX_REQUEST:
|
||||||
|
return self.parse_kexdh_gex_request(m)
|
||||||
|
elif ptype == MSG_KEXDH_GEX_GROUP:
|
||||||
|
return self.parse_kexdh_gex_group(m)
|
||||||
|
elif ptype == MSG_KEXDH_GEX_INIT:
|
||||||
|
return self.parse_kexdh_gex_init(m)
|
||||||
|
elif ptype == MSG_KEXDH_GEX_REPLY:
|
||||||
|
return self.parse_kexdh_gex_reply(m)
|
||||||
|
raise SSHException('KexGex asked to handle packet type %d' % ptype)
|
||||||
|
|
||||||
|
def bit_length(n):
|
||||||
|
norm = deflate_long(n, 0)
|
||||||
|
hbyte = ord(norm[0])
|
||||||
|
bitlen = len(norm) * 8
|
||||||
|
while not (hbyte & 0x80):
|
||||||
|
hbyte <<= 1
|
||||||
|
bitlen -= 1
|
||||||
|
return bitlen
|
||||||
|
bit_length = staticmethod(bit_length)
|
||||||
|
|
||||||
|
def generate_x(self):
|
||||||
|
# generate an "x" (1 < x < (p-1)/2).
|
||||||
|
q = (self.p - 1) // 2
|
||||||
|
qnorm = deflate_long(q, 0)
|
||||||
|
qhbyte = ord(qnorm[0])
|
||||||
|
bytes = len(qnorm)
|
||||||
|
qmask = 0xff
|
||||||
|
while not (qhbyte & 0x80):
|
||||||
|
qhbyte <<= 1
|
||||||
|
qmask >>= 1
|
||||||
|
while 1:
|
||||||
|
self.transport.randpool.stir()
|
||||||
|
x_bytes = self.transport.randpool.get_bytes(bytes)
|
||||||
|
x_bytes = chr(ord(x_bytes[0]) & qmask) + x_bytes[1:]
|
||||||
|
x = inflate_long(x_bytes, 1)
|
||||||
|
if (x > 1) and (x < q):
|
||||||
|
break
|
||||||
|
self.x = x
|
||||||
|
|
||||||
|
def parse_kexdh_gex_request(self, m):
|
||||||
|
min = m.get_int()
|
||||||
|
preferred = m.get_int()
|
||||||
|
max = m.get_int()
|
||||||
|
# smoosh the user's preferred size into our own limits
|
||||||
|
if preferred > self.max_bits:
|
||||||
|
preferred = self.max_bits
|
||||||
|
if preferred < self.min_bits:
|
||||||
|
preferred = self.min_bits
|
||||||
|
# now save a copy
|
||||||
|
self.min_bits = min
|
||||||
|
self.preferred_bits = preferred
|
||||||
|
self.max_bits = max
|
||||||
|
# generate prime
|
||||||
|
while 1:
|
||||||
|
self.transport.log(DEBUG, 'stir...')
|
||||||
|
self.transport.randpool.stir()
|
||||||
|
self.transport.log(DEBUG, 'get-prime %d...' % preferred)
|
||||||
|
self.p = number.getRandomNumber(preferred, self.transport.randpool.get_bytes)
|
||||||
|
self.transport.log(DEBUG, 'got ' + repr(self.p))
|
||||||
|
if number.isPrime((self.p - 1) // 2):
|
||||||
|
break
|
||||||
|
self.g = 2
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_KEXDH_GEX_GROUP))
|
||||||
|
m.add_mpint(self.p)
|
||||||
|
m.add_mpint(self.g)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.transport.expected_packet = MSG_KEXDH_GEX_INIT
|
||||||
|
|
||||||
|
def parse_kexdh_gex_group(self, m):
|
||||||
|
self.p = m.get_mpint()
|
||||||
|
self.g = m.get_mpint()
|
||||||
|
# reject if p's bit length < 1024 or > 8192
|
||||||
|
bitlen = self.bit_length(self.p)
|
||||||
|
if (bitlen < 1024) or (bitlen > 8192):
|
||||||
|
raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen)
|
||||||
|
self.transport.log(DEBUG, 'Got server p (%d bits)' % bitlen)
|
||||||
|
self.generate_x()
|
||||||
|
# now compute e = g^x mod p
|
||||||
|
self.e = pow(self.g, self.x, self.p)
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_KEXDH_GEX_INIT))
|
||||||
|
m.add_mpint(self.e)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.transport.expected_packet = MSG_KEXDH_GEX_REPLY
|
||||||
|
|
||||||
|
def parse_kexdh_gex_init(self, m):
|
||||||
|
self.e = m.get_mpint()
|
||||||
|
if (self.e < 1) or (self.e > self.p - 1):
|
||||||
|
raise SSHException('Client kex "e" is out of range')
|
||||||
|
self.generate_x()
|
||||||
|
K = pow(self.e, self.x, P)
|
||||||
|
key = str(self.transport.get_server_key())
|
||||||
|
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
|
||||||
|
hm = Message().add(self.transport.remote_version).add(self.transport.local_version)
|
||||||
|
hm.add(self.transport.remote_kex_init).add(self.transport.local_kex_init).add(key)
|
||||||
|
hm.add_int(self.min_bits)
|
||||||
|
hm.add_int(self.preferred_bits)
|
||||||
|
hm.add_int(self.max_bits)
|
||||||
|
hm.add_mpint(self.p)
|
||||||
|
hm.add_mpint(self.g)
|
||||||
|
hm.add(self.e).add(self.f).add(K)
|
||||||
|
H = SHA.new(str(hm)).digest()
|
||||||
|
self.transport.set_K_H(K, H)
|
||||||
|
# sign it
|
||||||
|
sig = self.transport.get_server_key().sign_ssh_data(H)
|
||||||
|
# send reply
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_KEXDH_GEX_REPLY))
|
||||||
|
m.add_string(key)
|
||||||
|
m.add_mpint(self.f)
|
||||||
|
m.add_string(sig)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.transport.activate_outbound()
|
||||||
|
self.transport.expected_packet = MSG_NEWKEYS
|
||||||
|
|
||||||
|
def parse_kexdh_gex_reply(self, m):
|
||||||
|
host_key = m.get_string()
|
||||||
|
self.f = m.get_mpint()
|
||||||
|
sig = m.get_string()
|
||||||
|
if (self.f < 1) or (self.f > self.p - 1):
|
||||||
|
raise SSHException('Server kex "f" is out of range')
|
||||||
|
K = pow(self.f, self.x, self.p)
|
||||||
|
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
|
||||||
|
hm = Message().add(self.transport.local_version).add(self.transport.remote_version)
|
||||||
|
hm.add(self.transport.local_kex_init).add(self.transport.remote_kex_init).add(host_key)
|
||||||
|
hm.add_int(self.min_bits)
|
||||||
|
hm.add_int(self.preferred_bits)
|
||||||
|
hm.add_int(self.max_bits)
|
||||||
|
hm.add_mpint(self.p)
|
||||||
|
hm.add_mpint(self.g)
|
||||||
|
hm.add(self.e).add(self.f).add(K)
|
||||||
|
self.transport.set_K_H(K, SHA.new(str(hm)).digest())
|
||||||
|
self.transport.verify_key(host_key, sig)
|
||||||
|
self.transport.activate_outbound()
|
||||||
|
self.transport.expected_packet = MSG_NEWKEYS
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# standard SSH key exchange ("kex" if you wanna sound cool):
|
||||||
|
# diffie-hellman of 1024 bit key halves, using a known "p" prime and
|
||||||
|
# "g" generator.
|
||||||
|
|
||||||
|
from message import Message, inflate_long
|
||||||
|
from secsh import SSHException
|
||||||
|
from transport import MSG_NEWKEYS
|
||||||
|
from Crypto.Hash import SHA
|
||||||
|
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
|
||||||
|
MSG_KEXDH_INIT, MSG_KEXDH_REPLY = range(30, 32)
|
||||||
|
|
||||||
|
# draft-ietf-secsh-transport-09.txt, page 17
|
||||||
|
P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFFL
|
||||||
|
G = 2
|
||||||
|
|
||||||
|
|
||||||
|
class KexGroup1(object):
|
||||||
|
|
||||||
|
name = 'diffie-hellman-group1-sha1'
|
||||||
|
|
||||||
|
def __init__(self, transport):
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def generate_x(self):
|
||||||
|
# generate an "x" (1 < x < q), where q is (p-1)/2.
|
||||||
|
# p is a 128-byte (1024-bit) number, where the first 64 bits are 1.
|
||||||
|
# therefore q can be approximated as a 2^1023. we drop the subset of
|
||||||
|
# potential x where the first 63 bits are 1, because some of those will be
|
||||||
|
# larger than q (but this is a tiny tiny subset of potential x).
|
||||||
|
while 1:
|
||||||
|
self.transport.randpool.stir()
|
||||||
|
x_bytes = self.transport.randpool.get_bytes(128)
|
||||||
|
x_bytes = chr(ord(x_bytes[0]) & 0x7f) + x_bytes[1:]
|
||||||
|
if (x_bytes[:8] != '\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF') and \
|
||||||
|
(x_bytes[:8] != '\x00\x00\x00\x00\x00\x00\x00\x00'):
|
||||||
|
break
|
||||||
|
self.x = inflate_long(x_bytes)
|
||||||
|
|
||||||
|
def start_kex(self):
|
||||||
|
self.generate_x()
|
||||||
|
if self.transport.server_mode:
|
||||||
|
# compute f = g^x mod p, but don't send it yet
|
||||||
|
self.f = pow(G, self.x, P)
|
||||||
|
self.transport.expected_packet = MSG_KEXDH_INIT
|
||||||
|
return
|
||||||
|
# compute e = g^x mod p (where g=2), and send it
|
||||||
|
self.e = pow(G, self.x, P)
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_KEXDH_INIT))
|
||||||
|
m.add_mpint(self.e)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.transport.expected_packet = MSG_KEXDH_REPLY
|
||||||
|
|
||||||
|
def parse_next(self, ptype, m):
|
||||||
|
if self.transport.server_mode and (ptype == MSG_KEXDH_INIT):
|
||||||
|
return self.parse_kexdh_init(m)
|
||||||
|
elif not self.transport.server_mode and (ptype == MSG_KEXDH_REPLY):
|
||||||
|
return self.parse_kexdh_reply(m)
|
||||||
|
raise SSHException('KexGroup1 asked to handle packet type %d' % ptype)
|
||||||
|
|
||||||
|
def parse_kexdh_reply(self, m):
|
||||||
|
# client mode
|
||||||
|
host_key = m.get_string()
|
||||||
|
self.f = m.get_mpint()
|
||||||
|
if (self.f < 1) or (self.f > P - 1):
|
||||||
|
raise SSHException('Server kex "f" is out of range')
|
||||||
|
sig = m.get_string()
|
||||||
|
K = pow(self.f, self.x, P)
|
||||||
|
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
|
||||||
|
hm = Message().add(self.transport.local_version).add(self.transport.remote_version)
|
||||||
|
hm.add(self.transport.local_kex_init).add(self.transport.remote_kex_init).add(host_key)
|
||||||
|
hm.add(self.e).add(self.f).add(K)
|
||||||
|
self.transport.set_K_H(K, SHA.new(str(hm)).digest())
|
||||||
|
self.transport.verify_key(host_key, sig)
|
||||||
|
self.transport.activate_outbound()
|
||||||
|
self.transport.expected_packet = MSG_NEWKEYS
|
||||||
|
|
||||||
|
def parse_kexdh_init(self, m):
|
||||||
|
# server mode
|
||||||
|
self.e = m.get_mpint()
|
||||||
|
if (self.e < 1) or (self.e > P - 1):
|
||||||
|
raise SSHException('Client kex "e" is out of range')
|
||||||
|
K = pow(self.e, self.x, P)
|
||||||
|
key = str(self.transport.get_server_key())
|
||||||
|
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
|
||||||
|
hm = Message().add(self.transport.remote_version).add(self.transport.local_version)
|
||||||
|
hm.add(self.transport.remote_kex_init).add(self.transport.local_kex_init).add(key)
|
||||||
|
hm.add(self.e).add(self.f).add(K)
|
||||||
|
H = SHA.new(str(hm)).digest()
|
||||||
|
self.transport.set_K_H(K, H)
|
||||||
|
# sign it
|
||||||
|
sig = self.transport.get_server_key().sign_ssh_data(H)
|
||||||
|
# send reply
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_KEXDH_REPLY))
|
||||||
|
m.add_string(key)
|
||||||
|
m.add_mpint(self.f)
|
||||||
|
m.add_string(sig)
|
||||||
|
self.transport.send_message(m)
|
||||||
|
self.transport.activate_outbound()
|
||||||
|
self.transport.expected_packet = MSG_NEWKEYS
|
|
@ -0,0 +1,119 @@
|
||||||
|
# implementation of a secsh "message"
|
||||||
|
|
||||||
|
import string, types, struct
|
||||||
|
from util import inflate_long, deflate_long
|
||||||
|
|
||||||
|
|
||||||
|
class Message(object):
|
||||||
|
"represents the encoding of a secsh message"
|
||||||
|
|
||||||
|
def __init__(self, content=''):
|
||||||
|
self.packet = content
|
||||||
|
self.idx = 0
|
||||||
|
self.seqno = -1
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.packet
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Message(' + repr(self.packet) + ')'
|
||||||
|
|
||||||
|
def get_remainder(self):
|
||||||
|
"remaining bytes still unparsed"
|
||||||
|
return self.packet[self.idx:]
|
||||||
|
|
||||||
|
def get_so_far(self):
|
||||||
|
"bytes that have been parsed"
|
||||||
|
return self.packet[:self.idx]
|
||||||
|
|
||||||
|
def get_bytes(self, n):
|
||||||
|
if self.idx + n > len(self.packet):
|
||||||
|
return '\x00'*n
|
||||||
|
b = self.packet[self.idx:self.idx+n]
|
||||||
|
self.idx = self.idx + n
|
||||||
|
return b
|
||||||
|
|
||||||
|
def get_byte(self):
|
||||||
|
return self.get_bytes(1)
|
||||||
|
|
||||||
|
def get_boolean(self):
|
||||||
|
b = self.get_bytes(1)
|
||||||
|
if b == '\x00':
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def get_int(self):
|
||||||
|
x = self.packet
|
||||||
|
i = self.idx
|
||||||
|
if i + 4 > len(x):
|
||||||
|
return 0
|
||||||
|
n = struct.unpack('>I', x[i:i+4])[0]
|
||||||
|
self.idx = i+4
|
||||||
|
return n
|
||||||
|
|
||||||
|
def get_mpint(self):
|
||||||
|
return inflate_long(self.get_string())
|
||||||
|
|
||||||
|
def get_string(self):
|
||||||
|
l = self.get_int()
|
||||||
|
if self.idx + l > len(self.packet):
|
||||||
|
return ''
|
||||||
|
str = self.packet[self.idx:self.idx+l]
|
||||||
|
self.idx = self.idx + l
|
||||||
|
return str
|
||||||
|
|
||||||
|
def get_list(self):
|
||||||
|
str = self.get_string()
|
||||||
|
l = string.split(str, ',')
|
||||||
|
return l
|
||||||
|
|
||||||
|
def add_bytes(self, b):
|
||||||
|
self.packet = self.packet + b
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_byte(self, b):
|
||||||
|
self.packet = self.packet + b
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_boolean(self, b):
|
||||||
|
if b:
|
||||||
|
self.add_byte('\x01')
|
||||||
|
else:
|
||||||
|
self.add_byte('\x00')
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_int(self, n):
|
||||||
|
self.packet = self.packet + struct.pack('>I', n)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_mpint(self, z):
|
||||||
|
"this only works on positive numbers"
|
||||||
|
self.add_string(deflate_long(z))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_string(self, s):
|
||||||
|
self.add_int(len(s))
|
||||||
|
self.packet = self.packet + s
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_list(self, l):
|
||||||
|
out = string.join(l, ',')
|
||||||
|
self.add_int(len(out))
|
||||||
|
self.packet = self.packet + out
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add(self, i):
|
||||||
|
if type(i) == types.StringType:
|
||||||
|
return self.add_string(i)
|
||||||
|
elif type(i) == types.IntType:
|
||||||
|
return self.add_int(i)
|
||||||
|
elif type(i) == types.LongType:
|
||||||
|
if i > 0xffffffffL:
|
||||||
|
return self.add_mpint(i)
|
||||||
|
else:
|
||||||
|
return self.add_int(i)
|
||||||
|
elif type(i) == types.ListType:
|
||||||
|
return self.add_list(i)
|
||||||
|
else:
|
||||||
|
raise exception('Unknown type')
|
|
@ -0,0 +1,102 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
from message import Message
|
||||||
|
from transport import MSG_USERAUTH_REQUEST
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Hash import SHA
|
||||||
|
from ber import BER
|
||||||
|
from util import format_binary, inflate_long, deflate_long
|
||||||
|
import base64
|
||||||
|
|
||||||
|
class RSAKey(object):
|
||||||
|
|
||||||
|
def __init__(self, msg=None):
|
||||||
|
self.valid = 0
|
||||||
|
if (msg == None) or (msg.get_string() != 'ssh-rsa'):
|
||||||
|
return
|
||||||
|
self.e = msg.get_mpint()
|
||||||
|
self.n = msg.get_mpint()
|
||||||
|
self.size = len(deflate_long(self.n, 0))
|
||||||
|
self.valid = 1
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if not self.valid:
|
||||||
|
return ''
|
||||||
|
m = Message()
|
||||||
|
m.add_string('ssh-rsa')
|
||||||
|
m.add_mpint(self.e)
|
||||||
|
m.add_mpint(self.n)
|
||||||
|
return str(m)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return 'ssh-rsa'
|
||||||
|
|
||||||
|
def pkcs1imify(self, data):
|
||||||
|
"""
|
||||||
|
turn a 20-byte SHA1 hash into a blob of data as large as the key's N,
|
||||||
|
using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre.
|
||||||
|
"""
|
||||||
|
SHA1_DIGESTINFO = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
|
||||||
|
filler = '\xff' * (self.size - len(SHA1_DIGESTINFO) - len(data) - 3)
|
||||||
|
return '\x00\x01' + filler + '\x00' + SHA1_DIGESTINFO + data
|
||||||
|
|
||||||
|
def verify_ssh_sig(self, data, msg):
|
||||||
|
if (not self.valid) or (msg.get_string() != 'ssh-rsa'):
|
||||||
|
return 0
|
||||||
|
sig = inflate_long(msg.get_string(), 1)
|
||||||
|
# verify the signature by SHA'ing the data and encrypting it using the
|
||||||
|
# public key. some wackiness ensues where we "pkcs1imify" the 20-byte
|
||||||
|
# hash into a string as long as the RSA key.
|
||||||
|
hash = inflate_long(self.pkcs1imify(SHA.new(data).digest()), 1)
|
||||||
|
rsa = RSA.construct((long(self.n), long(self.e)))
|
||||||
|
return rsa.verify(hash, (sig,))
|
||||||
|
|
||||||
|
def sign_ssh_data(self, data):
|
||||||
|
hash = SHA.new(data).digest()
|
||||||
|
rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
|
||||||
|
sig = deflate_long(rsa.sign(self.pkcs1imify(hash), '')[0], 0)
|
||||||
|
m = Message()
|
||||||
|
m.add_string('ssh-rsa')
|
||||||
|
m.add_string(sig)
|
||||||
|
return str(m)
|
||||||
|
|
||||||
|
def read_private_key_file(self, filename):
|
||||||
|
# private key file contains:
|
||||||
|
# RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p }
|
||||||
|
self.valid = 0
|
||||||
|
try:
|
||||||
|
f = open(filename, 'r')
|
||||||
|
lines = f.readlines()
|
||||||
|
f.close()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
if lines[0].strip() != '-----BEGIN RSA PRIVATE KEY-----':
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = base64.decodestring(''.join(lines[1:-1]))
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
keylist = BER(data).decode()
|
||||||
|
if (type(keylist) != type([])) or (len(keylist) < 4) or (keylist[0] != 0):
|
||||||
|
return
|
||||||
|
self.n = keylist[1]
|
||||||
|
self.e = keylist[2]
|
||||||
|
self.d = keylist[3]
|
||||||
|
# not really needed
|
||||||
|
self.p = keylist[4]
|
||||||
|
self.q = keylist[5]
|
||||||
|
self.size = len(deflate_long(self.n, 0))
|
||||||
|
self.valid = 1
|
||||||
|
|
||||||
|
def sign_ssh_session(self, randpool, sid, username):
|
||||||
|
m = Message()
|
||||||
|
m.add_string(sid)
|
||||||
|
m.add_byte(chr(MSG_USERAUTH_REQUEST))
|
||||||
|
m.add_string(username)
|
||||||
|
m.add_string('ssh-connection')
|
||||||
|
m.add_string('publickey')
|
||||||
|
m.add_boolean(1)
|
||||||
|
m.add_string('ssh-rsa')
|
||||||
|
m.add_string(str(self))
|
||||||
|
return self.sign_ssh_data(str(m))
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if (sys.version_info[0] < 2) or ((sys.version_info[0] == 2) and (sys.version_info[1] < 3)):
|
||||||
|
raise RuntimeError('You need python 2.3 for this module.')
|
||||||
|
|
||||||
|
# FIXME rename
|
||||||
|
class SSHException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
from auth_transport import Transport
|
||||||
|
from channel import Channel
|
||||||
|
from rsakey import RSAKey
|
||||||
|
from dsskey import DSSKey
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = "Robey Pointer <robey@lag.net>"
|
||||||
|
__date__ = "18 Sep 2003"
|
||||||
|
__version__ = "0.1-bulbasaur"
|
||||||
|
__credits__ = "Huzzah!"
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
longdesc = '''
|
||||||
|
This is a library for making client-side SSH2 connections (server-side is
|
||||||
|
coming soon). All major ciphers and hash methods are supported.
|
||||||
|
|
||||||
|
Required packages:
|
||||||
|
pyCrypto
|
||||||
|
'''
|
||||||
|
|
||||||
|
setup(name = "secsh",
|
||||||
|
version = "0.1-bulbasaur",
|
||||||
|
description = "SSH2 protocol library",
|
||||||
|
author = "Robey Pointer",
|
||||||
|
author_email = "robey@lag.net",
|
||||||
|
url = "http://www.lag.net/~robey/secsh/",
|
||||||
|
py_modules = [ 'secsh', 'transport', 'channel', 'message', 'util', 'ber',
|
||||||
|
'kex_group1', 'kex_gex', 'rsakey', 'dsskey' ],
|
||||||
|
scripts = [ 'demo.py' ],
|
||||||
|
download_url = 'http://www.lag.net/~robey/secsh/secsh-0.1-bulbasaur.zip',
|
||||||
|
license = 'LGPL',
|
||||||
|
platforms = 'Posix; MacOS X; Windows',
|
||||||
|
classifiers = [ 'Development Status :: 3 - Alpha',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Topic :: Internet',
|
||||||
|
'Topic :: Security :: Cryptography' ],
|
||||||
|
long_description = longdesc,
|
||||||
|
)
|
|
@ -0,0 +1,758 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_SERVICE_REQUEST, \
|
||||||
|
MSG_SERVICE_ACCEPT = range(1, 7)
|
||||||
|
MSG_KEXINIT, MSG_NEWKEYS = range(20, 22)
|
||||||
|
MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \
|
||||||
|
MSG_USERAUTH_BANNER = range(50, 54)
|
||||||
|
MSG_USERAUTH_PK_OK = 60
|
||||||
|
MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \
|
||||||
|
MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \
|
||||||
|
MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \
|
||||||
|
MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101)
|
||||||
|
|
||||||
|
|
||||||
|
import sys, os, string, threading, socket, logging, struct
|
||||||
|
from message import Message
|
||||||
|
from channel import Channel
|
||||||
|
from secsh import SSHException
|
||||||
|
from util import format_binary, safe_string, inflate_long, deflate_long
|
||||||
|
from rsakey import RSAKey
|
||||||
|
from dsskey import DSSKey
|
||||||
|
from kex_group1 import KexGroup1
|
||||||
|
from kex_gex import KexGex
|
||||||
|
|
||||||
|
# these come from PyCrypt
|
||||||
|
# http://www.amk.ca/python/writing/pycrypt/
|
||||||
|
# i believe this on the standards track.
|
||||||
|
# PyCrypt compiled for Win32 can be downloaded from the HashTar homepage:
|
||||||
|
# http://nitace.bsd.uchicago.edu:8080/hashtar
|
||||||
|
from Crypto.Util.randpool import PersistentRandomPool, RandomPool
|
||||||
|
from Crypto.Cipher import Blowfish, AES, DES3
|
||||||
|
from Crypto.Hash import SHA, MD5, HMAC
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
|
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
|
||||||
|
|
||||||
|
# channel request failed reasons:
|
||||||
|
CONNECTION_FAILED_CODE = {
|
||||||
|
1: 'Administratively prohibited',
|
||||||
|
2: 'Connect failed',
|
||||||
|
3: 'Unknown channel type',
|
||||||
|
4: 'Resource shortage'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# keep a crypto-strong PRNG nearby
|
||||||
|
try:
|
||||||
|
randpool = PersistentRandomPool(os.getenv('HOME') + '/.randpool')
|
||||||
|
except:
|
||||||
|
# the above will likely fail on Windows - fall back to non-persistent random pool
|
||||||
|
randpool = RandomPool()
|
||||||
|
|
||||||
|
randpool.randomize()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTransport(threading.Thread):
|
||||||
|
'''
|
||||||
|
An SSH Transport attaches to a stream (usually a socket), negotiates an
|
||||||
|
encrypted session, authenticates, and then creates stream tunnels, called
|
||||||
|
"channels", across the session. Multiple channels can be multiplexed
|
||||||
|
across a single session (and often are, in the case of port forwardings).
|
||||||
|
|
||||||
|
Transport expects to receive a "socket-like object" to talk to the SSH
|
||||||
|
server. This means it has a method "settimeout" which sets a timeout for
|
||||||
|
read/write calls, and a method "send()" to write bytes and "recv()" to
|
||||||
|
read bytes. "recv" returns from 1 to n bytes, or 0 if the stream has been
|
||||||
|
closed. EOFError may also be raised on a closed stream. (A return value
|
||||||
|
of 0 is converted to an EOFError internally.) "send(s)" writes from 1 to
|
||||||
|
len(s) bytes, and returns the number of bytes written, or returns 0 if the
|
||||||
|
stream has been closed. As with instream, EOFError may be raised instead
|
||||||
|
of returning 0.
|
||||||
|
|
||||||
|
FIXME: Describe events here.
|
||||||
|
'''
|
||||||
|
|
||||||
|
PROTO_ID = '2.0'
|
||||||
|
CLIENT_ID = 'pyssh_1.1'
|
||||||
|
|
||||||
|
preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ]
|
||||||
|
preferred_macs = [ 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ]
|
||||||
|
preferred_keys = [ 'ssh-rsa', 'ssh-dss' ]
|
||||||
|
preferred_kex = [ 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ]
|
||||||
|
|
||||||
|
cipher_info = {
|
||||||
|
'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 },
|
||||||
|
'aes128-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16 },
|
||||||
|
'aes256-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32 },
|
||||||
|
'3des-cbc': { 'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24 },
|
||||||
|
}
|
||||||
|
|
||||||
|
mac_info = {
|
||||||
|
'hmac-sha1': { 'class': SHA, 'size': 20 },
|
||||||
|
'hmac-sha1-96': { 'class': SHA, 'size': 12 },
|
||||||
|
'hmac-md5': { 'class': MD5, 'size': 16 },
|
||||||
|
'hmac-md5-96': { 'class': MD5, 'size': 12 },
|
||||||
|
}
|
||||||
|
|
||||||
|
kex_info = {
|
||||||
|
'diffie-hellman-group1-sha1': KexGroup1,
|
||||||
|
'diffie-hellman-group-exchange-sha1': KexGex,
|
||||||
|
}
|
||||||
|
|
||||||
|
REKEY_PACKETS = pow(2, 30)
|
||||||
|
REKEY_BYTES = pow(2, 30)
|
||||||
|
|
||||||
|
def __init__(self, sock):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
self.randpool = randpool
|
||||||
|
self.sock = sock
|
||||||
|
self.sock.settimeout(0.1)
|
||||||
|
# negotiated crypto parameters
|
||||||
|
self.local_version = 'SSH-' + self.PROTO_ID + '-' + self.CLIENT_ID
|
||||||
|
self.remote_version = ''
|
||||||
|
self.block_size_out = self.block_size_in = 8
|
||||||
|
self.local_mac_len = self.remote_mac_len = 0
|
||||||
|
self.engine_in = self.engine_out = None
|
||||||
|
self.local_cipher = self.remote_cipher = ''
|
||||||
|
self.sequence_number_in = self.sequence_number_out = 0L
|
||||||
|
self.local_kex_init = self.remote_kex_init = None
|
||||||
|
self.session_id = None
|
||||||
|
# /negotiated crypto parameters
|
||||||
|
self.expected_packet = 0
|
||||||
|
self.active = 0
|
||||||
|
self.initial_kex_done = 0
|
||||||
|
self.write_lock = threading.Lock() # lock around outbound writes (packet computation)
|
||||||
|
self.lock = threading.Lock() # synchronization (always higher level than write_lock)
|
||||||
|
self.authenticated = 0
|
||||||
|
self.channels = { } # (id -> Channel)
|
||||||
|
self.channel_events = { } # (id -> Event)
|
||||||
|
self.channel_counter = 1
|
||||||
|
self.logger = logging.getLogger('secsh.transport')
|
||||||
|
self.window_size = 65536
|
||||||
|
self.max_packet_size = 2048
|
||||||
|
self.ultra_debug = 0
|
||||||
|
# used for noticing when to re-key:
|
||||||
|
self.received_bytes = 0
|
||||||
|
self.received_packets = 0
|
||||||
|
self.received_packets_overflow = 0
|
||||||
|
# user-defined event callbacks:
|
||||||
|
self.completion_event = None
|
||||||
|
# server mode:
|
||||||
|
self.server_mode = 0
|
||||||
|
self.server_key_dict = { }
|
||||||
|
|
||||||
|
def start_client(self, event=None):
|
||||||
|
self.completion_event = event
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def start_server(self, event=None):
|
||||||
|
self.server_mode = 1
|
||||||
|
self.completion_event = event
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def add_server_key(self, key):
|
||||||
|
self.server_key_dict[key.get_name()] = key
|
||||||
|
|
||||||
|
def get_server_key(self):
|
||||||
|
try:
|
||||||
|
return self.server_key_dict[self.host_key_type]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if not self.active:
|
||||||
|
return '<secsh.Transport (unconnected)>'
|
||||||
|
out = '<sesch.Transport'
|
||||||
|
#if self.remote_version != '':
|
||||||
|
# out += ' (server version "%s")' % self.remote_version
|
||||||
|
if self.local_cipher != '':
|
||||||
|
out += ' (cipher %s)' % self.local_cipher
|
||||||
|
if self.authenticated:
|
||||||
|
if len(self.channels) == 1:
|
||||||
|
out += ' (active; 1 open channel)'
|
||||||
|
else:
|
||||||
|
out += ' (active; %d open channels)' % len(self.channels)
|
||||||
|
elif self.initial_kex_done:
|
||||||
|
out += ' (connected; awaiting auth)'
|
||||||
|
else:
|
||||||
|
out += ' (connecting)'
|
||||||
|
out += '>'
|
||||||
|
return out
|
||||||
|
|
||||||
|
def log(self, level, msg):
|
||||||
|
if type(msg) == type([]):
|
||||||
|
for m in msg:
|
||||||
|
self.logger.log(level, m)
|
||||||
|
else:
|
||||||
|
self.logger.log(level, msg)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.active = 0
|
||||||
|
self.engine_in = self.engine_out = None
|
||||||
|
self.sequence_number_in = self.sequence_number_out = 0L
|
||||||
|
for chan in self.channels.values():
|
||||||
|
chan.unlink()
|
||||||
|
|
||||||
|
def get_host_key(self):
|
||||||
|
'returns (type, key) where type is like "ssh-rsa" and key is an opaque string'
|
||||||
|
if (not self.active) or (not self.initial_kex_done):
|
||||||
|
raise SSHException('No existing session')
|
||||||
|
key_msg = Message(self.host_key)
|
||||||
|
key_type = key_msg.get_string()
|
||||||
|
return key_type, self.host_key
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return self.active
|
||||||
|
|
||||||
|
def is_authenticated(self):
|
||||||
|
return self.authenticated and self.active
|
||||||
|
|
||||||
|
def open_session(self):
|
||||||
|
return self.open_channel('session')
|
||||||
|
|
||||||
|
def open_channel(self, kind):
|
||||||
|
chan = None
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
chanid = self.channel_counter
|
||||||
|
self.channel_counter += 1
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_CHANNEL_OPEN))
|
||||||
|
m.add_string(kind)
|
||||||
|
m.add_int(chanid)
|
||||||
|
m.add_int(self.window_size)
|
||||||
|
m.add_int(self.max_packet_size)
|
||||||
|
self.channels[chanid] = chan = Channel(chanid, self)
|
||||||
|
self.channel_events[chanid] = event = threading.Event()
|
||||||
|
chan.set_window(self.window_size, self.max_packet_size)
|
||||||
|
self.send_message(m)
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
while 1:
|
||||||
|
event.wait(0.1);
|
||||||
|
if not self.active:
|
||||||
|
return None
|
||||||
|
if event.isSet():
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
if not self.channels.has_key(chanid):
|
||||||
|
chan = None
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
return chan
|
||||||
|
|
||||||
|
def unlink_channel(self, chanid):
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
if self.channels.has_key(chanid):
|
||||||
|
del self.channels[chanid]
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def read_all(self, n):
|
||||||
|
out = ''
|
||||||
|
while n > 0:
|
||||||
|
try:
|
||||||
|
x = self.sock.recv(n)
|
||||||
|
if len(x) == 0:
|
||||||
|
raise EOFError()
|
||||||
|
out += x
|
||||||
|
n -= len(x)
|
||||||
|
except socket.timeout:
|
||||||
|
if not self.active:
|
||||||
|
raise EOFError()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def write_all(self, out):
|
||||||
|
while len(out) > 0:
|
||||||
|
n = self.sock.send(out)
|
||||||
|
if n <= 0:
|
||||||
|
raise EOFError()
|
||||||
|
if n == len(out):
|
||||||
|
return
|
||||||
|
out = out[n:]
|
||||||
|
return
|
||||||
|
|
||||||
|
def build_packet(self, payload):
|
||||||
|
# pad up at least 4 bytes, to nearest block-size (usually 8)
|
||||||
|
bsize = self.block_size_out
|
||||||
|
padding = 3 + bsize - ((len(payload) + 8) % bsize)
|
||||||
|
packet = struct.pack('>I', len(payload) + padding + 1)
|
||||||
|
packet += chr(padding)
|
||||||
|
packet += payload
|
||||||
|
packet += randpool.get_bytes(padding)
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def send_message(self, data):
|
||||||
|
# encrypt this sucka
|
||||||
|
packet = self.build_packet(str(data))
|
||||||
|
if self.ultra_debug:
|
||||||
|
self.log(DEBUG, format_binary(packet, 'OUT: '))
|
||||||
|
if self.engine_out != None:
|
||||||
|
out = self.engine_out.encrypt(packet)
|
||||||
|
else:
|
||||||
|
out = packet
|
||||||
|
# + mac
|
||||||
|
try:
|
||||||
|
self.write_lock.acquire()
|
||||||
|
if self.engine_out != None:
|
||||||
|
payload = struct.pack('>I', self.sequence_number_out) + packet
|
||||||
|
out += HMAC.HMAC(self.mac_key_out, payload, self.local_mac_engine).digest()[:self.local_mac_len]
|
||||||
|
self.sequence_number_out += 1L
|
||||||
|
self.sequence_number_out %= 0x100000000L
|
||||||
|
self.write_all(out)
|
||||||
|
finally:
|
||||||
|
self.write_lock.release()
|
||||||
|
|
||||||
|
def read_message(self):
|
||||||
|
"only one thread will ever be in this function"
|
||||||
|
header = self.read_all(self.block_size_in)
|
||||||
|
if self.engine_in != None:
|
||||||
|
header = self.engine_in.decrypt(header)
|
||||||
|
if self.ultra_debug:
|
||||||
|
self.log(DEBUG, format_binary(header, 'IN: '));
|
||||||
|
packet_size = struct.unpack('>I', header[:4])[0]
|
||||||
|
# leftover contains decrypted bytes from the first block (after the length field)
|
||||||
|
leftover = header[4:]
|
||||||
|
if (packet_size - len(leftover)) % self.block_size_in != 0:
|
||||||
|
raise SSHException('Invalid packet blocking')
|
||||||
|
buffer = self.read_all(packet_size + self.remote_mac_len - len(leftover))
|
||||||
|
packet = buffer[:packet_size - len(leftover)]
|
||||||
|
post_packet = buffer[packet_size - len(leftover):]
|
||||||
|
if self.engine_in != None:
|
||||||
|
packet = self.engine_in.decrypt(packet)
|
||||||
|
if self.ultra_debug:
|
||||||
|
self.log(DEBUG, format_binary(packet, 'IN: '));
|
||||||
|
packet = leftover + packet
|
||||||
|
if self.remote_mac_len > 0:
|
||||||
|
mac = post_packet[:self.remote_mac_len]
|
||||||
|
mac_payload = struct.pack('>II', self.sequence_number_in, packet_size) + packet
|
||||||
|
my_mac = HMAC.HMAC(self.mac_key_in, mac_payload, self.remote_mac_engine).digest()[:self.remote_mac_len]
|
||||||
|
if my_mac != mac:
|
||||||
|
raise SSHException('Mismatched MAC')
|
||||||
|
padding = ord(packet[0])
|
||||||
|
payload = packet[1:packet_size - padding + 1]
|
||||||
|
randpool.add_event(packet[packet_size - padding + 1])
|
||||||
|
#self.log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding))
|
||||||
|
msg = Message(payload[1:])
|
||||||
|
msg.seqno = self.sequence_number_in
|
||||||
|
self.sequence_number_in = (self.sequence_number_in + 1) & 0xffffffffL
|
||||||
|
# check for rekey
|
||||||
|
self.received_bytes += packet_size + self.remote_mac_len + 4
|
||||||
|
self.received_packets += 1
|
||||||
|
if (self.received_packets >= self.REKEY_PACKETS) or (self.received_bytes >= self.REKEY_BYTES):
|
||||||
|
# only ask once for rekeying
|
||||||
|
if self.local_kex_init is None:
|
||||||
|
self.log(DEBUG, 'Rekeying (hit %d packets, %d bytes)' % (self.received_packets,
|
||||||
|
self.received_bytes))
|
||||||
|
self.received_packets_overflow = 0
|
||||||
|
self.send_kex_init()
|
||||||
|
else:
|
||||||
|
# we've asked to rekey already -- give them 20 packets to
|
||||||
|
# comply, then just drop the connection
|
||||||
|
self.received_packets_overflow += 1
|
||||||
|
if self.received_packets_overflow >= 20:
|
||||||
|
raise SSHException('Remote transport is ignoring rekey requests')
|
||||||
|
|
||||||
|
return ord(payload[0]), msg
|
||||||
|
|
||||||
|
def set_K_H(self, k, h):
|
||||||
|
"used by a kex object to set the K (root key) and H (exchange hash)"
|
||||||
|
self.K = k
|
||||||
|
self.H = h
|
||||||
|
if self.session_id == None:
|
||||||
|
self.session_id = h
|
||||||
|
|
||||||
|
def verify_key(self, host_key, sig):
|
||||||
|
if self.host_key_type == 'ssh-rsa':
|
||||||
|
key = RSAKey(Message(host_key))
|
||||||
|
elif self.host_key_type == 'ssh-dss':
|
||||||
|
key = DSSKey(Message(host_key))
|
||||||
|
else:
|
||||||
|
key = None
|
||||||
|
if (key == None) or not key.valid:
|
||||||
|
raise SSHException('Unknown host key type')
|
||||||
|
if not key.verify_ssh_sig(self.H, Message(sig)):
|
||||||
|
raise SSHException('Signature verification (%s) failed. Boo. Robey should debug this.' % self.host_key_type)
|
||||||
|
self.host_key = host_key
|
||||||
|
|
||||||
|
def compute_key(self, id, nbytes):
|
||||||
|
"id is 'A' - 'F' for the various keys used by ssh"
|
||||||
|
m = Message()
|
||||||
|
m.add_mpint(self.K)
|
||||||
|
m.add_bytes(self.H)
|
||||||
|
m.add_byte(id)
|
||||||
|
m.add_bytes(self.session_id)
|
||||||
|
out = sofar = SHA.new(str(m)).digest()
|
||||||
|
while len(out) < nbytes:
|
||||||
|
m = Message()
|
||||||
|
m.add_mpint(self.K)
|
||||||
|
m.add_bytes(self.H)
|
||||||
|
m.add_bytes(sofar)
|
||||||
|
hash = SHA.new(str(m)).digest()
|
||||||
|
out += hash
|
||||||
|
sofar += hash
|
||||||
|
return out[:nbytes]
|
||||||
|
|
||||||
|
def get_cipher(self, name, key, iv):
|
||||||
|
if not self.cipher_info.has_key(name):
|
||||||
|
raise SSHException('Unknown client cipher ' + name)
|
||||||
|
return self.cipher_info[name]['class'].new(key, self.cipher_info[name]['mode'], iv)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.active = 1
|
||||||
|
try:
|
||||||
|
# SSH-1.99-OpenSSH_2.9p2
|
||||||
|
self.write_all(self.local_version + '\r\n')
|
||||||
|
self.check_banner()
|
||||||
|
self.send_kex_init()
|
||||||
|
self.expected_packet = MSG_KEXINIT
|
||||||
|
|
||||||
|
while self.active:
|
||||||
|
ptype, m = self.read_message()
|
||||||
|
if ptype == MSG_IGNORE:
|
||||||
|
continue
|
||||||
|
elif ptype == MSG_DISCONNECT:
|
||||||
|
self.parse_disconnect(m)
|
||||||
|
self.active = 0
|
||||||
|
break
|
||||||
|
elif ptype == MSG_DEBUG:
|
||||||
|
self.parse_debug(m)
|
||||||
|
continue
|
||||||
|
if self.expected_packet != 0:
|
||||||
|
if ptype != self.expected_packet:
|
||||||
|
raise SSHException('Expecting packet %d, got %d' % (self.expected_packet, ptype))
|
||||||
|
self.expected_packet = 0
|
||||||
|
if (ptype >= 30) and (ptype <= 39):
|
||||||
|
self.kex_engine.parse_next(ptype, m)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.handler_table.has_key(ptype):
|
||||||
|
self.handler_table[ptype](self, m)
|
||||||
|
elif self.channel_handler_table.has_key(ptype):
|
||||||
|
chanid = m.get_int()
|
||||||
|
if self.channels.has_key(chanid):
|
||||||
|
self.channel_handler_table[ptype](self.channels[chanid], m)
|
||||||
|
else:
|
||||||
|
self.log(WARNING, 'Oops, unhandled type %d' % ptype)
|
||||||
|
msg = Message()
|
||||||
|
msg.add_byte(chr(MSG_UNIMPLEMENTED))
|
||||||
|
msg.add_int(m.seqno)
|
||||||
|
self.send_message(msg)
|
||||||
|
except SSHException, e:
|
||||||
|
self.log(DEBUG, 'Exception: ' + str(e))
|
||||||
|
except EOFError, e:
|
||||||
|
self.log(DEBUG, 'EOF')
|
||||||
|
except Exception, e:
|
||||||
|
self.log(DEBUG, 'Unknown exception: ' + str(e))
|
||||||
|
if self.active:
|
||||||
|
self.active = 0
|
||||||
|
if self.completion_event != None:
|
||||||
|
self.completion_event.set()
|
||||||
|
if self.auth_event != None:
|
||||||
|
self.auth_event.set()
|
||||||
|
for e in self.channel_events.values():
|
||||||
|
e.set()
|
||||||
|
self.sock.close()
|
||||||
|
|
||||||
|
### protocol stages
|
||||||
|
|
||||||
|
def renegotiate_keys(self):
|
||||||
|
self.completion_event = threading.Event()
|
||||||
|
self.send_kex_init()
|
||||||
|
while 1:
|
||||||
|
self.completion_event.wait(0.1);
|
||||||
|
if not self.active:
|
||||||
|
return 0
|
||||||
|
if self.completion_event.isSet():
|
||||||
|
break
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def negotiate_keys(self, m):
|
||||||
|
# throws SSHException on anything unusual
|
||||||
|
if self.local_kex_init == None:
|
||||||
|
# remote side wants to renegotiate
|
||||||
|
self.send_kex_init()
|
||||||
|
self.parse_kex_init(m)
|
||||||
|
self.kex_engine.start_kex()
|
||||||
|
|
||||||
|
def check_banner(self):
|
||||||
|
# this is slow, but we only have to do it once
|
||||||
|
for i in range(5):
|
||||||
|
buffer = ''
|
||||||
|
while not '\n' in buffer:
|
||||||
|
buffer += self.read_all(1)
|
||||||
|
buffer = buffer[:-1]
|
||||||
|
if (len(buffer) > 0) and (buffer[-1] == '\r'):
|
||||||
|
buffer = buffer[:-1]
|
||||||
|
if buffer[:4] == 'SSH-':
|
||||||
|
break
|
||||||
|
self.log(DEBUG, 'Banner: ' + buffer)
|
||||||
|
if buffer[:4] != 'SSH-':
|
||||||
|
raise SSHException('Indecipherable protocol version "' + buffer + '"')
|
||||||
|
# save this server version string for later
|
||||||
|
self.remote_version = buffer
|
||||||
|
# pull off any attached comment
|
||||||
|
comment = ''
|
||||||
|
i = string.find(buffer, ' ')
|
||||||
|
if i >= 0:
|
||||||
|
comment = buffer[i+1:]
|
||||||
|
buffer = buffer[:i]
|
||||||
|
# parse out version string and make sure it matches
|
||||||
|
_unused, version, client = string.split(buffer, '-')
|
||||||
|
if version != '1.99' and version != '2.0':
|
||||||
|
raise SSHException('Incompatible version (%s instead of 2.0)' % (version,))
|
||||||
|
self.log(INFO, 'Connected (version %s, client %s)' % (version, client))
|
||||||
|
|
||||||
|
def send_kex_init(self):
|
||||||
|
# send a really wimpy kex-init packet that says we're a bare-bones ssh client
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_KEXINIT))
|
||||||
|
m.add_bytes(randpool.get_bytes(16))
|
||||||
|
m.add(','.join(self.preferred_kex))
|
||||||
|
m.add(','.join(self.preferred_keys))
|
||||||
|
m.add(','.join(self.preferred_ciphers))
|
||||||
|
m.add(','.join(self.preferred_ciphers))
|
||||||
|
m.add(','.join(self.preferred_macs))
|
||||||
|
m.add(','.join(self.preferred_macs))
|
||||||
|
m.add('none')
|
||||||
|
m.add('none')
|
||||||
|
m.add('')
|
||||||
|
m.add('')
|
||||||
|
m.add_boolean(0)
|
||||||
|
m.add_int(0)
|
||||||
|
# save a copy for later (needed to compute a hash)
|
||||||
|
self.local_kex_init = str(m)
|
||||||
|
self.send_message(m)
|
||||||
|
|
||||||
|
def parse_kex_init(self, m):
|
||||||
|
# reset counters of when to re-key, since we are now re-keying
|
||||||
|
self.received_bytes = 0
|
||||||
|
self.received_packets = 0
|
||||||
|
self.received_packets_overflow = 0
|
||||||
|
|
||||||
|
cookie = m.get_bytes(16)
|
||||||
|
kex_algo_list = m.get_list()
|
||||||
|
server_key_algo_list = m.get_list()
|
||||||
|
client_encrypt_algo_list = m.get_list()
|
||||||
|
server_encrypt_algo_list = m.get_list()
|
||||||
|
client_mac_algo_list = m.get_list()
|
||||||
|
server_mac_algo_list = m.get_list()
|
||||||
|
client_compress_algo_list = m.get_list()
|
||||||
|
server_compress_algo_list = m.get_list()
|
||||||
|
client_lang_list = m.get_list()
|
||||||
|
server_lang_list = m.get_list()
|
||||||
|
kex_follows = m.get_boolean()
|
||||||
|
unused = m.get_int()
|
||||||
|
|
||||||
|
# no compression support (yet?)
|
||||||
|
if (not('none' in client_compress_algo_list) or
|
||||||
|
not('none' in server_compress_algo_list)):
|
||||||
|
raise SSHException('Incompatible ssh peer.')
|
||||||
|
|
||||||
|
# as a server, we pick the first item in the client's list that we support.
|
||||||
|
# as a client, we pick the first item in our list that the server supports.
|
||||||
|
if self.server_mode:
|
||||||
|
agreed_kex = filter(self.preferred_kex.__contains__, kex_algo_list)
|
||||||
|
else:
|
||||||
|
agreed_kex = filter(kex_algo_list.__contains__, self.preferred_kex)
|
||||||
|
if len(agreed_kex) == 0:
|
||||||
|
raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)')
|
||||||
|
self.kex_engine = self.kex_info[agreed_kex[0]](self)
|
||||||
|
|
||||||
|
if self.server_mode:
|
||||||
|
agreed_keys = filter(self.preferred_keys.__contains__, server_key_algo_list)
|
||||||
|
else:
|
||||||
|
agreed_keys = filter(server_key_algo_list.__contains__, self.preferred_keys)
|
||||||
|
if len(agreed_keys) == 0:
|
||||||
|
raise SSHException('Incompatible ssh peer (no acceptable host key)')
|
||||||
|
self.host_key_type = agreed_keys[0]
|
||||||
|
if self.server_mode and (self.get_server_key() is None):
|
||||||
|
raise SSHException('Incompatible ssh peer (can\'t match requested host key type)')
|
||||||
|
|
||||||
|
if self.server_mode:
|
||||||
|
agreed_local_ciphers = filter(self.preferred_ciphers.__contains__,
|
||||||
|
server_encrypt_algo_list)
|
||||||
|
agreed_remote_ciphers = filter(self.preferred_ciphers.__contains__,
|
||||||
|
client_encrypt_algo_list)
|
||||||
|
else:
|
||||||
|
agreed_local_ciphers = filter(client_encrypt_algo_list.__contains__,
|
||||||
|
self.preferred_ciphers)
|
||||||
|
agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__,
|
||||||
|
self.preferred_ciphers)
|
||||||
|
if (len(agreed_local_ciphers) == 0) or (len(agreed_remote_ciphers) == 0):
|
||||||
|
raise SSHException('Incompatible ssh server (no acceptable ciphers)')
|
||||||
|
self.local_cipher = agreed_local_ciphers[0]
|
||||||
|
self.remote_cipher = agreed_remote_ciphers[0]
|
||||||
|
self.log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher))
|
||||||
|
|
||||||
|
if self.server_mode:
|
||||||
|
agreed_remote_macs = filter(self.preferred_macs.__contains__, client_mac_algo_list)
|
||||||
|
agreed_local_macs = filter(self.preferred_macs.__contains__, server_mac_algo_list)
|
||||||
|
else:
|
||||||
|
agreed_local_macs = filter(client_mac_algo_list.__contains__, self.preferred_macs)
|
||||||
|
agreed_remote_macs = filter(server_mac_algo_list.__contains__, self.preferred_macs)
|
||||||
|
if (len(agreed_local_macs) == 0) or (len(agreed_remote_macs) == 0):
|
||||||
|
raise SSHException('Incompatible ssh server (no acceptable macs)')
|
||||||
|
self.local_mac = agreed_local_macs[0]
|
||||||
|
self.remote_mac = agreed_remote_macs[0]
|
||||||
|
|
||||||
|
self.log(DEBUG, 'kex algos:' + str(kex_algo_list) + ' server key:' + str(server_key_algo_list) + \
|
||||||
|
' client encrypt:' + str(client_encrypt_algo_list) + \
|
||||||
|
' server encrypt:' + str(server_encrypt_algo_list) + \
|
||||||
|
' client mac:' + str(client_mac_algo_list) + \
|
||||||
|
' server mac:' + str(server_mac_algo_list) + \
|
||||||
|
' client compress:' + str(client_compress_algo_list) + \
|
||||||
|
' server compress:' + str(server_compress_algo_list) + \
|
||||||
|
' client lang:' + str(client_lang_list) + \
|
||||||
|
' server lang:' + str(server_lang_list) + \
|
||||||
|
' kex follows?' + str(kex_follows))
|
||||||
|
self.log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s' %
|
||||||
|
(agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac,
|
||||||
|
self.remote_mac))
|
||||||
|
|
||||||
|
# save for computing hash later...
|
||||||
|
# now wait! openssh has a bug (and others might too) where there are
|
||||||
|
# actually some extra bytes (one NUL byte in openssh's case) added to
|
||||||
|
# the end of the packet but not parsed. turns out we need to throw
|
||||||
|
# away those bytes because they aren't part of the hash.
|
||||||
|
self.remote_kex_init = chr(MSG_KEXINIT) + m.get_so_far()
|
||||||
|
|
||||||
|
def activate_inbound(self):
|
||||||
|
"switch on newly negotiated encryption parameters for inbound traffic"
|
||||||
|
self.block_size_in = self.cipher_info[self.remote_cipher]['block-size']
|
||||||
|
if self.server_mode:
|
||||||
|
IV_in = self.compute_key('A', self.block_size_in)
|
||||||
|
key_in = self.compute_key('C', self.cipher_info[self.remote_cipher]['key-size'])
|
||||||
|
else:
|
||||||
|
IV_in = self.compute_key('B', self.block_size_in)
|
||||||
|
key_in = self.compute_key('D', self.cipher_info[self.remote_cipher]['key-size'])
|
||||||
|
self.engine_in = self.get_cipher(self.remote_cipher, key_in, IV_in)
|
||||||
|
self.remote_mac_len = self.mac_info[self.remote_mac]['size']
|
||||||
|
self.remote_mac_engine = self.mac_info[self.remote_mac]['class']
|
||||||
|
# initial mac keys are done in the hash's natural size (not the potentially truncated
|
||||||
|
# transmission size)
|
||||||
|
if self.server_mode:
|
||||||
|
self.mac_key_in = self.compute_key('E', self.remote_mac_engine.digest_size)
|
||||||
|
else:
|
||||||
|
self.mac_key_in = self.compute_key('F', self.remote_mac_engine.digest_size)
|
||||||
|
|
||||||
|
def activate_outbound(self):
|
||||||
|
"switch on newly negotiated encryption parameters for outbound traffic"
|
||||||
|
m = Message()
|
||||||
|
m.add_byte(chr(MSG_NEWKEYS))
|
||||||
|
self.send_message(m)
|
||||||
|
self.block_size_out = self.cipher_info[self.local_cipher]['block-size']
|
||||||
|
if self.server_mode:
|
||||||
|
IV_out = self.compute_key('B', self.block_size_out)
|
||||||
|
key_out = self.compute_key('D', self.cipher_info[self.local_cipher]['key-size'])
|
||||||
|
else:
|
||||||
|
IV_out = self.compute_key('A', self.block_size_out)
|
||||||
|
key_out = self.compute_key('C', self.cipher_info[self.local_cipher]['key-size'])
|
||||||
|
self.engine_out = self.get_cipher(self.local_cipher, key_out, IV_out)
|
||||||
|
self.local_mac_len = self.mac_info[self.local_mac]['size']
|
||||||
|
self.local_mac_engine = self.mac_info[self.local_mac]['class']
|
||||||
|
# initial mac keys are done in the hash's natural size (not the potentially truncated
|
||||||
|
# transmission size)
|
||||||
|
if self.server_mode:
|
||||||
|
self.mac_key_out = self.compute_key('F', self.local_mac_engine.digest_size)
|
||||||
|
else:
|
||||||
|
self.mac_key_out = self.compute_key('E', self.local_mac_engine.digest_size)
|
||||||
|
|
||||||
|
def parse_newkeys(self, m):
|
||||||
|
self.log(DEBUG, 'Switch to new keys ...')
|
||||||
|
self.activate_inbound()
|
||||||
|
# can also free a bunch of stuff here
|
||||||
|
self.local_kex_init = self.remote_kex_init = None
|
||||||
|
self.e = self.f = self.K = self.x = None
|
||||||
|
if not self.initial_kex_done:
|
||||||
|
# this was the first key exchange
|
||||||
|
self.initial_kex_done = 1
|
||||||
|
# send an event?
|
||||||
|
if self.completion_event != None:
|
||||||
|
self.completion_event.set()
|
||||||
|
return
|
||||||
|
|
||||||
|
def parse_disconnect(self, m):
|
||||||
|
code = m.get_int()
|
||||||
|
desc = m.get_string()
|
||||||
|
self.log(INFO, 'Disconnect (code %d): %s' % (code, desc))
|
||||||
|
def parse_channel_open_success(self, m):
|
||||||
|
chanid = m.get_int()
|
||||||
|
server_chanid = m.get_int()
|
||||||
|
server_window_size = m.get_int()
|
||||||
|
server_max_packet_size = m.get_int()
|
||||||
|
if not self.channels.has_key(chanid):
|
||||||
|
self.log(WARNING, 'Success for unrequested channel! [??]')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.lock.acquire()
|
||||||
|
chan = self.channels[chanid]
|
||||||
|
chan.set_server_channel(server_chanid, server_window_size, server_max_packet_size)
|
||||||
|
self.log(INFO, 'Secsh channel %d opened.' % chanid)
|
||||||
|
if self.channel_events.has_key(chanid):
|
||||||
|
self.channel_events[chanid].set()
|
||||||
|
del self.channel_events[chanid]
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
return
|
||||||
|
|
||||||
|
def parse_channel_open_failure(self, m):
|
||||||
|
chanid = m.get_int()
|
||||||
|
reason = m.get_int()
|
||||||
|
reason_str = m.get_string()
|
||||||
|
lang = m.get_string()
|
||||||
|
if CONNECTION_FAILED_CODE.has_key(reason):
|
||||||
|
reason_text = CONNECTION_FAILED_CODE[reason]
|
||||||
|
else:
|
||||||
|
reason_text = '(unknown code)'
|
||||||
|
self.log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text))
|
||||||
|
try:
|
||||||
|
self.lock.aquire()
|
||||||
|
if self.channels.has_key(chanid):
|
||||||
|
del self.channels[chanid]
|
||||||
|
if self.channel_events.has_key(chanid):
|
||||||
|
self.channel_events[chanid].set()
|
||||||
|
del self.channel_events[chanid]
|
||||||
|
finally:
|
||||||
|
self.lock_release()
|
||||||
|
return
|
||||||
|
|
||||||
|
def parse_channel_open(self, m):
|
||||||
|
kind = m.get_string()
|
||||||
|
self.log(DEBUG, 'Rejecting "%s" channel request from server.' % kind)
|
||||||
|
chanid = m.get_int()
|
||||||
|
msg = Message()
|
||||||
|
msg.add_byte(chr(MSG_CHANNEL_OPEN_FAILURE))
|
||||||
|
msg.add_int(chanid)
|
||||||
|
msg.add_int(1)
|
||||||
|
msg.add_string('Client connections are not allowed.')
|
||||||
|
msg.add_string('en')
|
||||||
|
self.send_message(msg)
|
||||||
|
|
||||||
|
def parse_debug(self, m):
|
||||||
|
always_display = m.get_boolean()
|
||||||
|
msg = m.get_string()
|
||||||
|
lang = m.get_string()
|
||||||
|
self.log(DEBUG, 'Debug msg: ' + safe_string(msg))
|
||||||
|
|
||||||
|
handler_table = {
|
||||||
|
MSG_NEWKEYS: parse_newkeys,
|
||||||
|
MSG_CHANNEL_OPEN_SUCCESS: parse_channel_open_success,
|
||||||
|
MSG_CHANNEL_OPEN_FAILURE: parse_channel_open_failure,
|
||||||
|
MSG_CHANNEL_OPEN: parse_channel_open,
|
||||||
|
MSG_KEXINIT: negotiate_keys,
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_handler_table = {
|
||||||
|
MSG_CHANNEL_SUCCESS: Channel.request_success,
|
||||||
|
MSG_CHANNEL_FAILURE: Channel.request_failed,
|
||||||
|
MSG_CHANNEL_DATA: Channel.feed,
|
||||||
|
MSG_CHANNEL_WINDOW_ADJUST: Channel.window_adjust,
|
||||||
|
MSG_CHANNEL_REQUEST: Channel.handle_request,
|
||||||
|
MSG_CHANNEL_EOF: Channel.handle_eof,
|
||||||
|
MSG_CHANNEL_CLOSE: Channel.handle_close,
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
def inflate_long(s, always_positive=0):
|
||||||
|
"turns a normalized byte string into a long-int (adapted from Crypto.Util.number)"
|
||||||
|
out = 0L
|
||||||
|
negative = 0
|
||||||
|
if not always_positive and (len(s) > 0) and (ord(s[0]) >= 0x80):
|
||||||
|
negative = 1
|
||||||
|
if len(s) % 4:
|
||||||
|
filler = '\x00'
|
||||||
|
if negative:
|
||||||
|
filler = '\xff'
|
||||||
|
s = filler * (4 - len(s) % 4) + s
|
||||||
|
for i in range(0, len(s), 4):
|
||||||
|
out = (out << 32) + struct.unpack('>I', s[i:i+4])[0]
|
||||||
|
if negative:
|
||||||
|
out -= (1L << (8 * len(s)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def deflate_long(n, add_sign_padding=1):
|
||||||
|
"turns a long-int into a normalized byte string (adapted from Crypto.Util.number)"
|
||||||
|
# after much testing, this algorithm was deemed to be the fastest
|
||||||
|
s = ''
|
||||||
|
n = long(n)
|
||||||
|
while (n != 0) and (n != -1):
|
||||||
|
s = struct.pack('>I', n & 0xffffffffL) + s
|
||||||
|
n = n >> 32
|
||||||
|
# strip off leading zeros, FFs
|
||||||
|
for i in enumerate(s):
|
||||||
|
if (n == 0) and (i[1] != '\000'):
|
||||||
|
break
|
||||||
|
if (n == -1) and (i[1] != '\xff'):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# degenerate case, n was either 0 or -1
|
||||||
|
i = (0,)
|
||||||
|
if n == 0:
|
||||||
|
s = '\000'
|
||||||
|
else:
|
||||||
|
s = '\xff'
|
||||||
|
s = s[i[0]:]
|
||||||
|
if add_sign_padding:
|
||||||
|
if (n == 0) and (ord(s[0]) >= 0x80):
|
||||||
|
s = '\x00' + s
|
||||||
|
if (n == -1) and (ord(s[0]) < 0x80):
|
||||||
|
s = '\xff' + s
|
||||||
|
return s
|
||||||
|
|
||||||
|
def format_binary_weird(data):
|
||||||
|
out = ''
|
||||||
|
for i in enumerate(data):
|
||||||
|
out += '%02X' % ord(i[1])
|
||||||
|
if i[0] % 2:
|
||||||
|
out += ' '
|
||||||
|
if i[0] % 16 == 15:
|
||||||
|
out += '\n'
|
||||||
|
return out
|
||||||
|
|
||||||
|
def format_binary(data, prefix=''):
|
||||||
|
x = 0
|
||||||
|
out = []
|
||||||
|
while len(data) > x + 16:
|
||||||
|
out.append(format_binary_line(data[x:x+16]))
|
||||||
|
x += 16
|
||||||
|
if x < len(data):
|
||||||
|
out.append(format_binary_line(data[x:]))
|
||||||
|
return [prefix + x for x in out]
|
||||||
|
|
||||||
|
def format_binary_line(data):
|
||||||
|
left = ' '.join(['%02X' % ord(c) for c in data])
|
||||||
|
right = ''.join([('.%c..' % c)[(ord(c)+61)//94] for c in data])
|
||||||
|
return '%-50s %s' % (left, right)
|
||||||
|
|
||||||
|
def hexify(s):
|
||||||
|
"turn a string into a hex sequence"
|
||||||
|
return ''.join(['%02X' % ord(c) for c in s])
|
||||||
|
|
||||||
|
def safe_string(s):
|
||||||
|
out = ''
|
||||||
|
for c in s:
|
||||||
|
if (ord(c) >= 32) and (ord(c) <= 127):
|
||||||
|
out += c
|
||||||
|
else:
|
||||||
|
out += '%%%02X' % ord(c)
|
||||||
|
return out
|
||||||
|
|
||||||
|
# ''.join([['%%%02X' % ord(c), c][(ord(c) >= 32) and (ord(c) <= 127)] for c in s])
|
Loading…
Reference in New Issue