Compare commits
266 Commits
with_open_
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a0d1c1df5c | ||
![]() |
1d35f4deca | ||
![]() |
1437639338 | ||
![]() |
90b88d82e8 | ||
e17005b729 | |||
aa0a7e5837 | |||
77b22b8b8a | |||
6543b230d4 | |||
67338238af | |||
d8686ca943 | |||
![]() |
b740d2bfff | ||
11a2db0acb | |||
379795ee06 | |||
![]() |
436bc3d083 | ||
4663c3cd02 | |||
f75e4ee896 | |||
![]() |
88828685c2 | ||
![]() |
4fa98eb805 | ||
916ea1dc2c | |||
9b2eff59d8 | |||
13349d6d99 | |||
b0fba9b441 | |||
6954e8dc4e | |||
![]() |
0febad432a | ||
473833a58f | |||
![]() |
743edf045b | ||
![]() |
8f3da5ee34 | ||
![]() |
c2c051b6a3 | ||
![]() |
6deef053d3 | ||
![]() |
c4bb612381 | ||
289c2711b8 | |||
b2848970c3 | |||
b638e58dc8 | |||
b8592686e4 | |||
95aa8aa3bc | |||
31eec2d3f3 | |||
fcc2cb674f | |||
add247d622 | |||
542166de67 | |||
66ece65699 | |||
![]() |
701949b8f7 | ||
![]() |
72298d7a4c | ||
![]() |
62a7d65be5 | ||
![]() |
6c7f0a3a6f | ||
![]() |
86f94f49ef | ||
![]() |
026a296444 | ||
![]() |
7474012ada | ||
![]() |
84b6c80b07 | ||
![]() |
3848a0bf7e | ||
![]() |
31826960c1 | ||
![]() |
af732a1d64 | ||
![]() |
0c0f6ee81b | ||
![]() |
c149a7b3b7 | ||
![]() |
3976fd631c | ||
![]() |
c95f1f535b | ||
![]() |
ea3c90d85d | ||
![]() |
eb9bbd8b3b | ||
![]() |
76c898588f | ||
![]() |
7dd42eaf4d | ||
![]() |
e6652df291 | ||
![]() |
2ab3116d52 | ||
![]() |
5af337fca7 | ||
![]() |
68669a7fd5 | ||
![]() |
118e1355bc | ||
![]() |
419f266f0f | ||
![]() |
e4b7bf85e9 | ||
![]() |
6d6d1e20b1 | ||
![]() |
001925af88 | ||
![]() |
6ba2b6287c | ||
![]() |
7eee1c4658 | ||
![]() |
a89a6ec94b | ||
![]() |
4ef4a939e8 | ||
![]() |
431b4e3425 | ||
![]() |
130746fa00 | ||
![]() |
36061cccb5 | ||
![]() |
0422038c47 | ||
![]() |
fbce0d448e | ||
![]() |
b1383ff3d5 | ||
![]() |
b8012e8b4b | ||
![]() |
58ee4cff4d | ||
![]() |
55b61fab65 | ||
![]() |
981d92db92 | ||
![]() |
b8622c4462 | ||
![]() |
d744250c1b | ||
![]() |
ae2a7be09d | ||
![]() |
305da25420 | ||
![]() |
623c0e3abd | ||
![]() |
31e8c4acee | ||
![]() |
c55b844ad1 | ||
![]() |
5d4e218db5 | ||
![]() |
423c509878 | ||
![]() |
94b326f1e5 | ||
![]() |
6cea3c012e | ||
![]() |
262d10f55d | ||
![]() |
06bfb8f3de | ||
![]() |
e091d94f91 | ||
![]() |
a79802afa1 | ||
![]() |
122c366490 | ||
![]() |
aa8fa6f1c4 | ||
![]() |
46357d8cf8 | ||
![]() |
c5821b4e56 | ||
![]() |
86875ff09f | ||
![]() |
676a2be088 | ||
![]() |
6d191405be | ||
![]() |
ca4a9e4b08 | ||
![]() |
cfc48cac26 | ||
![]() |
1fc59208b6 | ||
![]() |
69d13d5c97 | ||
![]() |
b17646ea4f | ||
![]() |
b27ee82a9d | ||
![]() |
33043a3499 | ||
![]() |
6f450ab68f | ||
![]() |
d84a98520a | ||
![]() |
fa2fda8054 | ||
![]() |
e1eefebf9d | ||
![]() |
f169080f59 | ||
![]() |
ef28b55686 | ||
![]() |
24b5899280 | ||
![]() |
81f85b7e48 | ||
![]() |
399819748f | ||
![]() |
6078736f70 | ||
![]() |
120b576a38 | ||
![]() |
b566970d57 | ||
![]() |
380301725d | ||
![]() |
cd9148bcec | ||
![]() |
4dba35f455 | ||
![]() |
5bd2a87d0c | ||
![]() |
91bff5412c | ||
![]() |
8c9a3cd14b | ||
![]() |
e1cd54a7b2 | ||
![]() |
e169160159 | ||
![]() |
9f1515b96c | ||
![]() |
aac603b4ee | ||
![]() |
5243da4d0a | ||
![]() |
a39e6e4bb6 | ||
![]() |
f23c20da99 | ||
![]() |
ba53a8d162 | ||
![]() |
e18ebb24fb | ||
![]() |
38227cf938 | ||
![]() |
07ab9840ca | ||
![]() |
4f775159c8 | ||
![]() |
36c20eae91 | ||
![]() |
eb33ecd559 | ||
![]() |
b843a473bc | ||
![]() |
2d0e15f811 | ||
![]() |
4640030373 | ||
![]() |
0836b93fee | ||
![]() |
20129f5ac0 | ||
![]() |
67c3eb93fa | ||
![]() |
0ed777e86b | ||
![]() |
c546d427fd | ||
![]() |
5b941f171f | ||
![]() |
652d616471 | ||
![]() |
6d04f262db | ||
![]() |
ea414ca5b7 | ||
![]() |
3d537e42bc | ||
![]() |
3252303573 | ||
![]() |
166f5a9021 | ||
![]() |
080ee26fff | ||
![]() |
5b2f4d5c0a | ||
![]() |
591db419ad | ||
![]() |
7b55f4e3f6 | ||
![]() |
48364561bf | ||
![]() |
a8b9ecc7a0 | ||
7f03396325 | |||
e4552a879f | |||
![]() |
caca1d1e84 | ||
![]() |
3ccef84028 | ||
![]() |
48eb809e84 | ||
![]() |
2d42adaaf7 | ||
![]() |
045bcfaa0d | ||
![]() |
9c528c4908 | ||
![]() |
b2109646f3 | ||
![]() |
7598e2bf6f | ||
![]() |
aa791870be | ||
![]() |
1bac4a9d78 | ||
![]() |
fd68ba87b7 | ||
![]() |
731609f689 | ||
![]() |
405bf79d56 | ||
![]() |
d264281284 | ||
![]() |
30a9e1fc58 | ||
![]() |
d2f92dad86 | ||
![]() |
598fd5fcde | ||
![]() |
aaf6b16407 | ||
![]() |
7ffde14257 | ||
![]() |
e41b3f5ab3 | ||
![]() |
e19ae130e9 | ||
![]() |
44fce6e456 | ||
![]() |
b09a07e3aa | ||
![]() |
ae118ee9ed | ||
![]() |
5ab91b01f7 | ||
![]() |
a8914da258 | ||
![]() |
58736f7f95 | ||
![]() |
b651901b91 | ||
![]() |
afd24b8070 | ||
![]() |
3a61ea161a | ||
![]() |
d4a5bf60db | ||
![]() |
5834e8fafc | ||
![]() |
c3e5baf04b | ||
![]() |
836aacd425 | ||
![]() |
b4ee009465 | ||
![]() |
33558610a6 | ||
![]() |
3bcdb408a1 | ||
![]() |
6801047d0a | ||
![]() |
11f82ee44e | ||
![]() |
f8b5d7e2d8 | ||
![]() |
ce317bf3f7 | ||
![]() |
b3a3427a9a | ||
![]() |
7bc6ea3408 | ||
![]() |
8add03fadb | ||
![]() |
f904052111 | ||
![]() |
704e590891 | ||
![]() |
08d3958b47 | ||
![]() |
c49112a28d | ||
![]() |
eb6999169e | ||
![]() |
b489c69772 | ||
![]() |
300beededb | ||
![]() |
7786f0f49d | ||
![]() |
8441148e36 | ||
![]() |
6423c36f24 | ||
![]() |
3776f89d9c | ||
![]() |
e74c554643 | ||
![]() |
f47f2f8640 | ||
![]() |
54751f9753 | ||
![]() |
a31a528e60 | ||
![]() |
ef73b92929 | ||
![]() |
055a373885 | ||
![]() |
1491db7e1e | ||
![]() |
a6c557097a | ||
![]() |
8aaf23cdac | ||
![]() |
6c2dfce9a7 | ||
![]() |
20185dea68 | ||
![]() |
a7fb958a2c | ||
![]() |
e03be139ef | ||
![]() |
2ab99f0f22 | ||
![]() |
4dedd79942 | ||
![]() |
b2ba35504d | ||
![]() |
4da7afdeaf | ||
![]() |
23a0dfedb1 | ||
![]() |
fb7d964516 | ||
![]() |
72c1532284 | ||
![]() |
130074788a | ||
![]() |
30f508f40c | ||
![]() |
8ff59fdaf0 | ||
![]() |
28e46f6f51 | ||
![]() |
e0a625853d | ||
![]() |
31ecf0b262 | ||
![]() |
fe48317d07 | ||
![]() |
9be695aea6 | ||
![]() |
b1aaca28d7 | ||
![]() |
05c3fcc825 | ||
![]() |
4cf5a6393a | ||
![]() |
3909b0c783 | ||
![]() |
5ace114ef8 | ||
![]() |
5ad4f0bda8 | ||
![]() |
3869b30198 | ||
![]() |
f652aa7c35 | ||
![]() |
6dbc713dc9 | ||
![]() |
20388431aa | ||
![]() |
eea9cf778e | ||
![]() |
b93ac7368d | ||
![]() |
7df13e51e3 | ||
![]() |
eddf7750c7 | ||
![]() |
efa84759da | ||
![]() |
86eba8b6ab | ||
![]() |
a1925e1053 |
16
.githooks/pre-commit/01_asciiref_gen
Executable file
16
.githooks/pre-commit/01_asciiref_gen
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
origdir="${PWD}"
|
||||||
|
docsdir="${PWD}/ref/ascii/"
|
||||||
|
|
||||||
|
if ! command -v asciidoctor &> /dev/null;
|
||||||
|
then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${docsdir}"
|
||||||
|
|
||||||
|
asciidoctor -o ascii.html ascii.adoc
|
||||||
|
|
||||||
|
cd ${origdir}
|
||||||
|
git add "${docsdir}/ascii.html"
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,4 +22,6 @@ __pycache__/
|
|||||||
*.run
|
*.run
|
||||||
*.7z
|
*.7z
|
||||||
*.rar
|
*.rar
|
||||||
|
*.sqlite3
|
||||||
*.deb
|
*.deb
|
||||||
|
.idea/
|
||||||
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, 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
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If 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 convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU 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
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state 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 program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
39
TODO
Normal file
39
TODO
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
- sshsecure is being re-written in golang
|
||||||
|
|
||||||
|
-vault, schema dumper (dump mounts, paths (otional w/switch or toggle), and meta information)
|
||||||
|
--ability to recreate from xml dump
|
||||||
|
|
||||||
|
-git
|
||||||
|
|
||||||
|
-net/addr needs DNS/PTR/allocation stuff etc.
|
||||||
|
|
||||||
|
-net/mirroring
|
||||||
|
|
||||||
|
-storage, see if we can access lvm and cryptsetup functions via https://github.com/storaged-project/libblockdev/issues/41
|
||||||
|
--http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.MDRaid.html
|
||||||
|
--http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Encrypted.html
|
||||||
|
--http://mindbending.org/en/python-and-udisks-part-2
|
||||||
|
--http://storaged.org/doc/udisks2-api/2.6.5/gdbus-org.freedesktop.UDisks2.Block.html
|
||||||
|
--https://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html
|
||||||
|
|
||||||
|
|
||||||
|
sshkeys:
|
||||||
|
-need to verify keys via GPG signature. we also need to have a more robust way of updating pubkeys - categorization, role
|
||||||
|
-write API to get pubkeys, hostkeys? really wish DBs supported nesting
|
||||||
|
-separate by algo, but this is easy to do (split on space, [0])
|
||||||
|
|
||||||
|
snippet: create mtree with libarchive, bsdtar -cf /tmp/win.mtree --one-file-system --format=mtree --options='mtree:sha512,mtree:indent' /path/*
|
||||||
|
probably need to package https://packages.debian.org/source/stretch/freebsd-buildutils to get fmtree for reading
|
||||||
|
|
||||||
|
-net, add ipxe - write flask app that determines path based on MAC addr
|
||||||
|
|
||||||
|
-net, add shorewall templater
|
||||||
|
|
||||||
|
-port in sslchk
|
||||||
|
|
||||||
|
-script that uses uconv(?) and pymysql to export database to .ods
|
||||||
|
|
||||||
|
-IRC
|
||||||
|
-- i should use the python IRC module on pypi to join an irc network (freenode, probably, for my personal interests) and
|
||||||
|
run an iteration over all nicks in a channel with /ctcp <nick> version. handy when i'm trying to find someone running
|
||||||
|
a certain platform/client i have some questions about.
|
62
aif/cfgs/base.xml
Normal file
62
aif/cfgs/base.xml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<aif xmlns:aif="https://aif.square-r00t.net"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="https://aif.square-r00t.net aif.xsd">
|
||||||
|
<storage>
|
||||||
|
<disk device="/dev/sda" diskfmt="gpt">
|
||||||
|
<part num="1" start="0%" size="10%" fstype="ef00" />
|
||||||
|
<part num="2" start="10%" size="100%" fstype="8300" />
|
||||||
|
</disk>
|
||||||
|
<mount source="/dev/sda2" target="/mnt/aif" order="1" />
|
||||||
|
<mount source="/dev/sda1" target="/mnt/aif/boot" order="2" />
|
||||||
|
</storage>
|
||||||
|
<network hostname="aiftest.square-r00t.net">
|
||||||
|
<iface device="auto" address="auto" netproto="ipv4" />
|
||||||
|
</network>
|
||||||
|
<system timezone="EST5EDT" locale="en_US.UTF-8" chrootpath="/mnt/aif" reboot="1">
|
||||||
|
<users rootpass="!" />
|
||||||
|
<service name="sshd" status="1" />
|
||||||
|
<service name="cronie" status="1" />
|
||||||
|
<service name="haveged" status="1" />
|
||||||
|
</system>
|
||||||
|
<pacman command="apacman -S">
|
||||||
|
<repos>
|
||||||
|
<repo name="core" enabled="true" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
|
||||||
|
<repo name="extra" enabled="true" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
|
||||||
|
<repo name="community" enabled="true" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
|
||||||
|
<repo name="multilib" enabled="true" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
|
||||||
|
<repo name="testing" enabled="false" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
|
||||||
|
<repo name="multilib-testing" enabled="false" siglevel="default" mirror="file:///etc/pacman.d/mirrorlist" />
|
||||||
|
<repo name="archlinuxfr" enabled="false" siglevel="Optional TrustedOnly" mirror="http://repo.archlinux.fr/$arch" />
|
||||||
|
</repos>
|
||||||
|
<mirrorlist>
|
||||||
|
<mirror>http://mirror.us.leaseweb.net/archlinux/$repo/os/$arch</mirror>
|
||||||
|
<mirror>http://mirrors.advancedhosters.com/archlinux/$repo/os/$arch</mirror>
|
||||||
|
<mirror>http://ftp.osuosl.org/pub/archlinux/$repo/os/$arch</mirror>
|
||||||
|
<mirror>http://arch.mirrors.ionfish.org/$repo/os/$arch</mirror>
|
||||||
|
<mirror>http://mirrors.gigenet.com/archlinux/$repo/os/$arch</mirror>
|
||||||
|
<mirror>http://mirror.jmu.edu/pub/archlinux/$repo/os/$arch</mirror>
|
||||||
|
</mirrorlist>
|
||||||
|
<software>
|
||||||
|
<package name="sed" repo="core" />
|
||||||
|
<package name="python" />
|
||||||
|
<package name="openssh" />
|
||||||
|
<package name="vim" />
|
||||||
|
<package name="vim-plugins" />
|
||||||
|
<package name="haveged" />
|
||||||
|
<package name="byobu" />
|
||||||
|
<package name="etc-update" />
|
||||||
|
<package name="cronie" />
|
||||||
|
<package name="mlocate" />
|
||||||
|
<package name="mtree-git" />
|
||||||
|
</software>
|
||||||
|
</pacman>
|
||||||
|
<bootloader type="grub" target="/boot" efi="true" />
|
||||||
|
<scripts>
|
||||||
|
<script uri="https://aif.square-r00t.net/cfgs/scripts/pkg/python.sh" order="1" execution="pkg" />
|
||||||
|
<script uri="https://aif.square-r00t.net/cfgs/scripts/pkg/apacman.py" order="2" execution="pkg" />
|
||||||
|
<script uri="https://aif.square-r00t.net/cfgs/scripts/post/sshsecure.py" order="1" execution="post" />
|
||||||
|
<script uri="https://aif.square-r00t.net/cfgs/scripts/post/sshkeys.py" order="2" execution="post" />
|
||||||
|
<script uri="https://aif.square-r00t.net/cfgs/scripts/post/configs.py" order="3" execution="post" />
|
||||||
|
</scripts>
|
||||||
|
</aif>
|
98
aif/scripts/pkg/apacman.py
Normal file
98
aif/scripts/pkg/apacman.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
pkg_base = 'apacman'
|
||||||
|
pkgs = ('', '-deps', '-utils')
|
||||||
|
url_base = 'https://aif.square-r00t.net/cfgs/files'
|
||||||
|
local_dir = '/tmp'
|
||||||
|
|
||||||
|
conf_options = {}
|
||||||
|
conf_options['apacman'] = {'enabled': ['needed', 'noconfirm', 'noedit', 'progress', 'purgebuild', 'skipcache', 'keepkeys'],
|
||||||
|
'disabled': [],
|
||||||
|
'values': {'tmpdir': '"/var/tmp/apacmantmp-$UID"'}}
|
||||||
|
conf_options['pacman'] = {'enabled': [],
|
||||||
|
'disabled': [],
|
||||||
|
'values': {'UseSyslog': None, 'Color': None, 'TotalDownload': None, 'CheckSpace': None, 'VerbosePkgLists': None}}
|
||||||
|
|
||||||
|
def downloadPkg(pkgfile, dlfile):
|
||||||
|
url = os.path.join(url_base, pkgfile)
|
||||||
|
# Prep the destination
|
||||||
|
os.makedirs(os.path.dirname(dlfile), exist_ok = True)
|
||||||
|
# Download the pacman package
|
||||||
|
with urlopen(url) as u:
|
||||||
|
with open(dlfile, 'wb') as f:
|
||||||
|
f.write(u.read())
|
||||||
|
return()
|
||||||
|
|
||||||
|
def installPkg(pkgfile):
|
||||||
|
# Install it
|
||||||
|
subprocess.run(['pacman', '-Syyu']) # Installing from an inconsistent state is bad, mmkay?
|
||||||
|
subprocess.run(['pacman', '--noconfirm', '--needed', '-S', 'base-devel'])
|
||||||
|
subprocess.run(['pacman', '--noconfirm', '--needed', '-S', 'multilib-devel'])
|
||||||
|
subprocess.run(['pacman', '--noconfirm', '--needed', '-U', pkgfile])
|
||||||
|
return()
|
||||||
|
|
||||||
|
def configurePkg(opts, pkgr):
|
||||||
|
cf = '/etc/{0}.conf'.format(pkgr)
|
||||||
|
# Configure it
|
||||||
|
shutil.copy2(cf, '{0}.bak.{1}'.format(cf, int(datetime.datetime.utcnow().timestamp())))
|
||||||
|
with open(cf, 'r') as f:
|
||||||
|
conf = f.readlines()
|
||||||
|
for idx, line in enumerate(conf):
|
||||||
|
l = line.split('=')
|
||||||
|
opt = l[0].strip('\n').strip()
|
||||||
|
if len(l) > 1:
|
||||||
|
val = l[1].strip('\n').strip()
|
||||||
|
# enabled options
|
||||||
|
for o in opts['enabled']:
|
||||||
|
if re.sub('^#?', '', opt).strip() == o:
|
||||||
|
if pkgr == 'apacman':
|
||||||
|
conf[idx] = '{0}=1\n'.format(o)
|
||||||
|
elif pkgr == 'pacman':
|
||||||
|
conf[idx] = '{0}\n'.format(o)
|
||||||
|
# disabled options
|
||||||
|
for o in opts['disabled']:
|
||||||
|
if re.sub('^#?', '', opt).strip() == o:
|
||||||
|
if pkgr == 'apacman':
|
||||||
|
conf[idx] = '{0}=0\n'.format(o)
|
||||||
|
elif pkgr == 'pacman':
|
||||||
|
conf[idx] = '#{0}\n'.format(o)
|
||||||
|
# values
|
||||||
|
for o in opts['values']:
|
||||||
|
if opts['values'][o] is not None:
|
||||||
|
if re.sub('^#?', '', opt).strip() == o:
|
||||||
|
if pkgr == 'apacman':
|
||||||
|
conf[idx] = '{0}={1}\n'.format(o, opts['values'][o])
|
||||||
|
elif pkgr == 'pacman':
|
||||||
|
conf[idx] = '{0} = {1}\n'.format(o, opts['values'][o])
|
||||||
|
else:
|
||||||
|
if re.sub('^#?', '', opt).strip() == o:
|
||||||
|
conf[idx] = '{0}\n'.format(o)
|
||||||
|
with open(cf, 'w') as f:
|
||||||
|
f.write(''.join(conf))
|
||||||
|
|
||||||
|
def finishPkg():
|
||||||
|
# Finish installing (optional deps)
|
||||||
|
for p in ('git', 'customizepkg-scripting', 'pkgfile', 'rsync'):
|
||||||
|
subprocess.run(['apacman', '--noconfirm', '--needed', '-S', p])
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for p in pkgs:
|
||||||
|
pkg = pkg_base + p
|
||||||
|
fname = '{0}.tar.xz'.format(pkg)
|
||||||
|
local_pkg = os.path.join(local_dir, fname)
|
||||||
|
downloadPkg(fname, local_pkg)
|
||||||
|
installPkg(local_pkg)
|
||||||
|
for tool in ('pacman', 'apacman'):
|
||||||
|
configurePkg(conf_options[tool], tool)
|
||||||
|
finishPkg()
|
||||||
|
return()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
3
aif/scripts/pkg/python.sh
Normal file
3
aif/scripts/pkg/python.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
pacman --needed --noconfirm -S python python-pip python-setuptools
|
136
aif/scripts/post/configs.py
Normal file
136
aif/scripts/post/configs.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import pwd
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def byobu(user = 'root'):
|
||||||
|
homedir = os.path.expanduser('~{0}'.format(user))
|
||||||
|
subprocess.run(['byobu-enable'])
|
||||||
|
b = '{0}/.byobu'.format(homedir)
|
||||||
|
# The keybindings, and general enabling
|
||||||
|
confs = {'backend': 'BYOBU_BACKEND=tmux\n',
|
||||||
|
'color': 'BACKGROUND=k\nFOREGROUND=w\nMONOCHROME=0', # NOT a typo; the original source I got this from had no end newline.
|
||||||
|
'color.tmux': 'BYOBU_DARK="\#333333"\nBYOBU_LIGHT="\#EEEEEE"\nBYOBU_ACCENT="\#75507B"\nBYOBU_HIGHLIGHT="\#DD4814"\n',
|
||||||
|
'datetime.tmux': 'BYOBU_DATE="%Y-%m-%d "\nBYOBU_TIME="%H:%M:%S"\n',
|
||||||
|
'keybindings': 'source $BYOBU_PREFIX/share/byobu/keybindings/common\n',
|
||||||
|
'keybindings.tmux': 'unbind-key -n C-a\nset -g prefix ^A\nset -g prefix2 ^A\nbind a send-prefix\n',
|
||||||
|
'profile': 'source $BYOBU_PREFIX/share/byobu/profiles/common\n',
|
||||||
|
'profile.tmux': 'source $BYOBU_PREFIX/share/byobu/profiles/tmux\n',
|
||||||
|
'prompt': '[ -r /usr/share/byobu/profiles/bashrc ] && . /usr/share/byobu/profiles/bashrc #byobu-prompt#\n',
|
||||||
|
'.screenrc': None,
|
||||||
|
'.tmux.conf': None,
|
||||||
|
'.welcome-displayed': None,
|
||||||
|
'windows': None,
|
||||||
|
'windows.tmux': None}
|
||||||
|
for c in confs.keys():
|
||||||
|
with open('{0}/{1}'.format(b, c), 'w') as f:
|
||||||
|
if confs[c] is not None:
|
||||||
|
f.write(confs[c])
|
||||||
|
else:
|
||||||
|
f.write('')
|
||||||
|
# The status file- add some extras, and remove the session string which is broken apparently.
|
||||||
|
# Holy shit I wish there was a way of storing compressed text in plaintext besides base64.
|
||||||
|
statusconf = ["# status - Byobu's default status enabled/disabled settings\n", '#\n', '# Override these in $BYOBU_CONFIG_DIR/status\n',
|
||||||
|
'# where BYOBU_CONFIG_DIR is XDG_CONFIG_HOME if defined,\n', '# and $HOME/.byobu otherwise.\n', '#\n',
|
||||||
|
'# Copyright (C) 2009-2011 Canonical Ltd.\n', '#\n', '# Authors: Dustin Kirkland <kirkland@byobu.org>\n', '#\n',
|
||||||
|
'# This program is free software: you can redistribute it and/or modify\n', '# it under the terms of the GNU ' +
|
||||||
|
'General Public License as published by\n', '# the Free Software Foundation, version 3 of the License.\n', '#\n',
|
||||||
|
'# This program is distributed in the hope that it will be useful,\n', '# but WITHOUT ANY WARRANTY; without even the ' +
|
||||||
|
'implied warranty of\n', '# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n', '# GNU General Public License ' +
|
||||||
|
'for more details.\n', '#\n', '# You should have received a copy of the GNU General Public License\n', '# along with this ' +
|
||||||
|
'program. If not, see <http://www.gnu.org/licenses/>.\n', '\n', "# Status beginning with '#' are disabled.\n", '\n', '# Screen has ' +
|
||||||
|
'two status lines, with 4 quadrants for status\n', 'screen_upper_left="color"\n', 'screen_upper_right="color whoami hostname ' +
|
||||||
|
'ip_address menu"\n', 'screen_lower_left="color logo distro release #arch session"\n', 'screen_lower_right="color network #disk_io ' +
|
||||||
|
'custom #entropy raid reboot_required updates_available #apport #services #mail users uptime #ec2_cost #rcs_cost #fan_speed #cpu_temp ' +
|
||||||
|
'battery wifi_quality #processes load_average cpu_count cpu_freq memory #swap disk #time_utc date time"\n', '\n', '# Tmux has one ' +
|
||||||
|
'status line, with 2 halves for status\n', 'tmux_left=" logo #distro release arch #session"\n', '# You can have as many tmux right ' +
|
||||||
|
'lines below here, and cycle through them using Shift-F5\n', 'tmux_right=" network disk_io #custom #entropy raid reboot_required ' +
|
||||||
|
'#updates_available #apport services #mail #users uptime #ec2_cost #rcs_cost #fan_speed #cpu_temp #battery #wifi_quality processes ' +
|
||||||
|
'load_average cpu_count cpu_freq memory #swap disk whoami hostname ip_address time_utc date time"\n', '#tmux_right="network ' +
|
||||||
|
'#disk_io #custom entropy raid reboot_required updates_available #apport #services #mail users uptime #ec2_cost #rcs_cost fan_speed ' +
|
||||||
|
'cpu_temp battery wifi_quality #processes load_average cpu_count cpu_freq memory #swap #disk whoami hostname ip_address #time_utc ' +
|
||||||
|
'date time"\n', '#tmux_right="network #disk_io custom #entropy raid reboot_required updates_available #apport #services #mail users ' +
|
||||||
|
'uptime #ec2_cost #rcs_cost #fan_speed #cpu_temp battery wifi_quality #processes load_average cpu_count cpu_freq memory #swap #disk ' +
|
||||||
|
'#whoami #hostname ip_address #time_utc date time"\n', '#tmux_right="#network disk_io #custom entropy #raid #reboot_required ' +
|
||||||
|
'#updates_available #apport #services #mail #users #uptime #ec2_cost #rcs_cost fan_speed cpu_temp #battery #wifi_quality #processes ' +
|
||||||
|
'#load_average #cpu_count #cpu_freq #memory #swap whoami hostname ip_address #time_utc disk date time"\n']
|
||||||
|
with open('{0}/status'.format(b), 'w') as f:
|
||||||
|
f.write(''.join(statusconf))
|
||||||
|
# The statusrc file is another lengthy one.
|
||||||
|
statusrc = ["# statusrc - Byobu's default status configurations\n", '#\n', '# Override these in $BYOBU_CONFIG_DIR/statusrc\n',
|
||||||
|
'# where BYOBU_CONFIG_DIR is XDG_CONFIG_HOME if defined,\n', '# and $HOME/.byobu otherwise.\n', '#\n', '# Copyright (C) ' +
|
||||||
|
'2009-2011 Canonical Ltd.\n', '#\n', '# Authors: Dustin Kirkland <kirkland@byobu.org>\n', '#\n', '# This program is free software: ' +
|
||||||
|
'you can redistribute it and/or modify\n', '# it under the terms of the GNU General Public License as published by\n',
|
||||||
|
'# the Free Software Foundation, version 3 of the License.\n', '#\n', '# This program is distributed in the hope that it will be ' +
|
||||||
|
'useful,\n', '# but WITHOUT ANY WARRANTY; without even the implied warranty of\n', '# MERCHANTABILITY or FITNESS FOR A PARTICULAR ' +
|
||||||
|
'PURPOSE. See the\n', '# GNU General Public License for more details.\n', '#\n', '# You should have received a copy of the GNU ' +
|
||||||
|
'General Public License\n', '# along with this program. If not, see <http://www.gnu.org/licenses/>.\n', '\n', '# Configurations that ' +
|
||||||
|
'you can override; if you leave these commented out,\n', '# Byobu will try to auto-detect them.\n', '\n', '# This should be auto-detected ' +
|
||||||
|
'for most distro, but setting it here will save\n', '# some call to lsb_release and the like.\n', '#BYOBU_DISTRO=Ubuntu\n', '\n',
|
||||||
|
'# Default: depends on the distro (which is either auto-detected, either set\n', '# via $DISTRO)\n', '#LOGO="\\o/"\n', '\n', '# Abbreviate ' +
|
||||||
|
'the release to N characters\n', '# By default, this is disabled. But if you set RELEASE_ABBREVIATED=1\n', '# and your lsb_release is ' +
|
||||||
|
'"precise", only "p" will be displayed\n', '#RELEASE_ABBREVIATED=1\n', '\n', '# Default: /\n', '#MONITORED_DISK=/\n', '\n', '# Minimum ' +
|
||||||
|
'disk throughput that triggers the notification (in kB/s)\n', '# Default: 50\n', '#DISK_IO_THRESHOLD=50\n', '\n', '# Default: eth0\n',
|
||||||
|
'#MONITORED_NETWORK=eth0\n', '\n', '# Unit used for network throughput (either bits per second or bytes per second)\n', '# Default: ' +
|
||||||
|
'bits\n', '#NETWORK_UNITS=bytes\n', '\n', '# Minimum network throughput that triggers the notification (in kbit/s)\n', '# Default: 20\n',
|
||||||
|
'#NETWORK_THRESHOLD=20\n', '\n', '# You can add an additional source of temperature here\n', '#MONITORED_TEMP=/proc/acpi/thermal_zone/' +
|
||||||
|
'THM0/temperature\n', '\n', '# Default: C\n', '#TEMP=F\n', '\n', '#SERVICES="eucalyptus-nc|NC eucalyptus-cloud|CLC eucalyptus-walrus ' +
|
||||||
|
'eucalyptus-cc|CC eucalyptus-sc|SC"\n', '\n', '#FAN=$(find /sys -type f -name fan1_input | head -n1)\n', '\n', '# You can set this to 1 ' +
|
||||||
|
'to report your external/public ip address\n', '# Default: 0\n', '#IP_EXTERNAL=0\n', '\n', '# The users notification normally counts ssh ' +
|
||||||
|
"sessions; set this configuration to '1'\n", '# to instead count number of distinct users logged onto the system\n', '# Default: 0\n',
|
||||||
|
'#USERS_DISTINCT=0\n', '\n', '# Set this to zero to hide seconds int the time display\n', '# Default 1\n', '#TIME_SECONDS=0\n']
|
||||||
|
with open('{0}/statusrc'.format(b), 'w') as f:
|
||||||
|
f.write(''.join(statusrc))
|
||||||
|
setPerms(user, b)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def vim():
|
||||||
|
vimc = ['\n', 'set nocompatible\n', 'set number\n', 'syntax on\n', 'set paste\n', 'set ruler\n', 'if has("autocmd")\n',' au BufReadPost * if ' +
|
||||||
|
'line("\'\\"") > 1 && line("\'\\"") <= line("$") | exe "normal! g\'\\"" | endif\n', 'endif\n', '\n', '" bind F3 to insert a timestamp.\n', '" In ' +
|
||||||
|
'normal mode, insert.\n', 'nmap <F3> i<C-R>=strftime("%c")<CR><Esc>\n', '\n', 'set pastetoggle=<F2>\n', '\n', '" https://stackoverflow.com/' +
|
||||||
|
'questions/27771616/turn-off-all-automatic-code-complete-in-jedi-vim\n', 'let g:jedi#completions_enabled = 0\n', 'let g:jedi#show_call_' +
|
||||||
|
'signatures = "0"\n']
|
||||||
|
with open('/etc/vimrc', 'a') as f:
|
||||||
|
f.write(''.join(vimc))
|
||||||
|
setPerms('root', '/etc/vimrc')
|
||||||
|
return()
|
||||||
|
|
||||||
|
def bash():
|
||||||
|
bashc = ['\n', 'alias vi=/usr/bin/vim\n', 'export EDITOR=vim\n', '\n', 'if [ -f ~/.bashrc ];\n', 'then\n', ' source ~/.bashrc\n', 'fi \n',
|
||||||
|
'if [ -d ~/bin ];\n', 'then\n', ' export PATH="$PATH:~/bin"\n', 'fi\n', '\n', 'alias grep="grep --color"\n',
|
||||||
|
'alias egrep="egrep --color"\n', '\n', 'alias ls="ls --color=auto"\n', 'alias vi="/usr/bin/vim"\n', '\n', 'export HISTTIMEFORMAT="%F %T "\n',
|
||||||
|
'export PATH="${PATH}:/sbin:/bin:/usr/sbin"\n']
|
||||||
|
with open('/etc/bash.bashrc', 'a') as f:
|
||||||
|
f.write(''.join(bashc))
|
||||||
|
setPerms('root', '/etc/bash.bashrc')
|
||||||
|
return()
|
||||||
|
|
||||||
|
def mlocate():
|
||||||
|
subprocess.run(['updatedb'])
|
||||||
|
return()
|
||||||
|
|
||||||
|
def setPerms(user, path):
|
||||||
|
uid = pwd.getpwnam(user).pw_uid
|
||||||
|
gid = pwd.getpwnam(user).pw_gid
|
||||||
|
pl = pathlib.PurePath(path).parts
|
||||||
|
for basedir, dirs, files in os.walk(path):
|
||||||
|
os.chown(basedir, uid, gid)
|
||||||
|
if os.path.isdir(basedir):
|
||||||
|
os.chmod(basedir, 0o755)
|
||||||
|
elif os.path.isfile(basedir):
|
||||||
|
os.chmod(basedir, 0o644)
|
||||||
|
for f in files:
|
||||||
|
os.chown(os.path.join(basedir, f), uid, gid)
|
||||||
|
os.chmod(os.path.join(basedir, f), 0o644)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
byobu()
|
||||||
|
vim()
|
||||||
|
bash()
|
||||||
|
mlocate()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
206
aif/scripts/post/hostscan.py
Executable file
206
aif/scripts/post/hostscan.py
Executable file
@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Note: for hashed known-hosts, https://gist.github.com/maxtaco/5080023
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import grp
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
#def_supported_keys = subprocess.run(['ssh',
|
||||||
|
# '-Q',
|
||||||
|
# 'key'], stdout = subprocess.PIPE).stdout.decode('utf-8').splitlines()
|
||||||
|
def_supported_keys = ['dsa', 'ecdsa', 'ed25519', 'rsa']
|
||||||
|
def_mode = 'append'
|
||||||
|
def_syshostkeys = '/etc/ssh/ssh_known_hosts'
|
||||||
|
def_user = pwd.getpwuid(os.geteuid())[0]
|
||||||
|
def_grp = grp.getgrgid(os.getegid())[0]
|
||||||
|
|
||||||
|
|
||||||
|
class hostscanner(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
self.args = args
|
||||||
|
if self.args['keytypes'] == ['all']:
|
||||||
|
self.args['keytypes'] = def_supported_keys
|
||||||
|
if self.args['system']:
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
exit(('You have specified system-wide modification but ' +
|
||||||
|
'are not running with root privileges! Exiting.'))
|
||||||
|
self.args['output'] = def_syshostkeys
|
||||||
|
if self.args['output'] != sys.stdout:
|
||||||
|
_pardir = os.path.dirname(os.path.abspath(os.path.expanduser(self.args['output'])))
|
||||||
|
if _pardir.startswith('/home'):
|
||||||
|
_octmode = 0o700
|
||||||
|
else:
|
||||||
|
_octmode = 0o755
|
||||||
|
os.makedirs(_pardir, mode = _octmode, exist_ok = True)
|
||||||
|
os.chown(_pardir,
|
||||||
|
pwd.getpwnam(self.args['chown_user'])[2],
|
||||||
|
grp.getgrnam(self.args['chown_grp'])[2])
|
||||||
|
|
||||||
|
def getHosts(self):
|
||||||
|
self.keys = {}
|
||||||
|
_hosts = os.path.abspath(os.path.expanduser(self.args['infile']))
|
||||||
|
with open(_hosts, 'r') as f:
|
||||||
|
for l in f.readlines():
|
||||||
|
l = l.strip()
|
||||||
|
if re.search('^\s*(#.*)?$', l, re.MULTILINE):
|
||||||
|
continue # Skip commented and blank lines
|
||||||
|
k = re.sub('^([0-9a-z-\.]+)\s*#.*$',
|
||||||
|
'\g<1>',
|
||||||
|
l.strip().lower(),
|
||||||
|
re.MULTILINE)
|
||||||
|
self.keys[k] = []
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getKeys(self):
|
||||||
|
def parseType(k):
|
||||||
|
_newkey = re.sub('^ssh-', '', k).split('-')[0]
|
||||||
|
if _newkey == 'dss':
|
||||||
|
_newkey = 'dsa'
|
||||||
|
return(_newkey)
|
||||||
|
for h in list(self.keys.keys()):
|
||||||
|
_h = h.split(':')
|
||||||
|
if len(_h) == 1:
|
||||||
|
_host = _h[0]
|
||||||
|
_port = 22
|
||||||
|
elif len(_h) == 2:
|
||||||
|
_host = _h[0]
|
||||||
|
_port = int(_h[1])
|
||||||
|
_cmdline = ['ssh-keyscan',
|
||||||
|
'-t', ','.join(self.args['keytypes']),
|
||||||
|
'-p', str(_port),
|
||||||
|
_host]
|
||||||
|
if self.args['hash']:
|
||||||
|
#https://security.stackexchange.com/a/56283
|
||||||
|
# verify via:
|
||||||
|
# SAMPLE ENTRY: |1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg= ssh-rsa ...
|
||||||
|
#key=$(echo F1E1KeoE/eEWhi10WpGv4OdiO6Y= | base64 -d | xxd -p)
|
||||||
|
#echo -n "192.168.1.61" | openssl sha1 -mac HMAC -macopt hexkey:${key} | awk '{print $2}' | xxd -r -p | base64
|
||||||
|
_cmdline.insert(1, '-H')
|
||||||
|
_cmd = subprocess.run(_cmdline,
|
||||||
|
stdout = subprocess.PIPE,
|
||||||
|
stderr = subprocess.PIPE)
|
||||||
|
if not re.match('\s*#.*', _cmd.stderr.decode('utf-8')):
|
||||||
|
_printerr = []
|
||||||
|
for i in _cmd.stderr.decode('utf-8').splitlines():
|
||||||
|
if i.strip() not in _printerr:
|
||||||
|
_printerr.append(i.strip())
|
||||||
|
print('{0}: errors detected; skipping ({1})'.format(h, '\n'.join(_printerr)))
|
||||||
|
del(self.keys[h])
|
||||||
|
continue
|
||||||
|
for l in _cmd.stdout.decode('utf-8').splitlines():
|
||||||
|
_l = l.split()
|
||||||
|
_key = {'type': _l[1],
|
||||||
|
'host': _l[0],
|
||||||
|
'key': _l[2]}
|
||||||
|
if parseType(_key['type']) in self.args['keytypes']:
|
||||||
|
self.keys[h].append(_key)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
if self.args['writemode'] == 'replace':
|
||||||
|
if os.path.isfile(self.args['output']) and self.args['output'] != sys.stdout:
|
||||||
|
os.move(self.args['output'], os.path.join(self.args['output'], '.bak'))
|
||||||
|
for h in self.keys.keys():
|
||||||
|
for i in self.keys[h]:
|
||||||
|
_s = '# Automatically added via hostscan.py\n{0} {1} {2}\n'.format(i['host'],
|
||||||
|
i['type'],
|
||||||
|
i['key'])
|
||||||
|
if self.args['output'] == sys.stdout:
|
||||||
|
print(_s, end = '')
|
||||||
|
else:
|
||||||
|
with open(self.args['output'], 'a') as f:
|
||||||
|
f.write(_s)
|
||||||
|
os.chmod(self.args['output'], 0o644)
|
||||||
|
os.chown(self.args['output'],
|
||||||
|
pwd.getpwnam(self.args['chown_user'])[2],
|
||||||
|
grp.getgrnam(self.args['chown_grp'])[2])
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
def getTypes(t):
|
||||||
|
keytypes = t.split(',')
|
||||||
|
keytypes = [k.strip() for k in keytypes]
|
||||||
|
for k in keytypes:
|
||||||
|
if k not in ('all', *def_supported_keys):
|
||||||
|
raise argparse.ArgumentError('Must be one or more of the following: all, {0}'.format(', '.join(def_supported_keys)))
|
||||||
|
return(keytypes)
|
||||||
|
args = argparse.ArgumentParser(description = ('Scan a list of hosts and present their hostkeys in ' +
|
||||||
|
'a format suitable for an SSH known_hosts file.'))
|
||||||
|
args.add_argument('-u',
|
||||||
|
'--user',
|
||||||
|
dest = 'chown_user',
|
||||||
|
default = def_user,
|
||||||
|
help = ('The username to chown the file to (if \033[1m{0}\033[0m is specified). ' +
|
||||||
|
'Default: \033[1m{1}\033[0m').format('-o/--output', def_user))
|
||||||
|
args.add_argument('-g',
|
||||||
|
'--group',
|
||||||
|
dest = 'chown_grp',
|
||||||
|
default = def_grp,
|
||||||
|
help = ('The group to chown the file to (if \033[1m{0}\033[0m is specified). ' +
|
||||||
|
'Default: \033[1m{1}\033[0m').format('-o/--output', def_grp))
|
||||||
|
args.add_argument('-H',
|
||||||
|
'--hash',
|
||||||
|
dest = 'hash',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, hash the hostkeys (see ssh-keyscan(1)\'s -H option for more info)'))
|
||||||
|
args.add_argument('-m',
|
||||||
|
'--mode',
|
||||||
|
dest = 'writemode',
|
||||||
|
default = def_mode,
|
||||||
|
choices = ['append', 'replace'],
|
||||||
|
help = ('If \033[1m{0}\033[0m is specified, the mode to use for the ' +
|
||||||
|
'destination file. The default is \033[1m{1}\033[0m').format('-o/--output', def_mode))
|
||||||
|
args.add_argument('-k',
|
||||||
|
'--keytypes',
|
||||||
|
dest = 'keytypes',
|
||||||
|
type = getTypes,
|
||||||
|
default = 'all',
|
||||||
|
help = ('A comma-separated list of key types to add (if supported by the target host). ' +
|
||||||
|
'The default is to add all keys found. Must be one (or more) of: \033[1m{0}\033[0m').format(', '.join(def_supported_keys)))
|
||||||
|
args.add_argument('-o',
|
||||||
|
'--output',
|
||||||
|
default = sys.stdout,
|
||||||
|
metavar = 'OUTFILE',
|
||||||
|
dest = 'output',
|
||||||
|
help = ('If specified, write the hostkeys to \033[1m{0}\033[0m instead of ' +
|
||||||
|
'\033[1m{1}\033[0m (the default). ' +
|
||||||
|
'Overrides \033[1m{2}\033[0m').format('OUTFILE',
|
||||||
|
'stdout',
|
||||||
|
'-S/--system-wide'))
|
||||||
|
args.add_argument('-S',
|
||||||
|
'--system-wide',
|
||||||
|
dest = 'system',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, apply to the entire system (not just the ' +
|
||||||
|
'specified/running user) via {0}. ' +
|
||||||
|
'Requires \033[1m{1}\033[0m in /etc/ssh/ssh_config (usually ' +
|
||||||
|
'enabled silently by default) and running with root ' +
|
||||||
|
'privileges').format(def_syshostkeys,
|
||||||
|
'GlobalKnownHostsFile {0}'.format(def_syshostkeys)))
|
||||||
|
args.add_argument(metavar = 'HOSTLIST_FILE',
|
||||||
|
dest = 'infile',
|
||||||
|
help = ('The path to the list of hosts. Can contain blank lines and/or comments. ' +
|
||||||
|
'One host per line. Can be \033[1m{0}\033[0m (as long as it\'s resolvable), ' +
|
||||||
|
'\033[1m{1}\033[0m, or \033[1m{2}\033[0m. To specify an alternate port, ' +
|
||||||
|
'add \033[1m{3}\033[0m to the end (e.g. ' +
|
||||||
|
'"some.host.tld:22")').format('hostname',
|
||||||
|
'IP address',
|
||||||
|
'FQDN',
|
||||||
|
':<PORTNUM>'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
scan = hostscanner(args)
|
||||||
|
scan.getHosts()
|
||||||
|
scan.getKeys()
|
||||||
|
scan.write()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
31
aif/scripts/post/sshkeys.py
Normal file
31
aif/scripts/post/sshkeys.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
keysfile = 'https://square-r00t.net/ssh/all'
|
||||||
|
|
||||||
|
def copyKeys(keystring, user = 'root'):
|
||||||
|
uid = pwd.getpwnam(user).pw_uid
|
||||||
|
gid = pwd.getpwnam(user).pw_gid
|
||||||
|
homedir = os.path.expanduser('~{0}'.format(user))
|
||||||
|
sshdir = '{0}/.ssh'.format(homedir)
|
||||||
|
authfile = '{0}/authorized_keys'.format(sshdir)
|
||||||
|
os.makedirs(sshdir, mode = 0o700, exist_ok = True)
|
||||||
|
with open(authfile, 'a') as f:
|
||||||
|
f.write(keystring)
|
||||||
|
for basedir, dirs, files in os.walk(sshdir):
|
||||||
|
os.chown(basedir, uid, gid)
|
||||||
|
os.chmod(basedir, 0o700)
|
||||||
|
for f in files:
|
||||||
|
os.chown(os.path.join(basedir, f), uid, gid)
|
||||||
|
os.chmod(os.path.join(basedir, f), 0o600)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with urlopen(keysfile) as keys:
|
||||||
|
copyKeys(keys.read().decode('utf-8'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
428
aif/scripts/post/sshsecure.py
Normal file
428
aif/scripts/post/sshsecure.py
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Pythonized automated way of running https://sysadministrivia.com/news/hardening-ssh-security
|
||||||
|
# TODO: check for cryptography module. if it exists, we can do this entirely pythonically
|
||||||
|
# without ever needing to use subprocess/ssh-keygen, i think!
|
||||||
|
|
||||||
|
# Thanks to https://stackoverflow.com/a/39126754.
|
||||||
|
|
||||||
|
# Also, I need to re-write this. It's getting uglier.
|
||||||
|
|
||||||
|
# stdlib
|
||||||
|
import datetime
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import shutil
|
||||||
|
import subprocess # REMOVE WHEN SWITCHING TO PURE PYTHON
|
||||||
|
#### PREP FOR PURE PYTHON IMPLEMENTATION ####
|
||||||
|
# # non-stdlib - testing and automatic install if necessary.
|
||||||
|
# # TODO #
|
||||||
|
# - cryptography module won't generate new-format "openssh-key-v1" keys.
|
||||||
|
# - See https://github.com/pts/py_ssh_keygen_ed25519 for possible conversion to python 3
|
||||||
|
# - https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
|
||||||
|
# - https://github.com/pyca/cryptography/issues/3509 and https://github.com/paramiko/paramiko/issues/1136
|
||||||
|
# has_crypto = False
|
||||||
|
# pure_py = False
|
||||||
|
# has_pip = False
|
||||||
|
# pipver = None
|
||||||
|
# try:
|
||||||
|
# import cryptography
|
||||||
|
# has_crypto = True
|
||||||
|
# except ImportError:
|
||||||
|
# # We'll try to install it. We set up the logic below.
|
||||||
|
# try:
|
||||||
|
# import pip
|
||||||
|
# has_pip = True
|
||||||
|
# # We'll use these to create a temporary lib path and remove it when done.
|
||||||
|
# import sys
|
||||||
|
# import tempfile
|
||||||
|
# except ImportError:
|
||||||
|
# # ABSOLUTE LAST fallback, if we got to THIS case, is to use subprocess.
|
||||||
|
# has_pip = False
|
||||||
|
# import subprocess
|
||||||
|
#
|
||||||
|
# # Try installing it then!
|
||||||
|
# if not all((has_crypto, )):
|
||||||
|
# # venv only included after python 3.3.x. We fallback to subprocess if we can't do dis.
|
||||||
|
# if sys.hexversion >= 0x30300f0:
|
||||||
|
# has_ensurepip = False
|
||||||
|
# import venv
|
||||||
|
# if not has_pip and sys.hexversion >= 0x30400f0:
|
||||||
|
# import ensurepip
|
||||||
|
# has_ensurepip = True
|
||||||
|
# temppath = tempfile.mkdtemp('_VENV')
|
||||||
|
# v = venv.create(temppath)
|
||||||
|
# if has_ensurepip and not has_pip:
|
||||||
|
# # This SHOULD be unnecessary, but we want to try really hard.
|
||||||
|
# ensurepip.bootstrap(root = temppath)
|
||||||
|
# import pip
|
||||||
|
# has_pip = True
|
||||||
|
# if has_pip:
|
||||||
|
# pipver = pip.__version__.split('.')
|
||||||
|
# # A thousand people are yelling at me for this.
|
||||||
|
# if int(pipver[0]) >= 10:
|
||||||
|
# from pip._internal import main as pipinstall
|
||||||
|
# else:
|
||||||
|
# pipinstall = pip.main
|
||||||
|
# if int(pipver[0]) >= 8:
|
||||||
|
# pipcmd = ['install',
|
||||||
|
# '--prefix={0}'.format(temppath),
|
||||||
|
# '--ignore-installed']
|
||||||
|
# else:
|
||||||
|
# pipcmd = ['install',
|
||||||
|
# '--install-option="--prefix={0}"'.format(temppath),
|
||||||
|
# '--ignore-installed']
|
||||||
|
# # Get the lib path.
|
||||||
|
# libpath = os.path.join(temppath, 'lib')
|
||||||
|
# if os.path.exists('{0}64'.format(libpath)) and not os.path.islink('{0}64'.format(libpath)):
|
||||||
|
# libpath += '64'
|
||||||
|
# for i in os.listdir(libpath): # TODO: make this more sane. We cheat a bit here by making assumptions.
|
||||||
|
# if re.search('python([0-9]+(\.[0-9]+)?)?$', i):
|
||||||
|
# libpath = os.path.join(libpath, i)
|
||||||
|
# break
|
||||||
|
# libpath = os.path.join(libpath, 'site-packages')
|
||||||
|
# sys.prefix = temppath
|
||||||
|
# for m in ('cryptography', 'ed25519'):
|
||||||
|
# pipinstall(['install', 'cryptography'])
|
||||||
|
# sys.path.append(libpath)
|
||||||
|
# try:
|
||||||
|
# import cryptography
|
||||||
|
# has_crypto = True
|
||||||
|
# except ImportError: # All that trouble for nothin'. Shucks.
|
||||||
|
# pass
|
||||||
|
#
|
||||||
|
# if all((has_crypto, )):
|
||||||
|
# pure_py = True
|
||||||
|
#
|
||||||
|
# if pure_py:
|
||||||
|
# from cryptography.hazmat.primitives import serialization as crypto_serialization
|
||||||
|
# from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
# from cryptography.hazmat.backends import default_backend as crypto_default_backend
|
||||||
|
#
|
||||||
|
|
||||||
|
# We need static backup suffixes.
|
||||||
|
tstamp = int(datetime.datetime.utcnow().timestamp())
|
||||||
|
|
||||||
|
# TODO: associate various config directives with version, too.
|
||||||
|
# For now, we use this for primarily CentOS 6.x, which doesn't support ED25519 and probably some of the MACs.
|
||||||
|
# Bastards.
|
||||||
|
# https://ssh-comparison.quendi.de/comparison/cipher.html at some point in the future...
|
||||||
|
# TODO: maybe implement some parsing of the ssh -Q stuff? https://superuser.com/a/869005/984616
|
||||||
|
# If you encounter a version incompatibility, please let me know!
|
||||||
|
# nmap --script ssh2-enum-algos -PN -sV -p22 <host>
|
||||||
|
magic_ver = 6.5
|
||||||
|
ssh_ver = subprocess.run(['ssh', '-V'], stderr = subprocess.PIPE).stderr.decode('utf-8').strip().split()[0]
|
||||||
|
# FUCK YOU, DEBIAN. FUCK YOU AND ALL OF YOUR DERIVATIVES. YOU'RE FUCKING TRASH.
|
||||||
|
# YOU BELONG NOWHERE NEAR A DATACENTER.
|
||||||
|
ssh_ver = float(re.sub('^(?:Open|Sun_)SSH_([0-9\.]+)(?:p[0-9]+)?(?:,.*)?.*$', '\g<1>', ssh_ver))
|
||||||
|
if ssh_ver >= magic_ver:
|
||||||
|
has_ed25519 = True
|
||||||
|
supported_keys = ('ed25519', 'rsa')
|
||||||
|
new_moduli = False
|
||||||
|
else:
|
||||||
|
has_ed25519 = False
|
||||||
|
supported_keys = ('rsa', )
|
||||||
|
new_moduli = False
|
||||||
|
# https://github.com/openssh/openssh-portable/commit/3e60d18fba1b502c21d64fc7e81d80bcd08a2092
|
||||||
|
if ssh_ver >= 8.1:
|
||||||
|
new_moduli = True
|
||||||
|
|
||||||
|
|
||||||
|
conf_options = {}
|
||||||
|
conf_options['sshd'] = {'KexAlgorithms': 'diffie-hellman-group-exchange-sha256',
|
||||||
|
'Protocol': '2',
|
||||||
|
'HostKey': ['/etc/ssh/ssh_host_rsa_key'],
|
||||||
|
#'PermitRootLogin': 'prohibit-password', # older daemons don't like "prohibit-..."
|
||||||
|
'PermitRootLogin': 'without-password',
|
||||||
|
'PasswordAuthentication': 'no',
|
||||||
|
'ChallengeResponseAuthentication': 'no',
|
||||||
|
'PubkeyAuthentication': 'yes',
|
||||||
|
'Ciphers': 'aes256-ctr,aes192-ctr,aes128-ctr',
|
||||||
|
'MACs': 'hmac-sha2-512,hmac-sha2-256'}
|
||||||
|
if has_ed25519:
|
||||||
|
conf_options['sshd']['HostKey'].append('/etc/ssh/ssh_host_ed25519_key')
|
||||||
|
conf_options['sshd']['KexAlgorithms'] = ','.join(('curve25519-sha256@libssh.org',
|
||||||
|
conf_options['sshd']['KexAlgorithms']))
|
||||||
|
conf_options['sshd']['Ciphers'] = ','.join((('chacha20-poly1305@openssh.com,'
|
||||||
|
'aes256-gcm@openssh.com,'
|
||||||
|
'aes128-gcm@openssh.com'),
|
||||||
|
conf_options['sshd']['Ciphers']))
|
||||||
|
conf_options['sshd']['MACs'] = ','.join((('hmac-sha2-512-etm@openssh.com,'
|
||||||
|
'hmac-sha2-256-etm@openssh.com,'
|
||||||
|
'umac-128-etm@openssh.com'),
|
||||||
|
conf_options['sshd']['MACs'],
|
||||||
|
'umac-128@openssh.com'))
|
||||||
|
# Uncomment if this is further configured
|
||||||
|
#conf_options['sshd']['AllowGroups'] = 'ssh-user'
|
||||||
|
|
||||||
|
conf_options['ssh'] = {'Host': {'*': {'KexAlgorithms': 'diffie-hellman-group-exchange-sha256',
|
||||||
|
'PubkeyAuthentication': 'yes',
|
||||||
|
'HostKeyAlgorithms': 'ssh-rsa'}}}
|
||||||
|
if has_ed25519:
|
||||||
|
conf_options['ssh']['Host']['*']['KexAlgorithms'] = ','.join(('curve25519-sha256@libssh.org',
|
||||||
|
conf_options['ssh']['Host']['*']['KexAlgorithms']))
|
||||||
|
conf_options['ssh']['Host']['*']['HostKeyAlgorithms'] = ','.join(
|
||||||
|
(('ssh-ed25519-cert-v01@openssh.com,'
|
||||||
|
'ssh-rsa-cert-v01@openssh.com,'
|
||||||
|
'ssh-ed25519'),
|
||||||
|
conf_options['ssh']['Host']['*']['HostKeyAlgorithms']))
|
||||||
|
|
||||||
|
|
||||||
|
def hostKeys(buildmoduli):
|
||||||
|
# Starting haveged should help lessen the time load a non-negligible amount, especially on virtual platforms.
|
||||||
|
if os.path.lexists('/usr/bin/haveged'):
|
||||||
|
# We could use psutil here, but then that's a python dependency we don't need.
|
||||||
|
# We could parse the /proc directory, but that's quite unnecessary. pgrep's installed by default on
|
||||||
|
# most distros.
|
||||||
|
with open(os.devnull, 'wb') as devnull:
|
||||||
|
if subprocess.run(['pgrep', 'haveged'], stdout = devnull).returncode != 0:
|
||||||
|
subprocess.run(['haveged'], stdout = devnull)
|
||||||
|
#Warning: The moduli stuff takes a LONG time to run. Hours.
|
||||||
|
if buildmoduli:
|
||||||
|
if not new_moduli:
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-G', '/etc/ssh/moduli.all',
|
||||||
|
'-b', '4096',
|
||||||
|
'-q'])
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-T', '/etc/ssh/moduli.safe',
|
||||||
|
'-f', '/etc/ssh/moduli.all',
|
||||||
|
'-q'])
|
||||||
|
else:
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-q',
|
||||||
|
'-M', 'generate',
|
||||||
|
'-O', 'bits=4096',
|
||||||
|
'/etc/ssh/moduli.all'])
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-q',
|
||||||
|
'-M', 'screen',
|
||||||
|
'-f', '/etc/ssh/moduli.all',
|
||||||
|
'/etc/ssh/moduli.safe'])
|
||||||
|
if os.path.lexists('/etc/ssh/moduli'):
|
||||||
|
os.rename('/etc/ssh/moduli', '/etc/ssh/moduli.old')
|
||||||
|
os.rename('/etc/ssh/moduli.safe', '/etc/ssh/moduli')
|
||||||
|
os.remove('/etc/ssh/moduli.all')
|
||||||
|
for suffix in ('', '.pub'):
|
||||||
|
for k in glob.glob('/etc/ssh/ssh_host_*key{0}'.format(suffix)):
|
||||||
|
os.rename(k, '{0}.old.{1}'.format(k, tstamp))
|
||||||
|
if has_ed25519:
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-t', 'ed25519',
|
||||||
|
'-f', '/etc/ssh/ssh_host_ed25519_key',
|
||||||
|
'-q',
|
||||||
|
'-N', ''])
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-t', 'rsa',
|
||||||
|
'-b', '4096',
|
||||||
|
'-f', '/etc/ssh/ssh_host_rsa_key',
|
||||||
|
'-q',
|
||||||
|
'-N', ''])
|
||||||
|
# We currently don't use this, but for simplicity's sake let's return the host keys.
|
||||||
|
hostkeys = {}
|
||||||
|
for k in supported_keys:
|
||||||
|
with open('/etc/ssh/ssh_host_{0}_key.pub'.format(k), 'r') as f:
|
||||||
|
hostkeys[k] = f.read()
|
||||||
|
return(hostkeys)
|
||||||
|
|
||||||
|
def config(opts, t):
|
||||||
|
special = {'sshd': {}, 'ssh': {}}
|
||||||
|
# We need to handle these directives a little differently...
|
||||||
|
special['sshd']['opts'] = ['Match']
|
||||||
|
special['sshd']['filters'] = ['User', 'Group', 'Host', 'LocalAddress', 'LocalPort', 'Address']
|
||||||
|
# These are arguments supported by each of the special options. We'll use this to verify entries.
|
||||||
|
special['sshd']['args'] = ['AcceptEnv', 'AllowAgentForwarding', 'AllowGroups', 'AllowStreamLocalForwarding',
|
||||||
|
'AllowTcpForwarding', 'AllowUsers', 'AuthenticationMethods', 'AuthorizedKeysCommand',
|
||||||
|
'AuthorizedKeysCommandUser', 'AuthorizedKeysFile', 'AuthorizedPrincipalsCommand',
|
||||||
|
'AuthorizedPrincipalsCommandUser', 'AuthorizedPrincipalsFile', 'Banner',
|
||||||
|
'ChrootDirectory', 'ClientAliveCountMax', 'ClientAliveInterval', 'DenyGroups',
|
||||||
|
'DenyUsers', 'ForceCommand', 'GatewayPorts', 'GSSAPIAuthentication',
|
||||||
|
'HostbasedAcceptedKeyTypes', 'HostbasedAuthentication',
|
||||||
|
'HostbasedUsesNameFromPacketOnly', 'IPQoS', 'KbdInteractiveAuthentication',
|
||||||
|
'KerberosAuthentication', 'MaxAuthTries', 'MaxSessions', 'PasswordAuthentication',
|
||||||
|
'PermitEmptyPasswords', 'PermitOpen', 'PermitRootLogin', 'PermitTTY', 'PermitTunnel',
|
||||||
|
'PermitUserRC', 'PubkeyAcceptedKeyTypes', 'PubkeyAuthentication', 'RekeyLimit',
|
||||||
|
'RevokedKeys', 'StreamLocalBindMask', 'StreamLocalBindUnlink', 'TrustedUserCAKeys',
|
||||||
|
'X11DisplayOffset', 'X11Forwarding', 'X11UseLocalHost']
|
||||||
|
special['ssh']['opts'] = ['Host', 'Match']
|
||||||
|
special['ssh']['args'] = ['canonical', 'exec', 'host', 'originalhost', 'user', 'localuser']
|
||||||
|
cf = '/etc/ssh/{0}_config'.format(t)
|
||||||
|
shutil.copy2(cf, '{0}.bak.{1}'.format(cf, tstamp))
|
||||||
|
with open(cf, 'r') as f:
|
||||||
|
conf = f.readlines()
|
||||||
|
conf.append('\n\n# Added per https://sysadministrivia.com/news/hardening-ssh-security\n\n')
|
||||||
|
confopts = []
|
||||||
|
# Get an index of directives pre-existing in the config file.
|
||||||
|
for line in conf[:]:
|
||||||
|
opt = line.split()
|
||||||
|
if opt:
|
||||||
|
if not re.match('^(#.*|\s+.*)$', opt[0]):
|
||||||
|
confopts.append(opt[0])
|
||||||
|
# We also need to modify the config file- comment out starting with the first occurrence of the
|
||||||
|
# specopts, if it exists. This is why we make a backup.
|
||||||
|
commentidx = None
|
||||||
|
for idx, i in enumerate(conf):
|
||||||
|
if re.match('^({0})\s+.*$'.format('|'.join(special[t]['opts'])), i):
|
||||||
|
commentidx = idx
|
||||||
|
break
|
||||||
|
if commentidx is not None:
|
||||||
|
idx = commentidx
|
||||||
|
while idx <= (len(conf) - 1):
|
||||||
|
conf[idx] = '#{0}'.format(conf[idx])
|
||||||
|
idx += 1
|
||||||
|
# Now we actually start replacing/adding some major configuration.
|
||||||
|
for o in opts.keys():
|
||||||
|
if o in special[t]['opts'] or isinstance(opts[o], dict):
|
||||||
|
# We need to put these at the bottom of the file due to how they're handled by sshd's config parsing.
|
||||||
|
continue
|
||||||
|
# We handle these a little specially too- they're for multiple lines sharing the same directive.
|
||||||
|
# Since the config should be explicit, we remove any existing entries specified that we find.
|
||||||
|
else:
|
||||||
|
if o in confopts:
|
||||||
|
# If I was more worried about recursion, or if I was appending here, I should use conf[:].
|
||||||
|
# But I'm not. So I won't.
|
||||||
|
for idx, opt in enumerate(conf):
|
||||||
|
if re.match('^{0}(\s.*)?\n$'.format(o), opt):
|
||||||
|
conf[idx] = '#{0}'.format(opt)
|
||||||
|
# Here we handle the "multiple-specifying" options- notably, HostKey.
|
||||||
|
if isinstance(opts[o], list):
|
||||||
|
for l in opts[o]:
|
||||||
|
if l is not None:
|
||||||
|
conf.append('{0} {1}\n'.format(o, l))
|
||||||
|
else:
|
||||||
|
conf.append('{0}\n'.format(o))
|
||||||
|
else:
|
||||||
|
# So it isn't something we explicitly save until the end (such as a Match or Host),
|
||||||
|
# and it isn't something that's specified multiple times.
|
||||||
|
if opts[o] is not None:
|
||||||
|
conf.append('{0} {1}\n'.format(o, opts[o]))
|
||||||
|
else:
|
||||||
|
conf.append('{0}\n'.format(o))
|
||||||
|
# NOW we can add the Host/Match/etc. directives.
|
||||||
|
for o in opts.keys():
|
||||||
|
if isinstance(opts[o], dict):
|
||||||
|
for k in opts[o].keys():
|
||||||
|
conf.append('{0} {1}\n'.format(o, k))
|
||||||
|
for l in opts[o][k].keys():
|
||||||
|
if opts[o][k][l] is not None:
|
||||||
|
conf.append('\t{0} {1}\n'.format(l, opts[o][k][l]))
|
||||||
|
else:
|
||||||
|
conf.append('\t{0}\n'.format(l))
|
||||||
|
with open(cf, 'w') as f:
|
||||||
|
f.write(''.join(conf))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def clientKeys(user = 'root'):
|
||||||
|
uid = pwd.getpwnam(user).pw_uid
|
||||||
|
gid = pwd.getpwnam(user).pw_gid
|
||||||
|
homedir = os.path.expanduser('~{0}'.format(user))
|
||||||
|
sshdir = '{0}/.ssh'.format(homedir)
|
||||||
|
os.makedirs(sshdir, mode = 0o700, exist_ok = True)
|
||||||
|
if has_ed25519:
|
||||||
|
if not os.path.lexists('{0}/id_ed25519'.format(sshdir)) \
|
||||||
|
and not os.path.lexists('{0}/id_ed25519.pub'.format(sshdir)):
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-t', 'ed25519',
|
||||||
|
'-o',
|
||||||
|
'-a', '100',
|
||||||
|
'-f', '{0}/id_ed25519'.format(sshdir),
|
||||||
|
'-q',
|
||||||
|
'-N', ''])
|
||||||
|
if not os.path.lexists('{0}/id_rsa'.format(sshdir)) and not os.path.lexists('{0}/id_rsa.pub'.format(sshdir)):
|
||||||
|
if has_ed25519:
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-t', 'rsa',
|
||||||
|
'-b', '4096',
|
||||||
|
'-o',
|
||||||
|
'-a', '100',
|
||||||
|
'-f', '{0}/id_rsa'.format(sshdir),
|
||||||
|
'-q',
|
||||||
|
'-N', ''])
|
||||||
|
else:
|
||||||
|
subprocess.run(['ssh-keygen',
|
||||||
|
'-t', 'rsa',
|
||||||
|
'-b', '4096',
|
||||||
|
'-a', '100',
|
||||||
|
'-f', '{0}/id_rsa'.format(sshdir),
|
||||||
|
'-q',
|
||||||
|
'-N', ''])
|
||||||
|
for basedir, dirs, files in os.walk(sshdir):
|
||||||
|
os.chown(basedir, uid, gid)
|
||||||
|
os.chmod(basedir, 0o700)
|
||||||
|
for f in files:
|
||||||
|
os.chown(os.path.join(basedir, f), uid, gid)
|
||||||
|
os.chmod(os.path.join(basedir, f), 0o600)
|
||||||
|
if 'pubkeys' not in globals():
|
||||||
|
pubkeys = {}
|
||||||
|
pubkeys[user] = {}
|
||||||
|
for k in supported_keys:
|
||||||
|
with open('{0}/id_{1}.pub'.format(sshdir, k), 'r') as f:
|
||||||
|
pubkeys[user][k] = f.read()
|
||||||
|
return(pubkeys)
|
||||||
|
|
||||||
|
def daemonMgr():
|
||||||
|
# In case the script is running without sshd running.
|
||||||
|
pidfile = '/var/run/sshd.pid'
|
||||||
|
if not os.path.isfile(pidfile):
|
||||||
|
return()
|
||||||
|
# We're about to do somethin' stupid. Let's make it a teeny bit less stupid.
|
||||||
|
with open(os.devnull, 'w') as devnull:
|
||||||
|
confchk = subprocess.run(['sshd', '-T'], stdout = devnull)
|
||||||
|
if confchk.returncode != 0:
|
||||||
|
for suffix in ('', '.pub'):
|
||||||
|
for k in glob.glob('/etc/ssh/ssh_host_*key{0}'.format(suffix)):
|
||||||
|
os.rename('{0}.old.{1}'.format(k, tstamp), k)
|
||||||
|
for conf in ('', 'd'):
|
||||||
|
cf = '/etc/ssh/ssh{0}_config'.format(conf)
|
||||||
|
os.rename('{0}.{1}'.format(cf, tstamp),
|
||||||
|
cf)
|
||||||
|
exit('OOPS. We goofed. Backup restored and bailing out.')
|
||||||
|
# We need to restart sshd once we're done. I feel dirty doing this, but this is the most cross-platform way I can
|
||||||
|
# do it. First, we need the path to the PID file.
|
||||||
|
# TODO: do some kind of better way of doing this.
|
||||||
|
with open('/etc/ssh/sshd_config', 'r') as f:
|
||||||
|
for line in f.readlines():
|
||||||
|
if re.search('^\s*PidFile\s+.*', line):
|
||||||
|
pidfile = re.sub('^\s*PidFile\s+(.*)(#.*)?$', '\g<1>', line)
|
||||||
|
break
|
||||||
|
with open(pidfile, 'r') as f:
|
||||||
|
pid = int(f.read().strip())
|
||||||
|
os.kill(pid, signal.SIGHUP)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
self_pidfile = '/tmp/sshsecure.pid'
|
||||||
|
is_running = False
|
||||||
|
# First, check to see if we're already running.
|
||||||
|
# This is where I'd put a psutil call... IF I HAD ONE.
|
||||||
|
if os.path.isfile(self_pidfile):
|
||||||
|
is_running = subprocess.run(['pgrep', '-F', self_pidfile], stdout = subprocess.PIPE)
|
||||||
|
if is_running.stdout.decode('utf-8').strip() != '':
|
||||||
|
# We're still running. Exit gracefully.
|
||||||
|
print('We seem to still be running from a past execution; exiting')
|
||||||
|
exit(0)
|
||||||
|
else:
|
||||||
|
# It's a stale PID file.
|
||||||
|
os.remove(self_pidfile)
|
||||||
|
with open(self_pidfile, 'w') as f:
|
||||||
|
f.write(str(os.getpid()) + '\n')
|
||||||
|
_chkfile = '/etc/ssh/.aif-generated'
|
||||||
|
if not os.path.isfile(_chkfile):
|
||||||
|
# Warning: The moduli stuff can take a LONG time to run. Hours.
|
||||||
|
buildmoduli = True
|
||||||
|
hostKeys(buildmoduli)
|
||||||
|
for t in ('sshd', 'ssh'):
|
||||||
|
config(conf_options[t], t)
|
||||||
|
clientKeys()
|
||||||
|
with open(_chkfile, 'w') as f:
|
||||||
|
f.write(('ssh, sshd, and hostkey configurations/keys have been modified by sshsecure.py from OpTools.\n'
|
||||||
|
'https://git.square-r00t.net/OpTools/\n'))
|
||||||
|
daemonMgr()
|
||||||
|
os.remove(self_pidfile)
|
||||||
|
return()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
147
arch/arch_mirror_ranking.py
Executable file
147
arch/arch_mirror_ranking.py
Executable file
@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
# import dns # TODO: replace server['ipv4'] with IPv4 address(es)? etc.
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from urllib.request import urlopen
|
||||||
|
##
|
||||||
|
import iso3166
|
||||||
|
|
||||||
|
|
||||||
|
servers_json_url = 'https://www.archlinux.org/mirrors/status/json/'
|
||||||
|
protos = ('http', 'https', 'rsync')
|
||||||
|
|
||||||
|
|
||||||
|
class MirrorIdx(object):
|
||||||
|
def __init__(self, country = None, proto = None, is_active = None, json_url = servers_json_url,
|
||||||
|
name_re = None, ipv4 = None, ipv6 = None, isos = None, statuses = False, *args, **kwargs):
|
||||||
|
_tmpargs = locals()
|
||||||
|
del (_tmpargs['self'])
|
||||||
|
for k, v in _tmpargs.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
self.validateParams()
|
||||||
|
self.servers_json = {}
|
||||||
|
self.servers = []
|
||||||
|
self.servers_with_scores = []
|
||||||
|
self.ranked_servers = []
|
||||||
|
self.fetchJSON()
|
||||||
|
self.buildServers()
|
||||||
|
self.rankServers()
|
||||||
|
|
||||||
|
def fetchJSON(self):
|
||||||
|
if self.statuses:
|
||||||
|
sys.stderr.write('Fetching servers from {0}...\n'.format(self.json_url))
|
||||||
|
with urlopen(self.json_url) as u:
|
||||||
|
self.servers_json = json.load(u)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def buildServers(self):
|
||||||
|
_limiters = (self.proto, self.ipv4, self.ipv6, self.isos)
|
||||||
|
_filters = list(_limiters)
|
||||||
|
_filters.extend([self.name_re, self.country])
|
||||||
|
_filters = tuple(_filters)
|
||||||
|
if self.statuses:
|
||||||
|
sys.stderr.write('Applying filters (if any)...\n')
|
||||||
|
for s in self.servers_json['urls']:
|
||||||
|
# We handle these as "tri-value" (None, True, False)
|
||||||
|
if self.is_active is not None:
|
||||||
|
if s['active'] != self.is_active:
|
||||||
|
continue
|
||||||
|
if not any(_filters):
|
||||||
|
self.servers.append(s.copy())
|
||||||
|
if s['score']:
|
||||||
|
self.servers_with_scores.append(s)
|
||||||
|
continue
|
||||||
|
# These are based on string values.
|
||||||
|
if self.name_re:
|
||||||
|
if not self.name_re.search(s['url']):
|
||||||
|
continue
|
||||||
|
if self.country:
|
||||||
|
if self.country != s['country_code']:
|
||||||
|
continue
|
||||||
|
# These are regular True/False switches
|
||||||
|
match = False
|
||||||
|
# We want to be *very* explicit about the ordering and inclusion/exclusion of these.
|
||||||
|
# They MUST match the order of _limiters.
|
||||||
|
values = []
|
||||||
|
for k in ('protocol', 'ipv4', 'ipv6', 'isos'):
|
||||||
|
values.append(s[k])
|
||||||
|
valid = all([v for k, v in zip(_limiters, values) if k])
|
||||||
|
if valid:
|
||||||
|
self.servers.append(s)
|
||||||
|
if s['score']:
|
||||||
|
self.servers_with_scores.append(s)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def rankServers(self):
|
||||||
|
if self.statuses:
|
||||||
|
sys.stderr.write('Ranking mirrors...\n')
|
||||||
|
self.ranked_servers = sorted(self.servers_with_scores, key = lambda i: i['score'])
|
||||||
|
return()
|
||||||
|
|
||||||
|
def validateParams(self):
|
||||||
|
if self.proto and self.proto.lower() not in protos:
|
||||||
|
err = '{0} must be one of: {1}'.format(self.proto, ', '.join([i.upper() for i in protos]))
|
||||||
|
raise ValueError(err)
|
||||||
|
elif self.proto:
|
||||||
|
self.proto = self.proto.upper()
|
||||||
|
if self.country and self.country.upper() not in iso3166.countries:
|
||||||
|
err = ('{0} must be a valid ISO-3166-1 ALPHA-2 country code. '
|
||||||
|
'See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes'
|
||||||
|
'#Current_ISO_3166_country_codes').format(self.country)
|
||||||
|
raise ValueError()
|
||||||
|
elif self.country:
|
||||||
|
self.country = self.country.upper()
|
||||||
|
if self.name_re:
|
||||||
|
self.name_re = re.compile(self.name_re)
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = 'Fetch and rank Arch Linux mirrors')
|
||||||
|
args.add_argument('-c', '--country',
|
||||||
|
dest = 'country',
|
||||||
|
help = ('If specified, limit results to this country (in ISO-3166-1 ALPHA-2 format)'))
|
||||||
|
args.add_argument('-p', '--protocol',
|
||||||
|
choices = protos,
|
||||||
|
dest = 'proto',
|
||||||
|
help = ('If specified, limit results to this protocol'))
|
||||||
|
args.add_argument('-r', '--name-regex',
|
||||||
|
dest = 'name_re',
|
||||||
|
help = ('If specified, limit results to URLs that match this regex pattern (Python re syntax)'))
|
||||||
|
args.add_argument('-4', '--ipv4',
|
||||||
|
dest = 'ipv4',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, limit results to servers that support IPv4'))
|
||||||
|
args.add_argument('-6', '--ipv6',
|
||||||
|
dest = 'ipv6',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, limit results to servers that support IPv6'))
|
||||||
|
args.add_argument('-i', '--iso',
|
||||||
|
dest = 'isos',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, limit results to servers that have ISO images'))
|
||||||
|
is_active = args.add_mutually_exclusive_group()
|
||||||
|
is_active.add_argument('-a', '--active-only',
|
||||||
|
default = None,
|
||||||
|
const = True,
|
||||||
|
action = 'store_const',
|
||||||
|
dest = 'is_active',
|
||||||
|
help = ('If specified, only include active servers (default is active + inactive)'))
|
||||||
|
is_active.add_argument('-n', '--inactive-only',
|
||||||
|
default = None,
|
||||||
|
const = False,
|
||||||
|
action = 'store_const',
|
||||||
|
dest = 'is_active',
|
||||||
|
help = ('If specified, only include inactive servers (default is active + inactive)'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
m = MirrorIdx(**args, statuses = True)
|
||||||
|
for s in m.ranked_servers:
|
||||||
|
print('Server = {0}$repo/os/$arch'.format(s['url']))
|
165
arch/autopkg/maintain.py
Executable file
165
arch/autopkg/maintain.py
Executable file
@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import run
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = ('Modify (add/remove) packages for use with Autopkg'),
|
||||||
|
epilog = ('Operation-specific help; try e.g. "add --help"'))
|
||||||
|
commonargs = argparse.ArgumentParser(add_help = False)
|
||||||
|
commonargs.add_argument('-n', '--name',
|
||||||
|
dest = 'pkgnm',
|
||||||
|
required = True,
|
||||||
|
help = ('The name of the PACKAGE to operate on.'))
|
||||||
|
commonargs.add_argument('-d', '--db',
|
||||||
|
dest = 'dbfile',
|
||||||
|
default = '~/.optools/autopkg.sqlite3',
|
||||||
|
help = ('The location of the package database. THIS SHOULD NOT BE ANY FILE USED BY '
|
||||||
|
'ANYTHING ELSE! A default one will be created if it doesn\'t exist'))
|
||||||
|
subparsers = args.add_subparsers(help = ('Operation to perform'),
|
||||||
|
metavar = 'OPERATION',
|
||||||
|
dest = 'oper')
|
||||||
|
addargs = subparsers.add_parser('add',
|
||||||
|
parents = [commonargs],
|
||||||
|
help = ('Add a package. If a matching package NAME exists (-n/--name), '
|
||||||
|
'we\'ll replace it'))
|
||||||
|
addargs.add_argument('-b', '--base',
|
||||||
|
dest = 'pkgbase',
|
||||||
|
default = None,
|
||||||
|
help = ('The pkgbase; only really needed for split-packages and we will automatically '
|
||||||
|
'fetch if it\'s left blank anyways'))
|
||||||
|
addargs.add_argument('-v', '--version',
|
||||||
|
dest = 'pkgver',
|
||||||
|
default = None,
|
||||||
|
help = ('The current version; we will automatically fetch it if it\'s left blank'))
|
||||||
|
addargs.add_argument('-l', '--lock',
|
||||||
|
dest = 'active',
|
||||||
|
action = 'store_false',
|
||||||
|
help = ('If specified, the package will still exist in the DB but it will be marked inactive'))
|
||||||
|
rmargs = subparsers.add_parser('rm',
|
||||||
|
parents = [commonargs],
|
||||||
|
help = ('Remove a package from the DB'))
|
||||||
|
buildargs = subparsers.add_parser('build',
|
||||||
|
help = ('Build all packages; same effect as running run.py'))
|
||||||
|
buildargs.add_argument('-d', '--db',
|
||||||
|
dest = 'dbfile',
|
||||||
|
default = '~/.optools/autopkg.sqlite3',
|
||||||
|
help = ('The location of the package database. THIS SHOULD NOT BE ANY FILE USED BY '
|
||||||
|
'ANYTHING ELSE! A default one will be created if it doesn\'t exist'))
|
||||||
|
listargs = subparsers.add_parser('ls',
|
||||||
|
help = ('List packages (and information about them) only'))
|
||||||
|
listargs.add_argument('-d', '--db',
|
||||||
|
dest = 'dbfile',
|
||||||
|
default = '~/.optools/autopkg.sqlite3',
|
||||||
|
help = ('The location of the package database. THIS SHOULD NOT BE ANY FILE USED BY '
|
||||||
|
'ANYTHING ELSE! A default one will be created if it doesn\'t exist'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def add(args):
|
||||||
|
db = sqlite3.connect(args['dbfile'])
|
||||||
|
db.row_factory = sqlite3.Row
|
||||||
|
cur = db.cursor()
|
||||||
|
if not all((args['pkgbase'], args['pkgver'])):
|
||||||
|
# We need some additional info from the AUR API...
|
||||||
|
aur_url = 'https://aur.archlinux.org/rpc/?v=5&type=info&by=name&arg%5B%5D={0}'.format(args['pkgnm'])
|
||||||
|
with urlopen(aur_url) as url:
|
||||||
|
aur = json.loads(url.read().decode('utf-8'))['results']
|
||||||
|
if not aur:
|
||||||
|
raise ValueError(('Either something is screwy with our network access '
|
||||||
|
'or the package {0} doesn\'t exist').format(args['pkgnm']))
|
||||||
|
if ((aur['PackageBase'] != aur['Name']) and (not args['pkgbase'])):
|
||||||
|
args['pkgbase'] = aur['PackageBase']
|
||||||
|
if not args['pkgver']:
|
||||||
|
args['pkgver'] = aur['Version']
|
||||||
|
cur.execute("SELECT id, pkgname, pkgbase, pkgver, active FROM packages WHERE pkgname = ?",
|
||||||
|
(args['pkgnm'], ))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
if args['pkgbase']:
|
||||||
|
q = ("UPDATE packages SET pkgbase = ? AND pkgver = ? AND ACTIVE = ? WHERE id = ?",
|
||||||
|
(args['pkgbase'], args['pkgver'], ('0' if args['lock'] else '1'), row['id']))
|
||||||
|
else:
|
||||||
|
q = ("UPDATE packages SET pkgver = ? AND ACTIVE = ? WHERE id = ?",
|
||||||
|
(args['pkgver'], ('0' if args['lock'] else '1'), row['id']))
|
||||||
|
else:
|
||||||
|
if args['pkgbase']:
|
||||||
|
q = (("INSERT INTO "
|
||||||
|
"packages (pkgname, pkgbase, pkgver, active) "
|
||||||
|
"VALUES (?, ?, ?, ?)"),
|
||||||
|
(args['pkgnm'], args['pkgbase'], args['pkgver'], ('0' if args['lock'] else '1')))
|
||||||
|
else:
|
||||||
|
q = (("INSERT INTO "
|
||||||
|
"packages (pkgname, pkgver, active) "
|
||||||
|
"VALUES (?, ?, ?)"),
|
||||||
|
(args['pkgnm'], args['pkgver'], ('0' if args['lock'] else '1')))
|
||||||
|
cur.execute(*q)
|
||||||
|
db.commit()
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def rm(args):
|
||||||
|
db = sqlite3.connect(args['dbfile'])
|
||||||
|
cur = db.cursor()
|
||||||
|
cur.execute("DELETE FROM packages WHERE pkgname = ?",
|
||||||
|
(args['pkgnm'], ))
|
||||||
|
db.commit()
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def build(args):
|
||||||
|
pm = run.PkgMake(db = args['dbfile'])
|
||||||
|
pm.main()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def ls(args):
|
||||||
|
db = sqlite3.connect(args['dbfile'])
|
||||||
|
db.row_factory = sqlite3.Row
|
||||||
|
cur = db.cursor()
|
||||||
|
rows = []
|
||||||
|
cur.execute("SELECT * FROM packages ORDER BY pkgname")
|
||||||
|
for r in cur.fetchall():
|
||||||
|
pkgnm = r['pkgname']
|
||||||
|
rows.append({'name': r['pkgname'],
|
||||||
|
'row_id': r['id'],
|
||||||
|
'pkgbase': ('' if not r['pkgbase'] else r['pkgbase']),
|
||||||
|
'ver': r['pkgver'],
|
||||||
|
'enabled': ('Yes' if r['active'] else 'No')})
|
||||||
|
header = '| NAME | PACKAGE BASE | VERSION | ENABLED | ROW ID |'
|
||||||
|
sep = '=' * len(header)
|
||||||
|
fmt = '|{name:<16}|{pkgbase:<16}|{ver:^9}|{enabled:^9}|{row_id:<8}|'
|
||||||
|
out = []
|
||||||
|
for row in rows:
|
||||||
|
out.append(fmt.format(**row))
|
||||||
|
header = '\n'.join((sep, header, sep))
|
||||||
|
out.insert(0, header)
|
||||||
|
out.append(sep)
|
||||||
|
print('\n'.join(out))
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rawargs = parseArgs()
|
||||||
|
args = vars(rawargs.parse_args())
|
||||||
|
if not args['oper']:
|
||||||
|
rawargs.print_help()
|
||||||
|
exit()
|
||||||
|
args['dbfile'] = os.path.abspath(os.path.expanduser(args['dbfile']))
|
||||||
|
if args['oper'] == 'add':
|
||||||
|
add(args)
|
||||||
|
elif args['oper'] == 'rm':
|
||||||
|
rm(args)
|
||||||
|
elif args['oper'] == 'build':
|
||||||
|
build(args)
|
||||||
|
elif args['oper'] == 'ls':
|
||||||
|
ls(args)
|
||||||
|
return()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
278
arch/autopkg/run.py
Executable file
278
arch/autopkg/run.py
Executable file
@ -0,0 +1,278 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import grp
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import urllib.request as reqs
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
import setup
|
||||||
|
# I *HATE* relying on non-stlib, and I hate even MORE that this is JUST TO COMPARE VERSION STRINGS.
|
||||||
|
# WHY IS THIS FUNCTIONALITY NOT STDLIB YET.
|
||||||
|
try:
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
has_lv = True
|
||||||
|
except ImportError:
|
||||||
|
has_lv = False
|
||||||
|
|
||||||
|
# The base API URL (https://wiki.archlinux.org/index.php/Aurweb_RPC_interface)
|
||||||
|
aur_base = 'https://aur.archlinux.org/rpc/?v=5&type=info&by=name'
|
||||||
|
# The length of the above. Important because of uri_limit.
|
||||||
|
base_len = len(aur_base)
|
||||||
|
# Maximum length of the URI.
|
||||||
|
uri_limit = 4443
|
||||||
|
|
||||||
|
class PkgMake(object):
|
||||||
|
def __init__(self, db = '~/.optools/autopkg.sqlite3'):
|
||||||
|
db = os.path.abspath(os.path.expanduser(db))
|
||||||
|
if not os.path.isfile(db):
|
||||||
|
setup.firstrun(db)
|
||||||
|
self.conn = sqlite3.connect(db)
|
||||||
|
self.conn.row_factory = sqlite3.Row
|
||||||
|
self.cur = self.conn.cursor()
|
||||||
|
self.cfg = setup.main(self.conn, self.cur)
|
||||||
|
if self.cfg['sign']:
|
||||||
|
_cmt_mode = self.conn.isolation_level # autocommit
|
||||||
|
self.conn.isolation_level = None
|
||||||
|
self.fpr, self.gpg = setup.GPG(self.cur, homedir = self.cfg['gpg_homedir'], keyid = self.cfg['gpg_keyid'])
|
||||||
|
self.conn.isolation_level = _cmt_mode
|
||||||
|
# don't need this anymore; it should be duplicated or populated into self.fpr.
|
||||||
|
del(self.cfg['gpg_keyid'])
|
||||||
|
self.my_key = self.gpg.get_key(self.fpr, secret = True)
|
||||||
|
self.gpg.signers = [self.my_key]
|
||||||
|
else:
|
||||||
|
self.fpr = self.gpg = self.my_key = None
|
||||||
|
del(self.cfg['gpg_keyid'])
|
||||||
|
self.pkgs = {}
|
||||||
|
self._populatePkgs()
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
self.getPkg()
|
||||||
|
self.buildPkg()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _chkver(self, pkgbase):
|
||||||
|
new_ver = self.pkgs[pkgbase]['meta']['new_ver']
|
||||||
|
old_ver = self.pkgs[pkgbase]['meta']['pkgver']
|
||||||
|
is_diff = (new_ver != old_ver) # A super-stupid fallback
|
||||||
|
if is_diff:
|
||||||
|
if has_lv:
|
||||||
|
is_diff = LooseVersion(new_ver) > LooseVersion(old_ver)
|
||||||
|
else:
|
||||||
|
# like, 90% of the time, this would work.
|
||||||
|
new_tuple = tuple(map(int, (re.split('\.|-', new_ver))))
|
||||||
|
old_tuple = tuple(map(int, (re.split('\.|-', old_ver))))
|
||||||
|
# But people at https://stackoverflow.com/a/11887825/733214 are very angry about it, hence the above.
|
||||||
|
is_diff = new_tuple > old_tuple
|
||||||
|
return(is_diff)
|
||||||
|
|
||||||
|
def _populatePkgs(self):
|
||||||
|
# These columns/keys are inferred by structure or unneeded. Applies to both DB and AUR API.
|
||||||
|
_notrack = ('pkgbase', 'pkgname', 'active', 'id', 'packagebaseid', 'numvotes', 'popularity', 'outofdate',
|
||||||
|
'maintainer', 'firstsubmitted', 'lastmodified', 'depends', 'optdepends', 'conflicts', 'license',
|
||||||
|
'keywords')
|
||||||
|
_attr_map = {'version': 'new_ver'}
|
||||||
|
# These are tracked per-package; all others are pkgbase and applied to all split pkgs underneath.
|
||||||
|
_pkg_specific = ('pkgdesc', 'arch', 'url', 'license', 'groups', 'depends', 'optdepends', 'provides',
|
||||||
|
'conflicts', 'replaces', 'backup', 'options', 'install', 'changelog')
|
||||||
|
_aur_results = []
|
||||||
|
_urls = []
|
||||||
|
_params = {'arg[]': []}
|
||||||
|
_tmp_params = {'arg[]': []}
|
||||||
|
self.cur.execute("SELECT * FROM packages WHERE active = '1'")
|
||||||
|
for row in self.cur.fetchall():
|
||||||
|
pkgbase = (row['pkgbase'] if row['pkgbase'] else row['pkgname'])
|
||||||
|
pkgnm = row['pkgname']
|
||||||
|
if pkgbase not in self.pkgs:
|
||||||
|
self.pkgs[pkgbase] = {'packages': {pkgnm: {}},
|
||||||
|
'meta': {}}
|
||||||
|
for k in dict(row):
|
||||||
|
if not k:
|
||||||
|
continue
|
||||||
|
if k in _notrack:
|
||||||
|
continue
|
||||||
|
if k in _pkg_specific:
|
||||||
|
self.pkgs[pkgbase]['packages'][pkgnm][k] = row[k]
|
||||||
|
else:
|
||||||
|
if k not in self.pkgs[pkgbase]['meta']:
|
||||||
|
self.pkgs[pkgbase]['meta'][k] = row[k]
|
||||||
|
# TODO: change this?
|
||||||
|
pkgstr = urlparse.quote(pkgnm) # We perform against a non-pkgbased name for the AUR search.
|
||||||
|
_tmp_params['arg[]'].append(pkgstr)
|
||||||
|
l = base_len + (len(urlparse.urlencode(_tmp_params, doseq = True)) + 1)
|
||||||
|
if l >= uri_limit:
|
||||||
|
# We need to split into multiple URIs based on URI size because of:
|
||||||
|
# https://wiki.archlinux.org/index.php/Aurweb_RPC_interface#Limitations
|
||||||
|
_urls.append('&'.join((aur_base, urlparse.urlencode(_params, doseq = True))))
|
||||||
|
_params = {'arg[]': []}
|
||||||
|
_tmp_params = {'arg[]': []}
|
||||||
|
_params['arg[]'].append(pkgstr)
|
||||||
|
_urls.append('&'.join((aur_base, urlparse.urlencode(_params, doseq = True))))
|
||||||
|
for url in _urls:
|
||||||
|
with reqs.urlopen(url) as u:
|
||||||
|
_aur_results.extend(json.loads(u.read().decode('utf-8'))['results'])
|
||||||
|
for pkg in _aur_results:
|
||||||
|
pkg = {k.lower(): v for (k, v) in pkg.items()}
|
||||||
|
pkgnm = pkg['name']
|
||||||
|
pkgbase = pkg['packagebase']
|
||||||
|
for (k, v) in pkg.items():
|
||||||
|
if k in _notrack:
|
||||||
|
continue
|
||||||
|
if k in _attr_map:
|
||||||
|
k = _attr_map[k]
|
||||||
|
if k in _pkg_specific:
|
||||||
|
self.pkgs[pkgbase]['packages'][pkgnm][k] = v
|
||||||
|
else:
|
||||||
|
self.pkgs[pkgbase]['meta'][k] = v
|
||||||
|
self.pkgs[pkgbase]['meta']['snapshot'] = 'https://aur.archlinux.org{0}'.format(pkg['urlpath'])
|
||||||
|
self.pkgs[pkgbase]['meta']['filename'] = os.path.basename(pkg['urlpath'])
|
||||||
|
self.pkgs[pkgbase]['meta']['build'] = self._chkver(pkgbase)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _drop_privs(self):
|
||||||
|
# First get the list of groups to assign.
|
||||||
|
# This *should* generate a list *exactly* like as if that user ran os.getgroups(),
|
||||||
|
# with the addition of self.cfg['build_user']['gid'] (if it isn't included already).
|
||||||
|
newgroups = list(sorted([g.gr_gid
|
||||||
|
for g in grp.getgrall()
|
||||||
|
if pwd.getpwuid(self.cfg['build_user']['uid'])
|
||||||
|
in g.gr_mem]))
|
||||||
|
if self.cfg['build_user']['gid'] not in newgroups:
|
||||||
|
newgroups.append(self.cfg['build_user']['gid'])
|
||||||
|
newgroups.sort()
|
||||||
|
# This is the user's "primary group"
|
||||||
|
user_gid = pwd.getpwuid(self.cfg['build_user']['uid']).pw_gid
|
||||||
|
if user_gid not in newgroups:
|
||||||
|
newgroups.append(user_gid)
|
||||||
|
os.setgroups(newgroups)
|
||||||
|
# If we used os.setgid and os.setuid, we would PERMANENTLY/IRREVOCABLY drop privs.
|
||||||
|
# Being that that doesn't suit the meta of the rest of the script (chmodding, etc.) - probably not a good idea.
|
||||||
|
os.setresgid(self.cfg['build_user']['gid'], self.cfg['build_user']['gid'], -1)
|
||||||
|
os.setresuid(self.cfg['build_user']['uid'], self.cfg['build_user']['uid'], -1)
|
||||||
|
# Default on most linux systems. reasonable enough for building? (equal to chmod 755/644)
|
||||||
|
os.umask(0o0022)
|
||||||
|
# TODO: we need a full env construction here, I think, as well. PATH, HOME, GNUPGHOME at the very least?
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _restore_privs(self):
|
||||||
|
os.setresuid(self.cfg['orig_user']['uid'], self.cfg['orig_user']['uid'], self.cfg['orig_user']['uid'])
|
||||||
|
os.setresgid(self.cfg['orig_user']['gid'], self.cfg['orig_user']['gid'], self.cfg['orig_user']['gid'])
|
||||||
|
os.setgroups(self.cfg['orig_user']['groups'])
|
||||||
|
os.umask(self.cfg['orig_user']['umask'])
|
||||||
|
# TODO: if we change the env, we need to change it back here. I capture it in self.cfg['orig_user']['env'].
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getPkg(self):
|
||||||
|
self._drop_privs()
|
||||||
|
for pkgbase in self.pkgs:
|
||||||
|
if not self.pkgs[pkgbase]['meta']['build']:
|
||||||
|
continue
|
||||||
|
_pkgre = re.compile('^(/?.*/)*({0})/?'.format(pkgbase))
|
||||||
|
builddir = os.path.join(self.cfg['cache'], pkgbase)
|
||||||
|
try:
|
||||||
|
shutil.rmtree(builddir)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# We *could* use ignore_errors or onerrors params, but we only want FileNotFoundError.
|
||||||
|
pass
|
||||||
|
os.makedirs(builddir, mode = self.cfg['chmod']['dirs'], exist_ok = True)
|
||||||
|
tarball = os.path.join(builddir, self.pkgs[pkgbase]['meta']['filename'])
|
||||||
|
with reqs.urlopen(self.pkgs[pkgbase]['meta']['snapshot']) as url:
|
||||||
|
# We have to write out to disk first because the tarfile module HATES trying to perform seeks on
|
||||||
|
# a tarfile stream. It HATES it.
|
||||||
|
with open(tarball, 'wb') as f:
|
||||||
|
f.write(url.read())
|
||||||
|
tarnames = {}
|
||||||
|
with tarfile.open(tarball, mode = 'r:*') as tar:
|
||||||
|
for i in tar.getmembers():
|
||||||
|
if any((i.isdir(), i.ischr(), i.isblk(), i.isfifo(), i.isdev())):
|
||||||
|
continue
|
||||||
|
if i.name.endswith('.gitignore'):
|
||||||
|
continue
|
||||||
|
# We want to strip leading dirs out.
|
||||||
|
tarnames[i.name] = _pkgre.sub('', i.name)
|
||||||
|
# Small bugfix.
|
||||||
|
if tarnames[i.name] == '':
|
||||||
|
tarnames[i.name] = os.path.basename(i.name)
|
||||||
|
tarnames[i.name] = os.path.join(builddir, tarnames[i.name])
|
||||||
|
for i in tar.getmembers():
|
||||||
|
if i.name in tarnames:
|
||||||
|
# GOLLY I WISH TARFILE WOULD LET US JUST CHANGE THE ARCNAME DURING EXTRACTION ON THE FLY.
|
||||||
|
with open(tarnames[i.name], 'wb') as f:
|
||||||
|
f.write(tar.extractfile(i.name).read())
|
||||||
|
# No longer needed, so clean it up behind us.
|
||||||
|
os.remove(tarball)
|
||||||
|
self._restore_privs()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def buildPkg(self):
|
||||||
|
self._drop_privs()
|
||||||
|
for pkgbase in self.pkgs:
|
||||||
|
if not self.pkgs[pkgbase]['meta']['build']:
|
||||||
|
continue
|
||||||
|
builddir = os.path.join(self.cfg['cache'], pkgbase)
|
||||||
|
os.chdir(builddir)
|
||||||
|
# subprocess.run(['makepkg']) # TODO: figure out gpg sig checking?
|
||||||
|
subprocess.run(['makepkg', '--clean', '--force', '--skippgpcheck'])
|
||||||
|
self._restore_privs()
|
||||||
|
for pkgbase in self.pkgs:
|
||||||
|
if not self.pkgs[pkgbase]['meta']['build']:
|
||||||
|
continue
|
||||||
|
builddir = os.path.join(self.cfg['cache'], pkgbase)
|
||||||
|
# The i686 isn't even supported anymore, but let's keep this friendly for Archlinux32 folks.
|
||||||
|
_pkgre = re.compile(('^({0})-{1}-'
|
||||||
|
'(x86_64|i686|any)'
|
||||||
|
'\.pkg\.tar\.xz$').format('|'.join(self.pkgs[pkgbase]['packages'].keys()),
|
||||||
|
self.pkgs[pkgbase]['meta']['new_ver']))
|
||||||
|
fname = None
|
||||||
|
# PROBABLY in the first root dir, and could be done with fnmatch, but...
|
||||||
|
for root, dirs, files in os.walk(builddir):
|
||||||
|
for f in files:
|
||||||
|
if _pkgre.search(f):
|
||||||
|
fname = os.path.join(root, f)
|
||||||
|
break
|
||||||
|
if not fname:
|
||||||
|
raise RuntimeError('Could not find proper package build filename for {0}'.format(pkgbase))
|
||||||
|
destfile = os.path.join(self.cfg['dest'], os.path.basename(fname))
|
||||||
|
os.rename(fname, destfile)
|
||||||
|
# TODO: HERE IS WHERE WE SIGN THE PACKAGE?
|
||||||
|
# We also need to update the package info in the DB.
|
||||||
|
for p in self.pkgs[pkgbase]['packages']:
|
||||||
|
self.cur.execute("UPDATE packages SET pkgver = ? WHERE pkgname = ?",
|
||||||
|
(self.pkgs[pkgbase]['meta']['new_ver'], p))
|
||||||
|
self.cfg['pkgpaths'].append(destfile)
|
||||||
|
# No longer needed, so we can clear out the build directory.
|
||||||
|
shutil.rmtree(builddir)
|
||||||
|
os.chdir(self.cfg['dest'])
|
||||||
|
dbfile = os.path.join(self.cfg['dest'], 'autopkg.db.tar.gz') # TODO: Custom repo name?
|
||||||
|
cmd = ['repo-add', '--nocolor', '--delta', dbfile] # -s/--sign?
|
||||||
|
cmd.extend(self.cfg['pkgpaths'])
|
||||||
|
subprocess.run(cmd)
|
||||||
|
for root, dirs, files in os.walk(self.cfg['dest']):
|
||||||
|
for f in files:
|
||||||
|
fpath = os.path.join(root, f)
|
||||||
|
os.chmod(fpath, self.cfg['chmod']['files'])
|
||||||
|
os.chown(fpath, self.cfg['chown']['uid'], self.cfg['chown']['gid'])
|
||||||
|
for d in dirs:
|
||||||
|
dpath = os.path.join(root, d)
|
||||||
|
os.chmod(dpath, self.cfg['chmod']['dirs'])
|
||||||
|
os.chown(dpath, self.cfg['chown']['uid'], self.cfg['chown']['gid'])
|
||||||
|
return()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.cur:
|
||||||
|
self.cur.close()
|
||||||
|
if self.conn:
|
||||||
|
self.conn.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
pm = PkgMake()
|
||||||
|
pm.main()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
127
arch/autopkg/setup.py
Executable file
127
arch/autopkg/setup.py
Executable file
@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import copy
|
||||||
|
import gpg
|
||||||
|
import grp
|
||||||
|
import json
|
||||||
|
import lzma
|
||||||
|
import os
|
||||||
|
import pwd
|
||||||
|
import re
|
||||||
|
from socket import gethostname
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# NOTE: The gpg homedir should be owned by the user *running autopkg*.
|
||||||
|
# Likely priv-dropping will only work for root.
|
||||||
|
#
|
||||||
|
|
||||||
|
dirs = ('cache', 'dest', 'gpg_homedir')
|
||||||
|
u_g_pairs = ('chown', 'build_user')
|
||||||
|
json_vals = ('chmod', )
|
||||||
|
|
||||||
|
blank_db = """
|
||||||
|
/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4H//AxNdACmURZ1gyBn4JmSIjib+MZX9x4eABpe77H+o
|
||||||
|
CX2bysoKzO/OaDh2QGbNjiU75tmhPrWMvTFue4XOq+6NPls33xRRL8eZoITBdAaLqbwYY2XW/V/X
|
||||||
|
Gx8vpjcBnpACjVno40FoJ1qWxJlBZ0PI/8gMoBr3Sgdqnf+Bqi+E6dOl66ktJMRr3bdZ5C9vOXAf
|
||||||
|
42BtRfwJlwN8NItaWtfRYVfXl+40D05dugcxDLY/3uUe9MSgt46Z9+Q9tGjjrUA8kb5K2fqWSlQ2
|
||||||
|
6KyF3KV1zsJSDLuaRkP42JNsBTgg6mU5rEk/3egdJiLn+7AupvWQ3YlKkeALZvgEKy75wdObf6QI
|
||||||
|
jY4qjXjxOTwOG4oou7lNZ3fPI5qLCQL48M8ZbOQoTAQCuArdYqJmBwT2rF86SdQRP4EY6TlExa4o
|
||||||
|
+E+v26hKhYXO7o188jlmGFbuzqtoyMB1y3UG+Hi2SjPDilD5o6f9fEjiHZm2FY6rkPb9Km4UFlH1
|
||||||
|
d2A4Wt4iGlciZBs0lFRPKkgHR4s7KHTMKuZyC08qE1B7FwvyBTBBYveA2UoZlKY7d22IbiiSQ3tP
|
||||||
|
JKhj8nf8EWcgHPt46Juo80l7vqqn6AviY7b1JZXICdiJMbuWJEyzTLWuk4qlUBfimP7k9IjhDFpJ
|
||||||
|
gEXdNgrnx/wr5CIbr1T5lI9vZz35EacgNA2bGxLA8VI0W9eYDts3BSfhiJOHWwLQPiNzJwd4aeM1
|
||||||
|
IhqgTEpk+BD0nIgSB3AAB+NfJJavoQjpv0QBA6dH52utA5Nw5L//Ufw/YKaA7ui8YQyDJ7y2n9L3
|
||||||
|
ugn6VJFFrYSgIe1oRkJBGRGuBgGNTS3aJmdFqEz1vjZBMkFdF+rryXzub4dst2Qh01E6/elowIUh
|
||||||
|
2whMRVDO28QjyS9tLtLLzfTmBk2NSxs4+znE0ePKKw3n/p6YlbPRAw24QR8MTCOpQ2lH1UZNWBM2
|
||||||
|
epxfmWtgO5b/wGYopRDEvDDdbPAq6+4zxTOT5RmdWZyc46gdizf9+dQW3wZ9iBDjh4MtuYPvLlqr
|
||||||
|
0GRmsyrxgFxkwvVoXASNndS0NPcAADkAhYCxn+W2AAGvBoCAAgB/TQWascRn+wIAAAAABFla
|
||||||
|
"""
|
||||||
|
|
||||||
|
def firstrun(dbfile):
|
||||||
|
dbdata = lzma.decompress(base64.b64decode(blank_db))
|
||||||
|
with open(dbfile, 'wb') as f:
|
||||||
|
f.write(dbdata)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def main(connection, cursor):
|
||||||
|
cfg = {'orig_cwd': os.getcwd(),
|
||||||
|
'pkgpaths': []}
|
||||||
|
cursor.execute("SELECT directive, value FROM config")
|
||||||
|
for r in cursor.fetchall():
|
||||||
|
cfg[r['directive']] = r['value'].strip()
|
||||||
|
for k in cfg:
|
||||||
|
for x in (True, False, None):
|
||||||
|
if cfg[k] == str(x):
|
||||||
|
cfg[k] = x
|
||||||
|
break
|
||||||
|
if k in json_vals:
|
||||||
|
cfg[k] = json.loads(cfg[k])
|
||||||
|
if k == 'path':
|
||||||
|
paths = []
|
||||||
|
for i in cfg[k].split(':'):
|
||||||
|
p = os.path.abspath(os.path.expanduser(i))
|
||||||
|
paths.append(p)
|
||||||
|
cfg[k] = paths
|
||||||
|
if k in dirs:
|
||||||
|
if cfg[k]:
|
||||||
|
cfg[k] = os.path.abspath(os.path.expanduser(cfg[k]))
|
||||||
|
os.makedirs(cfg[k], exist_ok = True)
|
||||||
|
if k in u_g_pairs:
|
||||||
|
dflt = [pwd.getpwuid(os.geteuid()).pw_name, grp.getgrgid(os.getegid()).gr_name]
|
||||||
|
l = re.split(':|\.', cfg[k])
|
||||||
|
if len(l) == 1:
|
||||||
|
l.append(None)
|
||||||
|
for idx, i in enumerate(l[:]):
|
||||||
|
if i in ('', None):
|
||||||
|
l[idx] = dflt[idx]
|
||||||
|
cfg[k] = {}
|
||||||
|
cfg[k]['uid'] = (int(l[0]) if l[0].isnumeric() else pwd.getpwnam(l[0]).pw_uid)
|
||||||
|
cfg[k]['gid'] = (int(l[1]) if l[1].isnumeric() else grp.getgrnam(l[1]).gr_gid)
|
||||||
|
cfg['orig_user'] = {'uid': os.geteuid(),
|
||||||
|
'gid': os.getegid()}
|
||||||
|
# Ugh. https://orkus.wordpress.com/2011/04/17/python-getting-umask-without-change/
|
||||||
|
cfg['orig_user']['umask'] = os.umask(0)
|
||||||
|
os.umask(cfg['orig_user']['umask'])
|
||||||
|
cfg['orig_user']['groups'] = os.getgroups()
|
||||||
|
for i in cfg['chmod']:
|
||||||
|
cfg['chmod'][i] = int(cfg['chmod'][i], 8)
|
||||||
|
cfg['orig_user']['env'] = copy.deepcopy(dict(os.environ))
|
||||||
|
os.chown(cfg['cache'], uid = cfg['build_user']['uid'], gid = cfg['build_user']['gid'])
|
||||||
|
os.chown(cfg['dest'], uid = cfg['chown']['uid'], gid = cfg['chown']['gid'])
|
||||||
|
return(cfg)
|
||||||
|
|
||||||
|
def GPG(cur, homedir = None, keyid = None):
|
||||||
|
g = gpg.Context(home_dir = homedir)
|
||||||
|
if not keyid:
|
||||||
|
# We don't have a key specified, so we need to generate one and update the config.
|
||||||
|
s = ('This signature and signing key were automatically generated using Autopkg from OpTools: '
|
||||||
|
'https://git.square-r00t.net/OpTools/')
|
||||||
|
g.sig_notation_add('automatically-generated@git.square-r00t.net', s, gpg.constants.sig.notation.HUMAN_READABLE)
|
||||||
|
userid = 'Autopkg Signing Key ({0}@{1})'.format(os.getenv('SUDO_USER', os.environ['USER']), gethostname())
|
||||||
|
params = {
|
||||||
|
#'algorithm': 'ed25519',
|
||||||
|
'algorithm': 'rsa4096',
|
||||||
|
'expires': False,
|
||||||
|
'expires_in': 0,
|
||||||
|
'sign': True,
|
||||||
|
'passphrase': None
|
||||||
|
}
|
||||||
|
keyid = g.create_key(userid, **params).fpr
|
||||||
|
# https://stackoverflow.com/a/50718957
|
||||||
|
q = {}
|
||||||
|
for col in ('keyid', 'homedir'):
|
||||||
|
if sqlite3.sqlite_version_info > (3, 24, 0):
|
||||||
|
q[col] = ("INSERT INTO config (directive, value) "
|
||||||
|
"VALUES ('gpg_{0}', ?) "
|
||||||
|
"ON CONFLICT (directive) "
|
||||||
|
"DO UPDATE SET value = excluded.value").format(col)
|
||||||
|
else:
|
||||||
|
cur.execute("SELECT id FROM config WHERE directive = 'gpg_{0}'".format(col))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
q[col] = ("UPDATE config SET value = ? WHERE id = '{0}'").format(row['id'])
|
||||||
|
else:
|
||||||
|
q[col] = ("INSERT INTO config (directive, value) VALUES ('gpg_{0}', ?)").format(col)
|
||||||
|
cur.execute(q[col], (locals()[col], ))
|
||||||
|
return(keyid, g)
|
223
arch/buildup/pkgchk.py
Executable file
223
arch/buildup/pkgchk.py
Executable file
@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import tarfile # for verifying built PKGBUILDs. We just need to grab <tar>/.PKGINFO, and check: pkgver = <version>
|
||||||
|
import tempfile
|
||||||
|
from collections import OrderedDict
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
class color(object):
|
||||||
|
PURPLE = '\033[95m'
|
||||||
|
CYAN = '\033[96m'
|
||||||
|
DARKCYAN = '\033[36m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
|
END = '\033[0m'
|
||||||
|
|
||||||
|
|
||||||
|
vcstypes = ('bzr', 'git', 'hg', 'svn')
|
||||||
|
|
||||||
|
class pkgChk(object):
|
||||||
|
def __init__(self, pkg):
|
||||||
|
# pkg should be a string of a PKGBUILD,
|
||||||
|
# not the path to a file.
|
||||||
|
self.pkg = pkg
|
||||||
|
# The below holds parsed data from the PKGBUILD.
|
||||||
|
self.pkgdata = {'pkgver': self.getLex('pkgver', 'var'),
|
||||||
|
'_pkgver': self.getLex('_pkgver', 'var'),
|
||||||
|
'pkgname': self.getLex('pkgname', 'var'),
|
||||||
|
'sources': self.getLex('source', 'array')}
|
||||||
|
|
||||||
|
def getLex(self, attrib, attrtype):
|
||||||
|
# Parse the PKGBUILD and return actual values from it.
|
||||||
|
# attrtype should be "var" or "array".
|
||||||
|
# var returns a string and array returns a list.
|
||||||
|
# If the given attrib isn't in the pkgbuild, None is returned.
|
||||||
|
# The sources array is special, though - it returns a tuple of:
|
||||||
|
# (hashtype, dict) where dict is a mapping of:
|
||||||
|
# filename: hash
|
||||||
|
# filename2: hash2
|
||||||
|
# etc.
|
||||||
|
if attrtype not in ('var', 'array'):
|
||||||
|
raise ValueError('{0} is not a valid attribute type.'.format(attrib))
|
||||||
|
_sums = ('sha512', 'sha384', 'sha256', 'sha1', 'md5') # in order of preference
|
||||||
|
_attrmap = {'var': 'echo ${{{0}}}'.format(attrib),
|
||||||
|
'array': 'echo ${{{}[@]}}'.format(attrib)}
|
||||||
|
_tempfile = tempfile.mkstemp(text = True)
|
||||||
|
with open(_tempfile[1], 'w') as f:
|
||||||
|
f.write(self.pkg)
|
||||||
|
_cmd = ['/bin/bash',
|
||||||
|
'--restricted', '--noprofile',
|
||||||
|
'--init-file', _tempfile[1],
|
||||||
|
'-i', '-c', _attrmap[attrtype]]
|
||||||
|
with open(os.devnull, 'wb') as devnull:
|
||||||
|
_out = subprocess.run(_cmd, env = {'PATH': ''},
|
||||||
|
stdout = subprocess.PIPE,
|
||||||
|
stderr = devnull).stdout.decode('utf-8').strip()
|
||||||
|
if _out == '':
|
||||||
|
os.remove(_tempfile[1])
|
||||||
|
return(None)
|
||||||
|
if attrtype == 'var':
|
||||||
|
os.remove(_tempfile[1])
|
||||||
|
return(_out)
|
||||||
|
else: # it's an array
|
||||||
|
if attrib == 'source':
|
||||||
|
_sources = {}
|
||||||
|
_source = shlex.split(_out)
|
||||||
|
_sumarr = [None] * len(_source)
|
||||||
|
for h in _sums:
|
||||||
|
_cmd[-1] = 'echo ${{{0}[@]}}'.format(h + 'sums')
|
||||||
|
with open(os.devnull, 'wb') as devnull:
|
||||||
|
_out = subprocess.run(_cmd, env = {'PATH': ''},
|
||||||
|
stdout = subprocess.PIPE,
|
||||||
|
stderr = devnull).stdout.decode('utf-8').strip()
|
||||||
|
if _out != '':
|
||||||
|
os.remove(_tempfile[1])
|
||||||
|
return(h, OrderedDict(zip(_source, shlex.split(_out))))
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
# No match for checksums.
|
||||||
|
os.remove(_tempfile[1])
|
||||||
|
return(None, OrderedDict(zip(_source, shlex.split(_out))))
|
||||||
|
else:
|
||||||
|
os.remove(_tempfile[1])
|
||||||
|
return(shlex.split(_out))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getURL(self, url):
|
||||||
|
with urlopen(url) as http:
|
||||||
|
code = http.getcode()
|
||||||
|
return(code)
|
||||||
|
|
||||||
|
def chkVer(self):
|
||||||
|
_separators = []
|
||||||
|
# TODO: this is to explicitly prevent parsing
|
||||||
|
# VCS packages, so might need some re-tooling in the future.
|
||||||
|
if self.pkgdata['pkgname'].split('-')[-1] in vcstypes:
|
||||||
|
return(None)
|
||||||
|
# transform the current version into a list of various components.
|
||||||
|
if not self.pkgdata['pkgver']:
|
||||||
|
return(None)
|
||||||
|
if self.pkgdata['_pkgver']:
|
||||||
|
_cur_ver = self.pkgdata['_pkgver']
|
||||||
|
else:
|
||||||
|
_cur_ver = self.pkgdata['pkgver']
|
||||||
|
# This will catch like 90% of the software versions out there.
|
||||||
|
# Unfortunately, it won't catch all of them. I dunno how to
|
||||||
|
# handle that quite yet. TODO.
|
||||||
|
_split_ver = _cur_ver.split('.')
|
||||||
|
_idx = len(_split_ver) - 1
|
||||||
|
while _idx >= 0:
|
||||||
|
_url = re.sub('^[A-Za-z0-9]+::',
|
||||||
|
'',
|
||||||
|
list(self.pkgdata['sources'].keys())[0])
|
||||||
|
_code = self.getURL(_url)
|
||||||
|
_idx -= 1
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
_ini = '~/.config/optools/buildup.ini'
|
||||||
|
_defini = os.path.abspath(os.path.expanduser(_ini))
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
args.add_argument('-c', '--config',
|
||||||
|
default = _defini,
|
||||||
|
dest = 'config',
|
||||||
|
help = ('The path to the config file. ' +
|
||||||
|
'Default: {0}{1}{2}').format(color.BOLD,
|
||||||
|
_defini,
|
||||||
|
color.END))
|
||||||
|
args.add_argument('-R', '--no-recurse',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'recurse',
|
||||||
|
help = ('If specified, and the path provided is a directory, ' +
|
||||||
|
'do NOT recurse into subdirectories.'))
|
||||||
|
args.add_argument('-p', '--path',
|
||||||
|
metavar = 'path/to/dir/or/PKGBUILD',
|
||||||
|
default = None,
|
||||||
|
dest = 'pkgpath',
|
||||||
|
help = ('The path to either a directory containing PKGBUILDs (recursion ' +
|
||||||
|
'enabled - see {0}-R/--no-recurse{1}) ' +
|
||||||
|
'or a single PKGBUILD. Use to override ' +
|
||||||
|
'the config\'s PKG:paths.').format(color.BOLD, color.END))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def parsePkg(pkgbuildstr):
|
||||||
|
p = pkgChk(pkgbuildstr)
|
||||||
|
p.chkVer()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def iterDir(pkgpath, recursion = True):
|
||||||
|
filepaths = []
|
||||||
|
if os.path.isfile(pkgpath):
|
||||||
|
return([pkgpath])
|
||||||
|
if recursion:
|
||||||
|
for root, subdirs, files in os.walk(pkgpath):
|
||||||
|
for vcs in vcstypes:
|
||||||
|
if '.{0}'.format(vcs) in subdirs:
|
||||||
|
subdirs.remove('.{0}'.format(vcs))
|
||||||
|
for f in files:
|
||||||
|
if 'PKGBUILD' in f:
|
||||||
|
filepaths.append(os.path.join(root, f))
|
||||||
|
else:
|
||||||
|
for f in os.listdir(pkgpath):
|
||||||
|
if 'PKGBUILD' in f:
|
||||||
|
filepaths.append(f)
|
||||||
|
filepaths.sort()
|
||||||
|
return(filepaths)
|
||||||
|
|
||||||
|
def parseCfg(cfgfile):
|
||||||
|
def getPath(p):
|
||||||
|
return(os.path.abspath(os.path.expanduser(p)))
|
||||||
|
_defcfg = '[PKG]\npaths = \ntestbuild = no\n[VCS]\n'
|
||||||
|
for vcs in vcstypes:
|
||||||
|
_defcfg += '{0} = no\n'.format(vcs)
|
||||||
|
_cfg = configparser.ConfigParser()
|
||||||
|
_cfg._interpolation = configparser.ExtendedInterpolation()
|
||||||
|
_cfg.read((_defcfg, cfgfile))
|
||||||
|
# We convert to a dict so we can do things like list comprehension.
|
||||||
|
cfg = {s:dict(_cfg.items(s)) for s in _cfg.sections()}
|
||||||
|
if 'paths' not in cfg['PKG'].keys():
|
||||||
|
raise ValueError('You must provide a valid configuration ' +
|
||||||
|
'file with the PKG:paths setting specified and valid.')
|
||||||
|
cfg['PKG']['paths'] = sorted([getPath(p.strip()) for p in cfg['PKG']['paths'].split(',')],
|
||||||
|
reverse = True)
|
||||||
|
for p in cfg['PKG']['paths'][:]:
|
||||||
|
if not os.path.exists(p):
|
||||||
|
print('WARNING: {0} does not exist; skipping...'.format(p))
|
||||||
|
cfg['PKG']['paths'].remove(p)
|
||||||
|
# We also want to convert these to pythonic True/False
|
||||||
|
cfg['PKG']['testbuild'] = _cfg['PKG'].getboolean('testbuild')
|
||||||
|
for k in vcstypes:
|
||||||
|
cfg['VCS'][k] = _cfg['VCS'].getboolean(k)
|
||||||
|
return(cfg)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
if not os.path.isfile(args['config']):
|
||||||
|
raise FileNotFoundError('{0} does not exist.'.format(cfg))
|
||||||
|
cfg = parseCfg(args['config'])
|
||||||
|
if args['pkgpath']:
|
||||||
|
args['pkgpath'] = os.path.abspath(os.path.expanduser(args['pkgpath']))
|
||||||
|
if os.path.isdir(args['pkgpath']):
|
||||||
|
iterDir(args['pkgpath'], recursion = args['recurse'])
|
||||||
|
elif os.path.isfile(args['pkgpath']):
|
||||||
|
parsePkg(args['pkgpath'])
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError('{0} does not exist.'.format(args['pkgpath']))
|
||||||
|
else:
|
||||||
|
files = []
|
||||||
|
for p in cfg['PKG']['paths']:
|
||||||
|
files.extend(iterDir(p))
|
||||||
|
files.sort()
|
||||||
|
for p in files:
|
||||||
|
with open(p, 'r') as f:
|
||||||
|
parsePkg(f.read())
|
39
arch/buildup/sample.buildup.ini
Normal file
39
arch/buildup/sample.buildup.ini
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
## This configuration file will allow you to perform more
|
||||||
|
## fine-grained control of BuildUp.
|
||||||
|
## It supports the syntax shortcuts found here:
|
||||||
|
## https://docs.python.org/3/library/configparser.html#configparser.ExtendedInterpolation
|
||||||
|
|
||||||
|
[PKG]
|
||||||
|
# The path(s) to your PKGBUILD(s), or a directory/directories containing them.
|
||||||
|
# If you have more than one, separate with a comma.
|
||||||
|
paths = path/to/pkgbuilds,another/path/to/pkgbuilds
|
||||||
|
|
||||||
|
# If 'yes', try building the package with the new version.
|
||||||
|
# If 'no' (the default), don't try to build with the new version.
|
||||||
|
# This can be a good way to test that you don't need to modify the PKGBUILD,
|
||||||
|
# but can be error-prone (missing makedeps, etc.).
|
||||||
|
testbuild = no
|
||||||
|
|
||||||
|
[VCS]
|
||||||
|
# Here you can enable or disable which VCS platforms you want to support.
|
||||||
|
# Note that it will increase the time of your check, as it will
|
||||||
|
# actually perform a checkout/clone/etc. of the source and check against
|
||||||
|
# the version function inside the PKGBUILD.
|
||||||
|
# It's also generally meaningless, as VCS PKGBUILDs are intended
|
||||||
|
# to be dynamic. Nonetheless, the options are there.
|
||||||
|
# Use 'yes' to enable, or 'no' to disable (the default).
|
||||||
|
# Currently only the given types are supported (i.e. no CVS).
|
||||||
|
|
||||||
|
# THESE ARE CURRENTLY NOT SUPPORTED.
|
||||||
|
|
||||||
|
# Check revisions for -git PKGBUILDs
|
||||||
|
git = no
|
||||||
|
|
||||||
|
# Check revisions for -svn PKGBUILDs
|
||||||
|
svn = no
|
||||||
|
|
||||||
|
# Check revisions for -hg PKGBUILDs
|
||||||
|
hg = no
|
||||||
|
|
||||||
|
# Check revisions for -bzr PKGBUILDs
|
||||||
|
bzr = no
|
81
arch/mirrorchk.py
Normal file
81
arch/mirrorchk.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
# The local list of mirrors
|
||||||
|
mfile = '/etc/pacman.d/mirrorlist'
|
||||||
|
# The URL for the list of mirros
|
||||||
|
# TODO: customize with country in a config
|
||||||
|
rlist = 'https://www.archlinux.org/mirrorlist/?country=US&protocol=http&protocol=https&ip_version=4&use_mirror_status=on'
|
||||||
|
# If local_mirror is set to None, don't do any modifications.
|
||||||
|
# If it's a dict in the format of:
|
||||||
|
# local_mirror = {'profile': 'PROFILE_NAME',
|
||||||
|
# 'url': 'http://host/arch/%os/$arch',
|
||||||
|
# 'state_file': '/var/lib/netctl/netctl.state'}
|
||||||
|
# Then we will check 'state_file'. If its contents match 'profile',
|
||||||
|
# then we will add 'url' to the *top* of mfile.
|
||||||
|
# TODO: I need to move this to a config.
|
||||||
|
local_mirror = {'profile': '<PROFILENAME>',
|
||||||
|
'url': 'http://<REPOBOX>/arch/$repo/os/$arch',
|
||||||
|
'state_file': '/var/lib/netctl/netctl.state'}
|
||||||
|
|
||||||
|
def getList(url):
|
||||||
|
with urlopen(url) as http:
|
||||||
|
l = http.read().decode('utf-8')
|
||||||
|
return(l)
|
||||||
|
|
||||||
|
def uncomment(url_list):
|
||||||
|
urls = []
|
||||||
|
if isinstance(url_list, str):
|
||||||
|
url_list = [u.strip() for u in url_list.splitlines()]
|
||||||
|
for u in url_list:
|
||||||
|
u = u.strip()
|
||||||
|
if u == '':
|
||||||
|
continue
|
||||||
|
urls.append(re.sub('^\s*#', '', u))
|
||||||
|
return(urls)
|
||||||
|
|
||||||
|
def rankList(mfile):
|
||||||
|
c = ['rankmirrors',
|
||||||
|
'-n', '6',
|
||||||
|
mfile]
|
||||||
|
ranked_urls = subprocess.run(c, stdout = subprocess.PIPE)
|
||||||
|
url_list = ranked_urls.stdout.decode('utf-8').splitlines()
|
||||||
|
for u in url_list[:]:
|
||||||
|
if u.strip() == '':
|
||||||
|
url_list.remove(u)
|
||||||
|
continue
|
||||||
|
if re.match('^\s*(#.*)$', u, re.MULTILINE | re.DOTALL):
|
||||||
|
url_list.remove(u)
|
||||||
|
return(url_list)
|
||||||
|
|
||||||
|
def localMirror(url_list):
|
||||||
|
# If checking the state_file doesn't work out, use netctl
|
||||||
|
# directly.
|
||||||
|
if not isinstance(local_mirror, dict):
|
||||||
|
return(url_list)
|
||||||
|
with open(local_mirror['state_file'], 'r') as f:
|
||||||
|
state = f.read().strip()
|
||||||
|
state = [s.strip() for s in state]
|
||||||
|
if local_mirror['profile'] in state:
|
||||||
|
url_list.insert(0, 'Server = {0}'.format(local_mirror['url']))
|
||||||
|
return(url_list)
|
||||||
|
|
||||||
|
def writeList(mirrorfile, url_list):
|
||||||
|
with open(mirrorfile, 'w') as f:
|
||||||
|
f.write('{0}\n'.format('\n'.join(url_list)))
|
||||||
|
return()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
exit('Must be run as root.')
|
||||||
|
urls = getList(rlist)
|
||||||
|
t = tempfile.mkstemp(text = True)
|
||||||
|
writeList(t[1], uncomment(urls))
|
||||||
|
ranked_mirrors = localMirror(rankList(t[1]))
|
||||||
|
writeList(mfile, ranked_mirrors)
|
||||||
|
os.remove(t[1])
|
89
arch/reference
Normal file
89
arch/reference
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
some random snippets to incorporate...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
this was to assist with https://www.archlinux.org/news/perl-library-path-change/
|
||||||
|
the following was used to gen the /tmp/perlfix.pkgs.lst:
|
||||||
|
pacman -Qqo '/usr/lib/perl5/vendor_perl' >> /tmp/perlfix.pkgs.lst ; pacman -Qqo '/usr/lib/perl5/site_perl' >> /tmp/perlfix.pkgs.lst
|
||||||
|
######################
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
pkgs = []
|
||||||
|
|
||||||
|
pkglstfile = '/tmp/perlfix.pkgs.lst'
|
||||||
|
|
||||||
|
if os.path.isfile(pkglstfile):
|
||||||
|
with open(pkglstfile, 'r') as f:
|
||||||
|
pkgs = f.read().splitlines()
|
||||||
|
|
||||||
|
pkgd = {'rdeps': [],
|
||||||
|
'deps': [],
|
||||||
|
'remove': []}
|
||||||
|
|
||||||
|
for p in pkgs:
|
||||||
|
pkgchkcmd = ['apacman', '-Q', p]
|
||||||
|
with open(os.devnull, 'w') as devnull:
|
||||||
|
pkgchk = subprocess.run(pkgchkcmd, stdout = devnull, stderr = devnull).returncode
|
||||||
|
if pkgchk != 0: # not installed anymore
|
||||||
|
break
|
||||||
|
cmd = ['apacman',
|
||||||
|
'-Qi',
|
||||||
|
p]
|
||||||
|
stdout = subprocess.run(cmd, stdout = subprocess.PIPE).stdout.decode('utf-8').strip().splitlines()
|
||||||
|
#pprint.pprint(stdout)
|
||||||
|
d = {re.sub('\s', '_', k.strip().lower()):v.strip() for k, v in (dict(k.split(':', 1) for k in stdout).items())}
|
||||||
|
|
||||||
|
# some pythonizations..
|
||||||
|
# list of things(keys) that should be lists
|
||||||
|
ll = ['architecture', 'conflicts_with', 'depends_on', 'groups', 'licenses', 'make_depends',
|
||||||
|
'optional_deps', 'provides', 'replaces', 'required_by']
|
||||||
|
# and now actually listify
|
||||||
|
for k in ll:
|
||||||
|
if k in d.keys():
|
||||||
|
if d[k].lower() in ('none', ''):
|
||||||
|
d[k] = None
|
||||||
|
else:
|
||||||
|
d[k] = d[k].split()
|
||||||
|
# Not necessary... blah blah inconsistent whitespace blah blah.
|
||||||
|
#for k in ('build_date', 'install_date'):
|
||||||
|
# if k in d.keys():
|
||||||
|
# try:
|
||||||
|
# d[k] = datetime.datetime.strptime(d[k], '%a %d %b %Y %H:%M:%S %p %Z')
|
||||||
|
# except:
|
||||||
|
# d[k] = datetime.datetime.strptime(d[k], '%a %d %b %Y %H:%M:%S %p')
|
||||||
|
|
||||||
|
#pprint.pprint(d)
|
||||||
|
if d['required_by']:
|
||||||
|
pkgd['rdeps'].extend(d['required_by'])
|
||||||
|
else:
|
||||||
|
if d['install_reason'] != 'Explicitly installed':
|
||||||
|
pkgd['remove'].append(p)
|
||||||
|
if d['depends_on']:
|
||||||
|
pkgd['deps'].extend(d['depends_on'])
|
||||||
|
#break
|
||||||
|
|
||||||
|
for x in ('rdeps', 'deps'):
|
||||||
|
pkgd[x].sort()
|
||||||
|
|
||||||
|
#for p in pkgd['rdeps']:
|
||||||
|
# if p in pkgd['deps']:
|
||||||
|
# pkgd['
|
||||||
|
|
||||||
|
#print('DEPENDENCIES:')
|
||||||
|
#print('\n'.join(pkgd['deps']))
|
||||||
|
#print('\nREQUIRED BY:')
|
||||||
|
#print('\n'.join(pkgd['rdeps']))
|
||||||
|
#print('\nCAN REMOVE:')
|
||||||
|
print('\n'.join(pkgd['remove']))
|
||||||
|
|
||||||
|
#cmd = ['apacman', '-R']
|
||||||
|
#cmd.extend(pkgd['remove'])
|
||||||
|
#subprocess.run(cmd)
|
288
arch/repo-maint.py
Executable file
288
arch/repo-maint.py
Executable file
@ -0,0 +1,288 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
|
||||||
|
# PREREQS:
|
||||||
|
# Mostly stdlib.
|
||||||
|
#
|
||||||
|
# IF:
|
||||||
|
# 1.) You want to sign or verify packages (-s/--sign and -v/--verify, respectively),
|
||||||
|
# 2.) You want to work with delta updates,
|
||||||
|
# THEN:
|
||||||
|
# 1.) You need to install the python GnuPG GPGME bindings (the "gpg" module; NOT the "gpgme" module). They're
|
||||||
|
# distributed with the GPG source. They're also in PyPI (https://pypi.org/project/gpg/).
|
||||||
|
# 2.) You need to install the xdelta3 module (https://pypi.org/project/xdelta3/).
|
||||||
|
|
||||||
|
_delta_re = re.compile('(.*)-*-*_to*')
|
||||||
|
|
||||||
|
|
||||||
|
class RepoMaint(object):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
# https://stackoverflow.com/a/2912884/733214
|
||||||
|
user_params = kwargs
|
||||||
|
# Define a set of defaults to update with kwargs since we
|
||||||
|
# aren't explicitly defining params.
|
||||||
|
self.args = {'color': True,
|
||||||
|
'db': './repo.db.tar.xz',
|
||||||
|
'key': None,
|
||||||
|
'pkgs': [],
|
||||||
|
'quiet': False,
|
||||||
|
'sign': False,
|
||||||
|
'verify': False}
|
||||||
|
self.args.update(user_params)
|
||||||
|
self.db_exts = {'db.tar': False, # No compression
|
||||||
|
'db.tar.xz': 'xz',
|
||||||
|
'db.tar.gz': 'gz',
|
||||||
|
'db.tar.bz2': 'bz2',
|
||||||
|
# We explicitly check False vs. None.
|
||||||
|
# For None, we do a custom check and wrap it.
|
||||||
|
# In .Z's case, we use the lzw module. It's the only non-stdlib compression
|
||||||
|
# that Arch Linux repo DB files support.
|
||||||
|
'db.tar.Z': None}
|
||||||
|
self.args['db'] = os.path.abspath(os.path.expanduser(self.args['db']))
|
||||||
|
self.db = None
|
||||||
|
_is_valid_repo_db = False
|
||||||
|
if not _is_valid_repo_db:
|
||||||
|
raise ValueError(('Repo DB {0} is not a valid DB type. '
|
||||||
|
'Must be one of {1}.').format(self.args['db'],
|
||||||
|
', '.join(['*.{0}'.format(i) for i in self.db_exts])))
|
||||||
|
self.repo_dir = os.path.dirname(self.args['db'])
|
||||||
|
self.lockfile = '{0}.lck'.format(self.args['db'])
|
||||||
|
os.makedirs(self.repo_dir, exist_ok = True)
|
||||||
|
self.gpg = None
|
||||||
|
self.sigkey = None
|
||||||
|
if self.args['sign'] or self.args['verify']:
|
||||||
|
# Set up GPG handler.
|
||||||
|
self._initGPG()
|
||||||
|
self._importDB()
|
||||||
|
|
||||||
|
def _initGPG(self):
|
||||||
|
import gpg
|
||||||
|
self.gpg = gpg.Context()
|
||||||
|
if self.args['sign']:
|
||||||
|
_seckeys = [k for k in self.gpg.keylist(secret = True) if k.can_sign]
|
||||||
|
if self.args['key']:
|
||||||
|
for k in _seckeys:
|
||||||
|
if self.sigkey:
|
||||||
|
break
|
||||||
|
for s in k.subkeys:
|
||||||
|
if self.sigkey:
|
||||||
|
break
|
||||||
|
if s.can_sign:
|
||||||
|
if self.args['key'].lower() in (s.keyid.lower(),
|
||||||
|
s.fpr.lower()):
|
||||||
|
self.sigkey = k
|
||||||
|
self.gpg.signers = [k]
|
||||||
|
else:
|
||||||
|
# Grab the first key that can sign.
|
||||||
|
if _seckeys:
|
||||||
|
self.sigkey = _seckeys[0]
|
||||||
|
self.gpg.signers = [_seckeys[0]]
|
||||||
|
if not self.args['quiet']:
|
||||||
|
print('Key ID not specified; using {0} as the default'.format(self.sigkey.fpr))
|
||||||
|
if not self.sigkey:
|
||||||
|
raise RuntimeError('Private key ID not found, cannot sign, or no secret keys exist.')
|
||||||
|
# TODO: confirm verifying works without a key
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _LZWcompress(self, data):
|
||||||
|
# Based largely on:
|
||||||
|
# https://github.com/HugoPouliquen/lzw-tools/blob/master/utils/compression.py
|
||||||
|
data_arr = []
|
||||||
|
rawdata = io.BytesIO(data)
|
||||||
|
for i in range(int(len(data) / 2)):
|
||||||
|
data_arr.insert(i, rawdata.read(2))
|
||||||
|
w = bytes()
|
||||||
|
b_size = 256
|
||||||
|
b = []
|
||||||
|
compressed = io.BytesIO()
|
||||||
|
for c in data_arr:
|
||||||
|
c = c.to_bytes(2, 'big')
|
||||||
|
wc = w + c
|
||||||
|
if wc in b:
|
||||||
|
w = wc
|
||||||
|
else:
|
||||||
|
b.insert(b_size, wc)
|
||||||
|
compressed.write(b.index(wc).to_bytes(2, 'big'))
|
||||||
|
b_size += 1
|
||||||
|
w = c
|
||||||
|
return(compressed.getvalue())
|
||||||
|
|
||||||
|
def _LZWdecompress(self, data):
|
||||||
|
# Based largely on:
|
||||||
|
# https://github.com/HugoPouliquen/lzw-tools/blob/master/utils/decompression.py
|
||||||
|
b_size = 256
|
||||||
|
b = []
|
||||||
|
out = io.BytesIO()
|
||||||
|
for i in range(b_size):
|
||||||
|
b.insert(i, i.to_bytes(2, 'big'))
|
||||||
|
w = data.pop(0)
|
||||||
|
out.write(w)
|
||||||
|
i = 0
|
||||||
|
for byte in data:
|
||||||
|
x = int.from_bytes(byte, byteorder = 'big')
|
||||||
|
if x < b_size:
|
||||||
|
entry = b[x]
|
||||||
|
elif x == b_size:
|
||||||
|
entry = w + w
|
||||||
|
else:
|
||||||
|
raise ValueError('Bad uncompressed value for "{0}"'.format(byte))
|
||||||
|
for y in entry:
|
||||||
|
if i % 2 == 1:
|
||||||
|
out.write(y.to_bytes(1, byteorder = 'big'))
|
||||||
|
i += 1
|
||||||
|
b.insert(b_size, w + x)
|
||||||
|
b_size += 1
|
||||||
|
w = entry
|
||||||
|
return(out.getvalue())
|
||||||
|
|
||||||
|
def _importDB(self):
|
||||||
|
# Get the compression type.
|
||||||
|
for ct in self.db_exts:
|
||||||
|
if self.args['db'].lower().endswith(ct):
|
||||||
|
if self.db_exts[ct] == False:
|
||||||
|
if ct.endswith('.Z'): # Currently the only custom one.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def add(self):
|
||||||
|
# Fresh pkg set (in case the instance was re-used).
|
||||||
|
self.pkgs = {}
|
||||||
|
# First handle any wildcard
|
||||||
|
for p in self.args['pkgs'][:]:
|
||||||
|
if p.strip() == '*':
|
||||||
|
for root, dirs, files in os.walk(self.repo_dir):
|
||||||
|
for f in files:
|
||||||
|
abspath = os.path.join(root, f)
|
||||||
|
if f.endswith('.pkg.tar.xz'): # Recommended not to be changed per makepkg.conf
|
||||||
|
if abspath not in self.args['pkgs']:
|
||||||
|
self.args['pkgs'].append(abspath)
|
||||||
|
if self.args['delta']:
|
||||||
|
if f.endswith('.delta'):
|
||||||
|
if abspath not in self.args['pkgs']:
|
||||||
|
self.args['pkgs'].append(abspath)
|
||||||
|
self.args['pkgs'].remove(p)
|
||||||
|
# Then de-dupe and convert to full path.
|
||||||
|
self.args['pkgs'] = sorted(list(set([os.path.abspath(os.path.expanduser(d)) for d in self.args['pkgs']])))
|
||||||
|
for p in self.args['pkgs']:
|
||||||
|
pkgfnm = os.path.basename(p)
|
||||||
|
if p.endswith('.delta'):
|
||||||
|
pkgnm = _delta_re.sub('\g<1>', os.path.basename(pkgfnm))
|
||||||
|
|
||||||
|
return()
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
for p in self.args['pkgs']:
|
||||||
|
pass
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
def hatch():
|
||||||
|
import base64
|
||||||
|
import lzma
|
||||||
|
import random
|
||||||
|
h = ((
|
||||||
|
'/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4AB6AEtdABBok+MQCtEh'
|
||||||
|
'BisubEtc2ebacaLGrSRAMmHrcwUr39J24q4iODdNz7wfQl9e6I3C'
|
||||||
|
'ooyuOkptNISdo50CRdknGAU4JBBh+IQTkHwiAAAABW1d7drLmkUA'
|
||||||
|
'AWd7/+DtzR+2830BAAAAAARZWg=='
|
||||||
|
).encode('utf-8'),
|
||||||
|
(
|
||||||
|
'/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4AHEALtdABBpE/AVEKFC'
|
||||||
|
'fdT16ly2cCwT/MnXTY2D4r8nWgH6mLetLPn17nza3ZK+tSFU7d5j'
|
||||||
|
'my91M8fvPGu9Tf0NYkWlRU7vJM8r2V3kK/Gs6/GS7tq2qIum/C/X'
|
||||||
|
'sOnYUewVB2yMvlACqwp3gWJlmXSfwcpGiU662EmATS8kUgF+OdP+'
|
||||||
|
'EATXhM/1bAn07wJbVWPoAL2SBmJBo2zL1tXQklbQu1J20eWfd1bD'
|
||||||
|
'cgSBGqcU1/CdHnW6lcb6BmWKTg0p9IAAAEoEyN1gLkAMAAHXAcUD'
|
||||||
|
'AACXcduyscRn+wIAAAAABFla'
|
||||||
|
).encode('utf-8'))
|
||||||
|
h = lzma.decompress(base64.b64decode(h[random.randint(0, 1)]))
|
||||||
|
return(h.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = ('Python implementation of repo-add/repo-remove.'),
|
||||||
|
epilog = ('See https://wiki.archlinux.org/index.php/Pacman/'
|
||||||
|
'Tips_and_tricks#Custom_local_repository for more information.\n'
|
||||||
|
'Each operation has sub-help (e.g. "... add -h")'),
|
||||||
|
formatter_class = argparse.RawDescriptionHelpFormatter)
|
||||||
|
operargs = args.add_subparsers(dest = 'oper',
|
||||||
|
help = ('Operation to perform'))
|
||||||
|
commonargs = argparse.ArgumentParser(add_help = False)
|
||||||
|
commonargs.add_argument('db',
|
||||||
|
metavar = '</path/to/repository/repo.db.tar.xz>',
|
||||||
|
help = ('The path to the repository DB (required)'))
|
||||||
|
commonargs.add_argument('pkgs',
|
||||||
|
nargs = '+',
|
||||||
|
metavar = '<package|delta>',
|
||||||
|
help = ('Package filepath (for adding)/name (for removing) or delta; '
|
||||||
|
'can be specified multiple times (at least 1 required)'))
|
||||||
|
commonargs.add_argument('--nocolor',
|
||||||
|
dest = 'color',
|
||||||
|
action = 'store_false',
|
||||||
|
help = ('If specified, turn off color in output (currently does nothing; '
|
||||||
|
'output is currently not colorized)'))
|
||||||
|
commonargs.add_argument('-q', '--quiet',
|
||||||
|
dest = 'quiet',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('Minimize output'))
|
||||||
|
commonargs.add_argument('-s', '--sign',
|
||||||
|
dest = 'sign',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, sign database with GnuPG after update'))
|
||||||
|
commonargs.add_argument('-k', '--key',
|
||||||
|
metavar = 'KEY_ID',
|
||||||
|
nargs = 1,
|
||||||
|
help = ('Use the specified GPG key to sign the database '
|
||||||
|
'(only used if -s/--sign is active)'))
|
||||||
|
commonargs.add_argument('-v', '--verify',
|
||||||
|
dest = 'verify',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, verify the database\'s signature before update'))
|
||||||
|
addargs = operargs.add_parser('add',
|
||||||
|
parents = [commonargs],
|
||||||
|
help = ('Add package(s) to a repository'))
|
||||||
|
remargs = operargs.add_parser('remove',
|
||||||
|
parents = [commonargs],
|
||||||
|
help = ('Remove package(s) from a repository'))
|
||||||
|
addargs.add_argument('-d', '--delta',
|
||||||
|
dest = 'delta',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, generate and add package deltas for the update'))
|
||||||
|
addargs.add_argument('-n', '--new',
|
||||||
|
dest = 'new_only',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, only add packages that are not already in the database'))
|
||||||
|
addargs.add_argument('-R', '--remove',
|
||||||
|
dest = 'remove_old',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, remove old packages from disk after updating the database'))
|
||||||
|
# Removal args have no add'l arguments, just the common ones.
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if (len(sys.argv) == 2) and (sys.argv[1] == 'elephant'):
|
||||||
|
print(hatch())
|
||||||
|
return()
|
||||||
|
else:
|
||||||
|
rawargs = parseArgs()
|
||||||
|
args = rawargs.parse_args()
|
||||||
|
if not args.oper:
|
||||||
|
rawargs.print_help()
|
||||||
|
exit()
|
||||||
|
rm = RepoMaint(**vars(args))
|
||||||
|
if args.oper == 'add':
|
||||||
|
rm.add()
|
||||||
|
elif args.oper == 'remove':
|
||||||
|
rm.remove()
|
||||||
|
return()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
207
centos/extract_files_package.py
Executable file
207
centos/extract_files_package.py
Executable file
@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Supports CentOS 6.9 and up, untested on lower versions.
|
||||||
|
# Lets you extract files for a given package name(s) without installing
|
||||||
|
# any extra packages (such as yum-utils for repoquery).
|
||||||
|
|
||||||
|
# NOTE: If you're on CentOS 6.x, since it uses such an ancient version of python you need to either install
|
||||||
|
# python-argparse OR just resign to using it for all packages with none of the features.
|
||||||
|
try:
|
||||||
|
import argparse
|
||||||
|
has_argparse = True
|
||||||
|
except ImportError:
|
||||||
|
has_argparse = False
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
# For when CentOS/RHEL switch to python 3 by default (if EVER).
|
||||||
|
import sys
|
||||||
|
pyver = sys.version_info
|
||||||
|
try:
|
||||||
|
import yum
|
||||||
|
# Needed for verbosity
|
||||||
|
from yum.logginglevels import __NO_LOGGING as yum_nolog
|
||||||
|
has_yum = True
|
||||||
|
except ImportError:
|
||||||
|
has_yum = False
|
||||||
|
exit('This script only runs on the system-provided Python on RHEL/CentOS/other RPM-based distros.')
|
||||||
|
try:
|
||||||
|
# pip install libarchive
|
||||||
|
# https://github.com/dsoprea/PyEasyArchive
|
||||||
|
import libarchive.public as lap
|
||||||
|
is_ctype = False
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# pip install libarchive
|
||||||
|
# https://github.com/Changaco/python-libarchive-c
|
||||||
|
import libarchive
|
||||||
|
if 'file_reader' in dir(libarchive):
|
||||||
|
is_legacy = False
|
||||||
|
else:
|
||||||
|
# https://code.google.com/archive/p/python-libarchive
|
||||||
|
is_legacy = True
|
||||||
|
is_ctype = True
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError('Try yum -y install python-libarchive')
|
||||||
|
|
||||||
|
|
||||||
|
class FileExtractor(object):
|
||||||
|
def __init__(self, dest_dir, paths, verbose = False, *args, **kwargs):
|
||||||
|
self.dest_dir = os.path.abspath(os.path.expanduser(dest_dir))
|
||||||
|
self.verbose = verbose # TODO: print file name as extracting? Verbose as argument?
|
||||||
|
self.rpms = {}
|
||||||
|
if 'pkgs' in kwargs and kwargs['pkgs']:
|
||||||
|
self.pkgs = kwargs['pkgs']
|
||||||
|
self.yum_getFiles()
|
||||||
|
if 'rpm_files' in kwargs and kwargs['rpm_files']:
|
||||||
|
self.rpm_files = kwargs['rpm_files']
|
||||||
|
self.getFiles()
|
||||||
|
if '*' in paths:
|
||||||
|
self.paths = None
|
||||||
|
else:
|
||||||
|
self.paths = [re.sub('^', '.', os.path.abspath(i)) for i in paths]
|
||||||
|
|
||||||
|
def yum_getFiles(self):
|
||||||
|
import logging
|
||||||
|
yumloggers = ['yum.filelogging.RPMInstallCallback', 'yum.verbose.Repos', 'yum.verbose.plugin', 'yum.Depsolve',
|
||||||
|
'yum.verbose', 'yum.plugin', 'yum.Repos', 'yum', 'yum.verbose.YumBase', 'yum.filelogging',
|
||||||
|
'yum.verbose.YumPlugins', 'yum.RepoStorage', 'yum.YumBase', 'yum.filelogging.YumBase',
|
||||||
|
'yum.verbose.Depsolve']
|
||||||
|
# This actually silences everything. Nice.
|
||||||
|
# https://stackoverflow.com/a/46716482/733214
|
||||||
|
if not self.verbose:
|
||||||
|
for loggerName in yumloggers:
|
||||||
|
logger = logging.getLogger(loggerName)
|
||||||
|
logger.setLevel(yum_nolog)
|
||||||
|
# http://yum.baseurl.org/api/yum/yum/__init__.html#yumbase
|
||||||
|
yb = yum.YumBase()
|
||||||
|
yb.conf.downloadonly = True
|
||||||
|
yb.conf.downloaddir = os.path.join(self.dest_dir, '.CACHE')
|
||||||
|
yb.conf.quiet = True
|
||||||
|
yb.conf.assumeyes = True
|
||||||
|
for pkg in self.pkgs:
|
||||||
|
try:
|
||||||
|
p = yb.reinstall(name = pkg)
|
||||||
|
except yum.Errors.ReinstallRemoveError:
|
||||||
|
p = yb.install(name = pkg)
|
||||||
|
p = p[0]
|
||||||
|
# I am... not 100% certain on this. Might be a better way?
|
||||||
|
fname = '{0}-{3}-{4}.{1}.rpm'.format(*p.pkgtup)
|
||||||
|
self.rpms[pkg] = os.path.join(yb.conf.downloaddir, fname)
|
||||||
|
yb.buildTransaction()
|
||||||
|
try:
|
||||||
|
yb.processTransaction()
|
||||||
|
except SystemExit:
|
||||||
|
pass # It keeps passing an exit because it's downloading only. Get it together, RH.
|
||||||
|
yb.closeRpmDB()
|
||||||
|
yb.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getFiles(self):
|
||||||
|
for rf in self.rpm_files:
|
||||||
|
# TODO: check if we have the rpm module and if so, rip pkg name from it? use that as key instead of rf?
|
||||||
|
self.rpms[os.path.basename(rf)] = os.path.abspath(os.path.expanduser(rf))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def extractFiles(self):
|
||||||
|
# TODO: globbing or regex on self.paths?
|
||||||
|
# If we have yum, we can, TECHNICALLY, do this with:
|
||||||
|
# http://yum.baseurl.org/api/yum/rpmUtils/miscutils.html#rpmUtils.miscutils.rpm2cpio
|
||||||
|
# But nope. We can't selectively decompress members based on path with rpm2cpio-like funcs.
|
||||||
|
# We keep getting extraction artefacts, at least with legacy libarchive_c, so we use a hammer.
|
||||||
|
_curdir = os.getcwd()
|
||||||
|
_tempdir = tempfile.mkdtemp()
|
||||||
|
os.chdir(_tempdir)
|
||||||
|
for rpm_file in self.rpms:
|
||||||
|
rf = self.rpms[rpm_file]
|
||||||
|
if is_ctype:
|
||||||
|
if not is_legacy:
|
||||||
|
# ctype - extracts to pwd
|
||||||
|
with libarchive.file_reader(rf) as reader:
|
||||||
|
for entry in reader:
|
||||||
|
if self.paths and entry.path not in self.paths:
|
||||||
|
continue
|
||||||
|
if entry.isdir():
|
||||||
|
continue
|
||||||
|
fpath = os.path.join(self.dest_dir, rpm_file, entry.path)
|
||||||
|
if not os.path.isdir(os.path.dirname(fpath)):
|
||||||
|
os.makedirs(os.path.dirname(fpath))
|
||||||
|
with open(fpath, 'wb') as f:
|
||||||
|
for b in entry.get_blocks():
|
||||||
|
f.write(b)
|
||||||
|
else:
|
||||||
|
with libarchive.Archive(rf) as reader:
|
||||||
|
for entry in reader:
|
||||||
|
if (self.paths and entry.pathname not in self.paths) or (entry.isdir()):
|
||||||
|
continue
|
||||||
|
fpath = os.path.join(self.dest_dir, rpm_file, entry.pathname)
|
||||||
|
if not os.path.isdir(os.path.dirname(fpath)):
|
||||||
|
os.makedirs(os.path.dirname(fpath))
|
||||||
|
reader.readpath(fpath)
|
||||||
|
else:
|
||||||
|
# pyEasyArchive/"pypi/libarchive"
|
||||||
|
with lap.file_reader(rf) as reader:
|
||||||
|
for entry in reader:
|
||||||
|
if (self.paths and entry.pathname not in self.paths) or (entry.filetype.IFDIR):
|
||||||
|
continue
|
||||||
|
fpath = os.path.join(self.dest_dir, rpm_file, entry.pathname)
|
||||||
|
if not os.path.isdir(os.path.dirname(fpath)):
|
||||||
|
os.makedirs(os.path.dirname(fpath))
|
||||||
|
with open(fpath, 'wb') as f:
|
||||||
|
for b in entry.get_blocks():
|
||||||
|
f.write(b)
|
||||||
|
os.chdir(_curdir)
|
||||||
|
shutil.rmtree(_tempdir)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = ('This script allows you to extract files for a given package '
|
||||||
|
'{0}without installing any extra packages (such as yum-utils '
|
||||||
|
'for repoquery). '
|
||||||
|
'You must use at least one -r/--rpm{1}.').format(
|
||||||
|
('name(s) ' if has_yum else ''),
|
||||||
|
(', -p/--package, or both' if has_yum else '')))
|
||||||
|
args.add_argument('-d', '--dest-dir',
|
||||||
|
dest = 'dest_dir',
|
||||||
|
default = '/var/tmp/rpm_extract',
|
||||||
|
help = ('The destination for the extracted package file tree (in the format of '
|
||||||
|
'<dest_dir>/<pkg_nm>/<tree>). '
|
||||||
|
'Default: /var/tmp/rpm_extract'))
|
||||||
|
args.add_argument('-r', '--rpm',
|
||||||
|
dest = 'rpm_files',
|
||||||
|
metavar = 'PATH/TO/RPM',
|
||||||
|
action = 'append',
|
||||||
|
default = [],
|
||||||
|
help = ('If specified, use this RPM file instead of the system\'s RPM database. Can be '
|
||||||
|
'specified multiple times'))
|
||||||
|
if has_yum:
|
||||||
|
args.add_argument('-p', '--package',
|
||||||
|
dest = 'pkgs',
|
||||||
|
#nargs = 1,
|
||||||
|
metavar = 'PKGNAME',
|
||||||
|
action = 'append',
|
||||||
|
default = [],
|
||||||
|
help = ('If specified, restrict the list of packages to check against to only this package. '
|
||||||
|
'Can be specified multiple times. HIGHLY RECOMMENDED'))
|
||||||
|
args.add_argument('paths',
|
||||||
|
nargs = '+',
|
||||||
|
metavar = 'path/file/name.ext',
|
||||||
|
help = ('The path(s) of files to extract. If \'*\' is used, extract all files'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if has_argparse:
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
args['rpm_files'] = [os.path.abspath(os.path.expanduser(i)) for i in args['rpm_files']]
|
||||||
|
if not any((args['rpm_files'], args['pkgs'])):
|
||||||
|
exit(('You have not specified any package files{0}.\n'
|
||||||
|
'This is so dumb we are bailing out.\n').format((' or package names') if has_yum else ''))
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Please yum -y install python-argparse')
|
||||||
|
fe = FileExtractor(**args)
|
||||||
|
fe.extractFiles()
|
||||||
|
return()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
171
centos/find_changed_confs.py
Executable file
171
centos/find_changed_confs.py
Executable file
@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Supports CentOS 6.9 and up, untested on lower versions.
|
||||||
|
# Definitely probably won't work on 5.x since they use MD5(?), and 6.5? and up
|
||||||
|
# use SHA256.
|
||||||
|
|
||||||
|
# TODO: add support for .rpm files (like list_files_package.py)
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from sys import version_info as py_ver
|
||||||
|
try:
|
||||||
|
import rpm
|
||||||
|
except ImportError:
|
||||||
|
exit('This script only runs on RHEL/CentOS/other RPM-based distros.')
|
||||||
|
|
||||||
|
# Thanks, dude!
|
||||||
|
# https://blog.fpmurphy.com/2011/08/programmatically-retrieve-rpm-package-details.html
|
||||||
|
|
||||||
|
class PkgChk(object):
|
||||||
|
def __init__(self, dirpath, symlinks = True, pkgs = None):
|
||||||
|
self.path = dirpath
|
||||||
|
self.pkgs = pkgs
|
||||||
|
self.symlinks = symlinks
|
||||||
|
self.orig_pkgs = copy.deepcopy(pkgs)
|
||||||
|
self.pkgfilemap = {}
|
||||||
|
self.flatfiles = []
|
||||||
|
self.flst = {}
|
||||||
|
self.trns = rpm.TransactionSet()
|
||||||
|
self.getFiles()
|
||||||
|
self.getActualFiles()
|
||||||
|
|
||||||
|
def getFiles(self):
|
||||||
|
if not self.pkgs:
|
||||||
|
for p in self.trns.dbMatch():
|
||||||
|
self.pkgs.append(p['name'])
|
||||||
|
for p in self.pkgs:
|
||||||
|
for pkg in self.trns.dbMatch('name', p):
|
||||||
|
# Get the canonical package name
|
||||||
|
_pkgnm = pkg.sprintf('%{NAME}')
|
||||||
|
self.pkgfilemap[_pkgnm] = {}
|
||||||
|
# Get the list of file(s) and their MD5 hash(es)
|
||||||
|
for f in pkg.fiFromHeader():
|
||||||
|
if not f[0].startswith(self.path):
|
||||||
|
continue
|
||||||
|
if f[12] == '0' * 64:
|
||||||
|
_hash = None
|
||||||
|
else:
|
||||||
|
_hash = f[12]
|
||||||
|
self.pkgfilemap[_pkgnm][f[0]] = {'hash': _hash,
|
||||||
|
'date': f[3],
|
||||||
|
'size': f[1]}
|
||||||
|
self.flatfiles.append(f[0])
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getActualFiles(self):
|
||||||
|
print('Getting a list of local files and their hashes.')
|
||||||
|
print('Please wait...\n')
|
||||||
|
for root, dirs, files in os.walk(self.path):
|
||||||
|
for f in files:
|
||||||
|
_fpath = os.path.join(root, f)
|
||||||
|
_stat = os.stat(_fpath)
|
||||||
|
if _fpath in self.flatfiles:
|
||||||
|
_hash = hashlib.sha256()
|
||||||
|
with open(_fpath, 'rb') as r:
|
||||||
|
for chunk in iter(lambda: r.read(4096), b''):
|
||||||
|
_hash.update(chunk)
|
||||||
|
self.flst[_fpath] = {'hash': str(_hash.hexdigest()),
|
||||||
|
'date': int(_stat.st_mtime),
|
||||||
|
'size': _stat.st_size}
|
||||||
|
else:
|
||||||
|
# It's not even in the package, so don't waste time
|
||||||
|
# with generating hashes or anything else.
|
||||||
|
self.flst[_fpath] = {'hash': None}
|
||||||
|
return()
|
||||||
|
|
||||||
|
def compareFiles(self):
|
||||||
|
for f in self.flst.keys():
|
||||||
|
if f not in self.flatfiles:
|
||||||
|
if not self.orig_pkgs:
|
||||||
|
print(('{0} is not installed by any package.').format(f))
|
||||||
|
else:
|
||||||
|
print(('{0} is not installed by package(s) ' +
|
||||||
|
'specified.').format(f))
|
||||||
|
else:
|
||||||
|
for p in self.pkgs:
|
||||||
|
if f not in self.pkgfilemap[p].keys():
|
||||||
|
continue
|
||||||
|
if (f in self.flst.keys() and
|
||||||
|
(self.flst[f]['hash'] !=
|
||||||
|
self.pkgfilemap[p][f]['hash'])):
|
||||||
|
if not self.symlinks:
|
||||||
|
if ((not self.pkgfilemap[p][f]['hash'])
|
||||||
|
or re.search('^0+$',
|
||||||
|
self.pkgfilemap[p][f]['hash'])):
|
||||||
|
continue
|
||||||
|
r_time = datetime.datetime.fromtimestamp(
|
||||||
|
self.pkgfilemap[p][f]['date'])
|
||||||
|
r_hash = self.pkgfilemap[p][f]['hash']
|
||||||
|
r_size = self.pkgfilemap[p][f]['size']
|
||||||
|
l_time = datetime.datetime.fromtimestamp(
|
||||||
|
self.flst[f]['date'])
|
||||||
|
l_hash = self.flst[f]['hash']
|
||||||
|
l_size = self.flst[f]['size']
|
||||||
|
r_str = ('\n{0} differs per {1}:\n' +
|
||||||
|
'\tRPM:\n' +
|
||||||
|
'\t\tSHA256: {2}\n' +
|
||||||
|
'\t\tBYTES: {3}\n' +
|
||||||
|
'\t\tDATE: {4}').format(f, p,
|
||||||
|
r_hash,
|
||||||
|
r_size,
|
||||||
|
r_time)
|
||||||
|
l_str = ('\tLOCAL:\n' +
|
||||||
|
'\t\tSHA256: {0}\n' +
|
||||||
|
'\t\tBYTES: {1}\n' +
|
||||||
|
'\t\tDATE: {2}').format(l_hash,
|
||||||
|
l_size,
|
||||||
|
l_time)
|
||||||
|
print(r_str)
|
||||||
|
print(l_str)
|
||||||
|
# Now we print missing files
|
||||||
|
for f in sorted(list(set(self.flatfiles))):
|
||||||
|
if not os.path.exists(f):
|
||||||
|
print('{0} was deleted from the filesystem.'.format(f))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
def dirchk(path):
|
||||||
|
p = os.path.abspath(path)
|
||||||
|
if not os.path.isdir(p):
|
||||||
|
raise argparse.ArgumentTypeError(('{0} is not a valid ' +
|
||||||
|
'directory').format(path))
|
||||||
|
return(p)
|
||||||
|
args = argparse.ArgumentParser(description = ('Get a list of config ' +
|
||||||
|
'files that have changed ' +
|
||||||
|
'from the package\'s ' +
|
||||||
|
'defaults'))
|
||||||
|
args.add_argument('-l', '--ignore-symlinks',
|
||||||
|
dest = 'symlinks',
|
||||||
|
action = 'store_false',
|
||||||
|
help = ('If specified, don\'t track files that are ' +
|
||||||
|
'symlinks in the RPM'))
|
||||||
|
args.add_argument('-p', '--package',
|
||||||
|
dest = 'pkgs',
|
||||||
|
#nargs = 1,
|
||||||
|
metavar = 'PKGNAME',
|
||||||
|
action = 'append',
|
||||||
|
default = [],
|
||||||
|
help = ('If specified, restrict the list of ' +
|
||||||
|
'packages to check against to only this ' +
|
||||||
|
'package. Can be specified multiple times. ' +
|
||||||
|
'HIGHLY RECOMMENDED'))
|
||||||
|
args.add_argument('dirpath',
|
||||||
|
type = dirchk,
|
||||||
|
metavar = 'path/to/directory',
|
||||||
|
help = ('The path to the directory containing the ' +
|
||||||
|
'configuration files to check against (e.g. ' +
|
||||||
|
'"/etc/ssh")'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
p = PkgChk(**args)
|
||||||
|
p.compareFiles()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
92
centos/isomirror_sort.py
Executable file
92
centos/isomirror_sort.py
Executable file
@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# requires python lxml module as well
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
from urllib.request import urlopen
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
# The page that contains the list of (authoritative ISO) mirrors
|
||||||
|
URL = 'http://isoredirect.centos.org/centos/7/isos/x86_64/'
|
||||||
|
# The formatting on the page is pretty simple - no divs, etc. - so we need to
|
||||||
|
# blacklist some links we pull in.
|
||||||
|
blacklisted_link_URLs = ('http://bittorrent.com/',
|
||||||
|
'http://wiki.centos.org/AdditionalResources/Repositories')
|
||||||
|
|
||||||
|
mirrors = {}
|
||||||
|
|
||||||
|
dflt_ports = {'https': 443, # unlikely. "HTTPS is currently not used for mirrors." per https://wiki.centos.org/HowTos/CreatePublicMirrors
|
||||||
|
'http': 80, # most likely.
|
||||||
|
'ftp': 21,
|
||||||
|
'rsync': 873}
|
||||||
|
|
||||||
|
def getMirrors():
|
||||||
|
mirrors = []
|
||||||
|
with urlopen(URL) as u:
|
||||||
|
pg_src = u.read().decode('utf-8')
|
||||||
|
soup = BeautifulSoup(pg_src, 'lxml')
|
||||||
|
for tag in soup.find_all('br')[4].next_siblings:
|
||||||
|
if tag.name == 'a' and tag['href'] not in blacklisted_link_URLs:
|
||||||
|
mirrors.append(tag['href'].strip())
|
||||||
|
return(mirrors)
|
||||||
|
|
||||||
|
def getHosts(mirror):
|
||||||
|
port = None
|
||||||
|
fqdn = None
|
||||||
|
login = ''
|
||||||
|
# "mirror" should be a base URI of the CentOS mirror path.
|
||||||
|
# mirrors.centos.org is pointless to use for this!
|
||||||
|
#url = os.path.join(mirror, 'sha256sum.txt.asc')
|
||||||
|
uri = urlparse(mirror)
|
||||||
|
spl_dom = uri.netloc.split(':')
|
||||||
|
if len(spl_dom) >= 2: # more complex URI
|
||||||
|
if len(spl_dom) == 2: # probably domain:port?
|
||||||
|
try:
|
||||||
|
port = int(spl_dom[-1:])
|
||||||
|
except ValueError: # ooookay, so it's not domain:port, it's a user:pass@
|
||||||
|
if '@' in uri.netloc:
|
||||||
|
auth = uri.netloc.split('@')
|
||||||
|
fqdn = auth[1]
|
||||||
|
login = auth[0] + '@'
|
||||||
|
elif len(spl_dom) > 2: # even more complex URI, which ironically makes parsing easier
|
||||||
|
auth = uri.netloc.split('@')
|
||||||
|
fqdn = spl_dom[1].split('@')[1]
|
||||||
|
port = int(spl_dom[-1:])
|
||||||
|
login = auth[0] + '@'
|
||||||
|
# matches missing values and simple URI. like, 99%+ of mirror URIs being passed.
|
||||||
|
if not fqdn:
|
||||||
|
fqdn = uri.netloc
|
||||||
|
if not port:
|
||||||
|
port = dflt_ports[uri.scheme]
|
||||||
|
mirrors[fqdn] = {'proto': uri.scheme,
|
||||||
|
'port': port,
|
||||||
|
'path': uri.path,
|
||||||
|
'auth': login}
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getSpeeds():
|
||||||
|
for fqdn in mirrors.keys():
|
||||||
|
start = time.time()
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.connect((fqdn, mirrors[fqdn]['port']))
|
||||||
|
mirrors[fqdn]['time'] = time.time() - start
|
||||||
|
sock.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for m in getMirrors():
|
||||||
|
getHosts(m)
|
||||||
|
getSpeeds()
|
||||||
|
ranking = sorted(mirrors.keys(), key = lambda k: (mirrors[k]['time']))
|
||||||
|
for i in ranking:
|
||||||
|
str_port = ':' + str(mirrors[i]['port'])
|
||||||
|
if mirrors[i]['port'] in dflt_ports.values():
|
||||||
|
str_port = ''
|
||||||
|
print('{proto}://{auth}{0}{p}{path}'.format(i,
|
||||||
|
**mirrors[i],
|
||||||
|
p = str_port))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
155
centos/list_files_package.py
Executable file
155
centos/list_files_package.py
Executable file
@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Supports CentOS 6.9 and up, untested on lower versions.
|
||||||
|
# Lets you get a list of files for a given package name(s) without installing
|
||||||
|
# any extra packages (such as yum-utils for repoquery).
|
||||||
|
|
||||||
|
# NOTE: If you're on CentOS 6.x, since it uses such an ancient version of python you need to either install
|
||||||
|
# python-argparse OR just resign to using it for all packages with none of the features.
|
||||||
|
try:
|
||||||
|
import argparse
|
||||||
|
has_argparse = True
|
||||||
|
except ImportError:
|
||||||
|
has_argparse = False
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
# For when CentOS/RHEL switch to python 3 by default (if EVER).
|
||||||
|
import sys
|
||||||
|
pyver = sys.version_info
|
||||||
|
try:
|
||||||
|
import rpm
|
||||||
|
except ImportError:
|
||||||
|
exit('This script only runs on the system-provided Python on RHEL/CentOS/other RPM-based distros.')
|
||||||
|
|
||||||
|
def all_pkgs():
|
||||||
|
# Gets a list of all packages.
|
||||||
|
pkgs = []
|
||||||
|
trns = rpm.TransactionSet()
|
||||||
|
for p in trns.dbMatch():
|
||||||
|
pkgs.append(p['name'])
|
||||||
|
pkgs = list(sorted(set(pkgs)))
|
||||||
|
return(pkgs)
|
||||||
|
|
||||||
|
class FileGetter(object):
|
||||||
|
def __init__(self, symlinks = True, verbose = False, *args, **kwargs):
|
||||||
|
self.symlinks = symlinks
|
||||||
|
self.verbose = verbose
|
||||||
|
self.trns = rpm.TransactionSet()
|
||||||
|
self.files = {}
|
||||||
|
for p in kwargs['pkgs']:
|
||||||
|
if p not in self.files.keys():
|
||||||
|
self.getFiles(p)
|
||||||
|
if kwargs['rpm_files']:
|
||||||
|
self.getLocalFiles(kwargs['rpm_files'])
|
||||||
|
|
||||||
|
def getLocalFiles(self, rpm_files):
|
||||||
|
# Needed because the rpm module can't handle arbitrary rpm files??? If it can, someone let me know.
|
||||||
|
# According to http://rpm5.org/docs/api/classRpmhdr.html#_details I can.
|
||||||
|
import yum
|
||||||
|
for r in rpm_files:
|
||||||
|
pkg = yum.YumLocalPackage(ts = self.trns,
|
||||||
|
filename = r)
|
||||||
|
_pkgnm = pkg.hdr.sprintf('%{NAME}')
|
||||||
|
if _pkgnm in self.files:
|
||||||
|
continue
|
||||||
|
if self.verbose:
|
||||||
|
self.files[_pkgnm] = {}
|
||||||
|
else:
|
||||||
|
self.files[_pkgnm] = []
|
||||||
|
for f in pkg.hdr.fiFromHeader():
|
||||||
|
_symlink = (True if re.search('^0+$', f[12]) else False)
|
||||||
|
if self.verbose:
|
||||||
|
if _symlink:
|
||||||
|
if self.symlinks:
|
||||||
|
self.files[_pkgnm][f[0]] = '(symbolic link or directory)'
|
||||||
|
continue
|
||||||
|
self.files[_pkgnm][f[0]] = f[12]
|
||||||
|
else:
|
||||||
|
# Skip if it is a symlink but they aren't enabled
|
||||||
|
if _symlink and not self.symlinks:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.files[_pkgnm].append(f[0])
|
||||||
|
self.files[_pkgnm].sort()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getFiles(self, pkgnm):
|
||||||
|
for pkg in self.trns.dbMatch('name', pkgnm):
|
||||||
|
# The canonical package name
|
||||||
|
_pkgnm = pkg.sprintf('%{NAME}')
|
||||||
|
# Return just a list of files, or a dict of filepath:hash if verbose is enabled.
|
||||||
|
if self.verbose:
|
||||||
|
self.files[_pkgnm] = {}
|
||||||
|
else:
|
||||||
|
self.files[_pkgnm] = []
|
||||||
|
for f in pkg.fiFromHeader():
|
||||||
|
_symlink = (True if re.search('^0+$', f[12]) else False)
|
||||||
|
if self.verbose:
|
||||||
|
if _symlink:
|
||||||
|
if self.symlinks:
|
||||||
|
self.files[_pkgnm][f[0]] = '(symbolic link)'
|
||||||
|
continue
|
||||||
|
self.files[_pkgnm][f[0]] = f[12]
|
||||||
|
else:
|
||||||
|
# Skip if it is a symlink but they aren't enabled
|
||||||
|
if _symlink and not self.symlinks:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.files[_pkgnm].append(f[0])
|
||||||
|
self.files[_pkgnm].sort()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = ('This script allows you get a list of files for a given package '
|
||||||
|
'name(s) without installing any extra packages (such as yum-utils '
|
||||||
|
'for repoquery). It is highly recommended to use at least one '
|
||||||
|
'-r/--rpm, -p/--package, or both.'))
|
||||||
|
args.add_argument('-l', '--ignore-symlinks',
|
||||||
|
dest = 'symlinks',
|
||||||
|
action = 'store_false',
|
||||||
|
help = ('If specified, don\'t report files that are symlinks in the RPM'))
|
||||||
|
args.add_argument('-v', '--verbose',
|
||||||
|
dest = 'verbose',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, include the hashes of the files'))
|
||||||
|
args.add_argument('-r', '--rpm',
|
||||||
|
dest = 'rpm_files',
|
||||||
|
metavar = 'PATH/TO/RPM',
|
||||||
|
action = 'append',
|
||||||
|
default = [],
|
||||||
|
help = ('If specified, use this RPM file instead of the system\'s RPM database. Can be '
|
||||||
|
'specified multiple times'))
|
||||||
|
args.add_argument('-p', '--package',
|
||||||
|
dest = 'pkgs',
|
||||||
|
#nargs = 1,
|
||||||
|
metavar = 'PKGNAME',
|
||||||
|
action = 'append',
|
||||||
|
default = [],
|
||||||
|
help = ('If specified, restrict the list of packages to check against to only this package. Can '
|
||||||
|
'be specified multiple times. HIGHLY RECOMMENDED'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if has_argparse:
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
args['rpm_files'] = [os.path.abspath(os.path.expanduser(i)) for i in args['rpm_files']]
|
||||||
|
if not any((args['rpm_files'], args['pkgs'])):
|
||||||
|
prompt_str = ('You have not specified any package names.\nThis means we will get file lists for EVERY SINGLE '
|
||||||
|
'installed package.\nThis is a LOT of output and can take a few moments.\nIf this was a mistake, '
|
||||||
|
'you can hit ctrl-c now.\nOtherwise, hit the enter key to continue.\n')
|
||||||
|
sys.stderr.write(prompt_str)
|
||||||
|
if pyver.major >= 3:
|
||||||
|
input()
|
||||||
|
elif pyver.major == 2:
|
||||||
|
raw_input()
|
||||||
|
args['pkgs'] = all_pkgs()
|
||||||
|
else:
|
||||||
|
args = {'pkgs': all_pkgs(),
|
||||||
|
'rpm_files': []}
|
||||||
|
gf = FileGetter(**args)
|
||||||
|
print(json.dumps(gf.files, indent = 4))
|
||||||
|
return()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
192
centos/list_pkgs.py
Executable file
192
centos/list_pkgs.py
Executable file
@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Supports CentOS 6.9 and up, untested on lower versions.
|
||||||
|
# Lets you dump a list of installed packages for backup purposes
|
||||||
|
# Reference: https://blog.fpmurphy.com/2011/08/programmatically-retrieve-rpm-package-details.html
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
import yum
|
||||||
|
except ImportError:
|
||||||
|
exit('This script only runs on RHEL/CentOS/other yum-based distros.')
|
||||||
|
# Detect RH version.
|
||||||
|
ver_re = re.compile('^(centos( linux)? release) ([0-9\.]+) .*$', re.IGNORECASE)
|
||||||
|
# distro module isn't stdlib, and platform.linux_distribution() (AND platform.distro()) are both deprecated in 3.7.
|
||||||
|
# So we get hacky.
|
||||||
|
with open('/etc/redhat-release', 'r') as f:
|
||||||
|
ver = [int(i) for i in ver_re.sub('\g<3>', f.read().strip()).split('.')]
|
||||||
|
import pprint
|
||||||
|
|
||||||
|
repo_re = re.compile('^@')
|
||||||
|
|
||||||
|
class PkgIndexer(object):
|
||||||
|
def __init__(self, **args):
|
||||||
|
self.pkgs = []
|
||||||
|
self.args = args
|
||||||
|
self.yb = yum.YumBase()
|
||||||
|
# Make the Yum API shut the heck up.
|
||||||
|
self.yb.preconf.debuglevel = 0
|
||||||
|
self.yb.preconf.errorlevel = 0
|
||||||
|
self._pkgs = self._pkglst()
|
||||||
|
self._build_pkginfo()
|
||||||
|
if self.args['report'] == 'csv':
|
||||||
|
self._gen_csv()
|
||||||
|
elif self.args['report'] == 'json':
|
||||||
|
self._gen_json()
|
||||||
|
elif self.args['report'] == 'xml':
|
||||||
|
self._gen_xml()
|
||||||
|
|
||||||
|
def _pkglst(self):
|
||||||
|
pkgs = []
|
||||||
|
# Get the list of packages
|
||||||
|
if self.args['reason'] != 'all':
|
||||||
|
for p in sorted(self.yb.rpmdb.returnPackages()):
|
||||||
|
if 'reason' not in p.yumdb_info:
|
||||||
|
continue
|
||||||
|
reason = getattr(p.yumdb_info, 'reason')
|
||||||
|
if reason == self.args['reason']:
|
||||||
|
pkgs.append(p)
|
||||||
|
else:
|
||||||
|
pkgs = sorted(self.yb.rpmdb.returnPackages())
|
||||||
|
return(pkgs)
|
||||||
|
|
||||||
|
def _build_pkginfo(self):
|
||||||
|
for p in self._pkgs:
|
||||||
|
_pkg = {'name': p.name,
|
||||||
|
'desc': p.summary,
|
||||||
|
'version': p.ver,
|
||||||
|
'release': p.release,
|
||||||
|
'arch': p.arch,
|
||||||
|
'built': datetime.datetime.fromtimestamp(p.buildtime),
|
||||||
|
'installed': datetime.datetime.fromtimestamp(p.installtime),
|
||||||
|
'repo': repo_re.sub('', p.ui_from_repo),
|
||||||
|
'sizerpm': p.packagesize,
|
||||||
|
'sizedisk': p.installedsize}
|
||||||
|
self.pkgs.append(_pkg)
|
||||||
|
|
||||||
|
def _gen_csv(self):
|
||||||
|
if self.args['plain']:
|
||||||
|
_fields = ['name']
|
||||||
|
else:
|
||||||
|
_fields = ['name', 'version', 'release', 'arch', 'desc', 'built',
|
||||||
|
'installed', 'repo', 'sizerpm', 'sizedisk']
|
||||||
|
import csv
|
||||||
|
if sys.hexversion >= 0x30000f0:
|
||||||
|
_buf = io.StringIO()
|
||||||
|
else:
|
||||||
|
_buf = io.BytesIO()
|
||||||
|
_csv = csv.writer(_buf, delimiter = self.args['sep_char'])
|
||||||
|
if self.args['header']:
|
||||||
|
if self.args['plain']:
|
||||||
|
_csv.writerow(['Name'])
|
||||||
|
else:
|
||||||
|
_csv.writerow(['Name', 'Version', 'Release', 'Architecture', 'Description', 'Build Time',
|
||||||
|
'Install Time', 'Repository', 'Size (RPM)', 'Size (On-Disk)'])
|
||||||
|
_csv = csv.DictWriter(_buf, fieldnames = _fields, extrasaction = 'ignore', delimiter = self.args['sep_char'])
|
||||||
|
for p in self.pkgs:
|
||||||
|
_csv.writerow(p)
|
||||||
|
_buf.seek(0, 0)
|
||||||
|
self.report = _buf.read().replace('\r\n', '\n')
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _gen_json(self):
|
||||||
|
import json
|
||||||
|
if self.args['plain']:
|
||||||
|
self.report = json.dumps([p['name'] for p in self.pkgs], indent = 4)
|
||||||
|
else:
|
||||||
|
self.report = json.dumps(self.pkgs, default = str, indent = 4)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _gen_xml(self):
|
||||||
|
from lxml import etree
|
||||||
|
_xml = etree.Element('packages')
|
||||||
|
for p in self.pkgs:
|
||||||
|
_attrib = copy.deepcopy(p)
|
||||||
|
for i in ('built', 'installed', 'sizerpm', 'sizedisk'):
|
||||||
|
_attrib[i] = str(_attrib[i])
|
||||||
|
if self.args['plain']:
|
||||||
|
_pkg = etree.Element('package', attrib = {'name': p['name']})
|
||||||
|
else:
|
||||||
|
_pkg = etree.Element('package', attrib = _attrib)
|
||||||
|
_xml.append(_pkg)
|
||||||
|
#del(_attrib['name']) # I started to make it a more complex, nested structure... is that necessary?
|
||||||
|
if self.args['header']:
|
||||||
|
self.report = etree.tostring(_xml, pretty_print = True, xml_declaration = True, encoding = 'UTF-8')
|
||||||
|
else:
|
||||||
|
self.report = etree.tostring(_xml, pretty_print = True)
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = ('This script lets you dump the list of installed packages'))
|
||||||
|
args.add_argument('-p', '--plain',
|
||||||
|
dest = 'plain',
|
||||||
|
action = 'store_true',
|
||||||
|
help = 'If specified, only create a list of plain package names (i.e. don\'t include extra '
|
||||||
|
'information)')
|
||||||
|
args.add_argument('-n', '--no-header',
|
||||||
|
dest = 'header',
|
||||||
|
action = 'store_false',
|
||||||
|
help = 'If specified, do not print column headers/XML headers')
|
||||||
|
args.add_argument('-s', '--separator',
|
||||||
|
dest = 'sep_char',
|
||||||
|
default = ',',
|
||||||
|
help = 'The separator used to split fields in the output (default: ,) (only used for CSV '
|
||||||
|
'reports)')
|
||||||
|
rprt = args.add_mutually_exclusive_group()
|
||||||
|
rprt.add_argument('-c', '--csv',
|
||||||
|
dest = 'report',
|
||||||
|
default = 'csv',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'csv',
|
||||||
|
help = 'Generate CSV output (this is the default). See -n/--no-header, -s/--separator')
|
||||||
|
rprt.add_argument('-x', '--xml',
|
||||||
|
dest = 'report',
|
||||||
|
default = 'csv',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'xml',
|
||||||
|
help = 'Generate XML output (requires the LXML module: yum install python-lxml)')
|
||||||
|
rprt.add_argument('-j', '--json',
|
||||||
|
dest = 'report',
|
||||||
|
default = 'csv',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'json',
|
||||||
|
help = 'Generate JSON output')
|
||||||
|
rsn = args.add_mutually_exclusive_group()
|
||||||
|
rsn.add_argument('-a', '--all',
|
||||||
|
dest = 'reason',
|
||||||
|
default = 'all',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'all',
|
||||||
|
help = ('Parse/report all packages that are currently installed. '
|
||||||
|
'Conflicts with -u/--user and -d/--dep. '
|
||||||
|
'This is the default'))
|
||||||
|
rsn.add_argument('-u', '--user',
|
||||||
|
dest = 'reason',
|
||||||
|
default = 'all',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'user',
|
||||||
|
help = ('Parse/report only packages which were explicitly installed. '
|
||||||
|
'Conflicts with -a/--all and -d/--dep'))
|
||||||
|
rsn.add_argument('-d', '--dep',
|
||||||
|
dest = 'reason',
|
||||||
|
default = 'all',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'dep',
|
||||||
|
help = ('Parse/report only packages which were installed to satisfy a dependency. '
|
||||||
|
'Conflicts with -a/--all and -u/--user'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
p = PkgIndexer(**args)
|
||||||
|
print(p.report)
|
||||||
|
return()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
119
git/remotehooks.py
Executable file
119
git/remotehooks.py
Executable file
@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import ast # Needed for localhost cmd strings
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
modules = {}
|
||||||
|
try:
|
||||||
|
import git
|
||||||
|
modules['git'] = True
|
||||||
|
except ImportError:
|
||||||
|
import subprocess
|
||||||
|
modules['git'] = False
|
||||||
|
try:
|
||||||
|
import paramiko
|
||||||
|
import socket
|
||||||
|
modules['ssh'] = True
|
||||||
|
except ImportError:
|
||||||
|
modules['ssh'] = False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
repos = {}
|
||||||
|
repos['bdisk'] = {'remotecmds': {'g.rainwreck.com': {'gitbot': {'cmds': ['git -C /var/lib/gitbot/clonerepos/BDisk pull',
|
||||||
|
'git -C /var/lib/gitbot/clonerepos/BDisk pull --tags',
|
||||||
|
'asciidoctor /var/lib/gitbot/clonerepos/BDisk/docs/manual/HEAD.adoc -o /srv/http/bdisk/index.html']}}}}
|
||||||
|
repos['test'] = {'remotecmds': {'g.rainwreck.com': {'gitbot': {'cmds': ['echo $USER']}}}}
|
||||||
|
repos['games-site'] = {'remotecmds': {'games.square-r00t.net':
|
||||||
|
{'gitbot':
|
||||||
|
{'cmds': ['cd /srv/http/games-site && git pull']}}}}
|
||||||
|
repos['aif-ng'] = {'cmds': [['asciidoctor', '/opt/git/repo.checkouts/aif-ng/docs/README.adoc', '-o', '/srv/http/aif/index.html']]}
|
||||||
|
|
||||||
|
def execHook(gitinfo = False):
|
||||||
|
if not gitinfo:
|
||||||
|
gitinfo = getGitInfo()
|
||||||
|
repo = gitinfo['repo'].lower()
|
||||||
|
print('Executing hooks for {0}:{1}...'.format(repo, gitinfo['branch']))
|
||||||
|
print('This commit: {0}\nLast commit: {1}'.format(gitinfo['currev'], gitinfo['oldrev']))
|
||||||
|
# Execute local commands first
|
||||||
|
if 'cmds' in repos[repo].keys():
|
||||||
|
for cmd in repos[repo]['cmds']:
|
||||||
|
print('\tExecuting {0}...'.format(' '.join(cmd)))
|
||||||
|
subprocess.call(cmd)
|
||||||
|
if 'remotecmds' in repos[repo].keys():
|
||||||
|
for host in repos[repo]['remotecmds'].keys():
|
||||||
|
if 'port' in repos[repo]['remotecmds'][host].keys():
|
||||||
|
port = int(repos[repo]['remotecmds'][host]['port'])
|
||||||
|
else:
|
||||||
|
port = 22
|
||||||
|
for user in repos[repo]['remotecmds'][host].keys():
|
||||||
|
print('{0}@{1}:'.format(user, host))
|
||||||
|
if paramikomodule:
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect(host, username = user, port = port)
|
||||||
|
try:
|
||||||
|
for cmd in repos[repo]['remotecmds'][host][user]['cmds']:
|
||||||
|
print('\tExecuting \'{0}\'...'.format(cmd))
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||||
|
stdout = stdout.read().decode('utf-8')
|
||||||
|
stderr = stderr.read().decode('utf-8')
|
||||||
|
print(stdout)
|
||||||
|
if stderr != '':
|
||||||
|
print(stderr)
|
||||||
|
except paramiko.AuthenticationException:
|
||||||
|
print('({0}@{1}) AUTHENTICATION FAILED!'.format(user, host))
|
||||||
|
except paramiko.BadHostKeyException:
|
||||||
|
print('({0}@{1}) INCORRECT HOSTKEY!'.format(user, host))
|
||||||
|
except paramiko.SSHException:
|
||||||
|
print('({0}@{1}) FAILED TO ESTABLISH SSH!'.format(user, host))
|
||||||
|
except socket.error:
|
||||||
|
print('({0}@{1}) SOCKET CONNECTION FAILURE! (DNS, timeout/firewall, etc.)'.format(user, host))
|
||||||
|
else:
|
||||||
|
for cmd in repos[repo]['remotecmds'][host][user]['cmds']:
|
||||||
|
try:
|
||||||
|
print('\tExecuting \'{0}\'...'.format(cmd))
|
||||||
|
subprocess.call(['ssh', '{0}@{1}'.format(user, host), cmd])
|
||||||
|
except:
|
||||||
|
print('({0}@{1}) An error occurred!'.format(user, host))
|
||||||
|
|
||||||
|
def getGitInfo():
|
||||||
|
refs = sys.argv[1].split('/')
|
||||||
|
gitinfo = {}
|
||||||
|
if refs[1] == 'tags':
|
||||||
|
gitinfo['branch'] = False
|
||||||
|
gitinfo['tag'] = refs[2]
|
||||||
|
elif refs[1] == 'heads':
|
||||||
|
gitinfo['branch'] = refs[2]
|
||||||
|
gitinfo['tag'] = False
|
||||||
|
gitinfo['repo'] = os.environ['GL_REPO']
|
||||||
|
gitinfo['user'] = os.environ['GL_USER']
|
||||||
|
clientinfo = os.environ['SSH_CONNECTION'].split()
|
||||||
|
gitinfo['ssh'] = {'client': {'ip': clientinfo[0], 'port': clientinfo[1]},
|
||||||
|
'server': {'ip': clientinfo[2], 'port': clientinfo[3]},
|
||||||
|
'user': os.environ['USER']
|
||||||
|
}
|
||||||
|
if os.environ['GIT_DIR'] == '.':
|
||||||
|
gitinfo['dir'] = os.environ['PWD']
|
||||||
|
else:
|
||||||
|
#gitinfo['dir'] = os.path.join(os.environ['GL_REPO_BASE'], gitinfo['repo'], '.git')
|
||||||
|
gitinfo['dir'] = os.path.abspath(os.path.expanduser(os.environ['GIT_DIR']))
|
||||||
|
if gitmodule:
|
||||||
|
# This is preferred, because it's a lot more faster and a lot more flexible.
|
||||||
|
#https://gitpython.readthedocs.io/en/stable
|
||||||
|
gitobj = git.Repo(gitinfo['dir'])
|
||||||
|
commits = list(gitobj.iter_commits(gitobj.head.ref.name, max_count = 2))
|
||||||
|
else:
|
||||||
|
commits = subprocess.check_output(['git', 'rev-parse', 'HEAD..HEAD^1']).decode('utf-8').splitlines()
|
||||||
|
gitinfo['oldrev'] = re.sub('^\^', '', commits[1])
|
||||||
|
gitinfo['currev'] = re.sub('^\^', '', commits[0])
|
||||||
|
return(gitinfo)
|
||||||
|
#sys.exit(0)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
execHook()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
69
git/remotehooks2.py
Executable file
69
git/remotehooks2.py
Executable file
@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
# Can we use paramiko for remotecmds?
|
||||||
|
try:
|
||||||
|
import paramiko
|
||||||
|
import socket
|
||||||
|
has_ssh = True
|
||||||
|
except ImportError:
|
||||||
|
has_ssh = False
|
||||||
|
# Can we use the python git module?
|
||||||
|
try:
|
||||||
|
import git # "python-gitpython" in Arch; https://github.com/gitpython-developers/gitpython
|
||||||
|
has_git = True
|
||||||
|
except ImportError:
|
||||||
|
has_git = False
|
||||||
|
|
||||||
|
|
||||||
|
class repoHooks(object):
|
||||||
|
def __init__(self):
|
||||||
|
with open(os.path.join(os.environ['HOME'],
|
||||||
|
'.gitolite',
|
||||||
|
'local',
|
||||||
|
'hooks',
|
||||||
|
'repo-specific',
|
||||||
|
'githooks.json'), 'r') as f:
|
||||||
|
self.cfg = json.loads(f.read())
|
||||||
|
self.repos = list(self.cfg.keys())
|
||||||
|
self.env = os.environ.copy()
|
||||||
|
if 'GIT_DIR' in self.env.keys():
|
||||||
|
del(self.env['GIT_DIR'])
|
||||||
|
self.repo = self.env['GL_REPO']
|
||||||
|
|
||||||
|
def remoteExec(self):
|
||||||
|
for _host in self.repos[self.repo]['remotecmds'].keys():
|
||||||
|
if len(_host.split(':')) == 2:
|
||||||
|
_server, _port = [i.strip() for i in _host.split(':')]
|
||||||
|
else:
|
||||||
|
_port = 22
|
||||||
|
_server = _host.split(':')[0]
|
||||||
|
_h = self.repos[self.repo]['remotecmds'][_host]
|
||||||
|
for _user in _h.keys():
|
||||||
|
_u = _h[_user]
|
||||||
|
if has_ssh:
|
||||||
|
_ssh = paramiko.SSHClient()
|
||||||
|
_ssh.load_system_host_keys()
|
||||||
|
_ssh.missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
_ssh.connect(_server,
|
||||||
|
int(_port),
|
||||||
|
_user)
|
||||||
|
for _cmd in _h.keys():
|
||||||
|
pass # DO STUFF HERE
|
||||||
|
else:
|
||||||
|
return() # no-op; no paramiko
|
||||||
|
|
||||||
|
def localExec(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def main():
|
||||||
|
h = repoHooks()
|
||||||
|
if h.repo not in h.repos:
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
27
git/sample.githooks.json
Normal file
27
git/sample.githooks.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# remotehooks.py should go in your <gitolite repo>/local/hooks/repo-specific directory,
|
||||||
|
# along with the (uncommented) format of this file configured for your particular hooks
|
||||||
|
# "cmds" is a list of commands performed locally on the gitolite server,
|
||||||
|
# "remotecmds" contains a recursive directory of commands to run remotely
|
||||||
|
|
||||||
|
{
|
||||||
|
"<REPO_NAME>": {
|
||||||
|
"remotecmds": {
|
||||||
|
"<HOST_OR_IP_ADDRESS>": {
|
||||||
|
"<USER>": {
|
||||||
|
"cmds": [
|
||||||
|
"<COMMAND_1>",
|
||||||
|
"<COMMAND_2>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"<REPO2_NAME>": {
|
||||||
|
"cmds": [
|
||||||
|
[
|
||||||
|
"<LOCAL_COMMAND_1>",
|
||||||
|
"<LOCAL_COMMAND_2>"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
353
gpg/kant.py
353
gpg/kant.py
@ -1,353 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import datetime
|
|
||||||
import email
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from io import BytesIO
|
|
||||||
from socket import *
|
|
||||||
import urllib.parse
|
|
||||||
import gpgme # non-stdlib; Arch package is "python-pygpgme"
|
|
||||||
|
|
||||||
# TODO:
|
|
||||||
# -attach pubkey when sending below email
|
|
||||||
# mail to first email address in key with signed message:
|
|
||||||
#Subj: Your GPG key has been signed
|
|
||||||
#
|
|
||||||
#Hello! Thank you for participating in a keysigning party and exchanging keys.
|
|
||||||
#
|
|
||||||
#I have signed your key (KEYID) with trust level "TRUSTLEVEL" because:
|
|
||||||
#
|
|
||||||
#* You have presented sufficient proof of identity
|
|
||||||
#
|
|
||||||
#The signatures have been pushed to KEYSERVERS.
|
|
||||||
#
|
|
||||||
#I have taken the liberty of attaching my public key in the event you've not signed it yet and were unable to find it. Please feel free to push to pgp.mit.edu or hkps.pool.sks-keyservers.net.
|
|
||||||
#
|
|
||||||
#As a reminder, my key ID, Keybase.io username, and verification/proof of identity can all be found at:
|
|
||||||
#
|
|
||||||
#https://devblog.square-r00t.net/about/my-gpg-public-key-verification-of-identity
|
|
||||||
#
|
|
||||||
#Thanks again!
|
|
||||||
|
|
||||||
def getKeys(args):
|
|
||||||
# Get our concept
|
|
||||||
os.environ['GNUPGHOME'] = args['gpgdir']
|
|
||||||
gpg = gpgme.Context()
|
|
||||||
keys = {}
|
|
||||||
allkeys = []
|
|
||||||
# Do we have the key already? If not, fetch.
|
|
||||||
for k in args['rcpts'].keys():
|
|
||||||
if args['rcpts'][k]['type'] == 'fpr':
|
|
||||||
allkeys.append(k)
|
|
||||||
if args['rcpts'][k]['type'] == 'email':
|
|
||||||
# We need to actually do a lookup on the email address.
|
|
||||||
with open(os.devnull, 'w') as f:
|
|
||||||
# TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms?
|
|
||||||
keyout = subprocess.run(['gpg2',
|
|
||||||
'--search-keys',
|
|
||||||
'--with-colons',
|
|
||||||
'--batch',
|
|
||||||
k],
|
|
||||||
stdout = subprocess.PIPE,
|
|
||||||
stderr = f)
|
|
||||||
keyout = keyout.stdout.decode('utf-8').splitlines()
|
|
||||||
for line in keyout:
|
|
||||||
if line.startswith('pub:'):
|
|
||||||
key = line.split(':')[1]
|
|
||||||
keys[key] = {}
|
|
||||||
keys[key]['uids'] = {}
|
|
||||||
keys[key]['time'] = int(line.split(':')[4])
|
|
||||||
elif line.startswith('uid:'):
|
|
||||||
uid = re.split('<(.*)>', urllib.parse.unquote(line.split(':')[1].strip()))
|
|
||||||
uid.remove('')
|
|
||||||
uid = [u.strip() for u in uid]
|
|
||||||
keys[key]['uids'][uid[1]] = {}
|
|
||||||
keys[key]['uids'][uid[1]]['comment'] = uid[0]
|
|
||||||
keys[key]['uids'][uid[1]]['time'] = int(line.split(':')[2])
|
|
||||||
if len(keys) > 1: # Print the keys and prompt for a selection.
|
|
||||||
print('\nWe found the following keys for <{0}>...\n\nKEY ID:'.format(k))
|
|
||||||
for k in keys:
|
|
||||||
print('{0}\n{1:6}(Generated at {2}) UIDs:'.format(k, '', datetime.datetime.utcfromtimestamp(keys[k]['time'])))
|
|
||||||
for email in keys[k]['uids']:
|
|
||||||
print('{0:42}(Generated {3}) <{2}> {1}'.format('',
|
|
||||||
keys[k]['uids'][email]['comment'],
|
|
||||||
email,
|
|
||||||
datetime.datetime.utcfromtimestamp(
|
|
||||||
keys[k]['uids'][email]['time'])))
|
|
||||||
print()
|
|
||||||
while True:
|
|
||||||
key = input('Please enter the (full) appropriate key: ')
|
|
||||||
if key not in keys.keys():
|
|
||||||
print('Please enter a full key ID from the list above or hit ctrl-d to exit.')
|
|
||||||
else:
|
|
||||||
allkeys.append(key)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if not len(keys.keys()) >= 1:
|
|
||||||
print('Could not find {0}!'.format(k))
|
|
||||||
continue
|
|
||||||
key = list(keys.keys())[0]
|
|
||||||
print('\nFound key {0} for <{1}> (Generated at {2}):'.format(key, k, datetime.datetime.utcfromtimestamp(keys[key]['time'])))
|
|
||||||
for email in keys[key]['uids']:
|
|
||||||
print('\t(Generated {2}) {0} <{1}>'.format(keys[key]['uids'][email]['comment'],
|
|
||||||
email,
|
|
||||||
datetime.datetime.utcfromtimestamp(keys[key]['uids'][email]['time'])))
|
|
||||||
allkeys.append(key)
|
|
||||||
print()
|
|
||||||
## And now we can (FINALLY) fetch the key(s).
|
|
||||||
# TODO: replace with gpg.keylist_mode(gpgme.KEYLIST_MODE_EXTERN) and internal mechanisms?
|
|
||||||
recvcmd = ['gpg2', '--recv-keys', '--batch', '--yes'] # We'll add the keys onto the end of this next.
|
|
||||||
recvcmd.extend(allkeys)
|
|
||||||
with open(os.devnull, 'w') as f:
|
|
||||||
subprocess.run(recvcmd, stdout = f, stderr = f) # We hide stderr because gpg, for some unknown reason, spits non-errors to stderr.
|
|
||||||
return(allkeys)
|
|
||||||
|
|
||||||
def sigKeys(keyids):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def modifyDirmngr(op, args):
|
|
||||||
if not args['keyservers']:
|
|
||||||
return()
|
|
||||||
pid = str(os.getpid())
|
|
||||||
activecfg = os.path.join(args['gpgdir'], 'dirmngr.conf')
|
|
||||||
bakcfg = '{0}.{1}'.format(activecfg, pid)
|
|
||||||
if op in ('new', 'start'):
|
|
||||||
if os.path.lexists(activecfg):
|
|
||||||
shutil.copy2(activecfg, bakcfg)
|
|
||||||
with open(bakcfg, 'r') as read, open(activecfg, 'w') as write:
|
|
||||||
for line in read:
|
|
||||||
if not line.startswith('keyserver '):
|
|
||||||
write.write(line)
|
|
||||||
with open(activecfg, 'a') as f:
|
|
||||||
for s in args['keyservers']:
|
|
||||||
uri = '{0}://{1}:{2}'.format(s['proto'], s['server'], s['port'][0])
|
|
||||||
f.write('keyserver {0}\n'.format(uri))
|
|
||||||
if op in ('old', 'stop'):
|
|
||||||
if os.path.lexists(bakcfg):
|
|
||||||
with open(bakcfg, 'r') as read, open(activecfg, 'w') as write:
|
|
||||||
for line in read:
|
|
||||||
write.write(line)
|
|
||||||
os.remove(bakcfg)
|
|
||||||
else:
|
|
||||||
os.remove(activecfg)
|
|
||||||
subprocess.run(['gpgconf',
|
|
||||||
'--reload',
|
|
||||||
'dirmngr'])
|
|
||||||
return()
|
|
||||||
|
|
||||||
def serverParser(uri):
|
|
||||||
# https://en.wikipedia.org/wiki/Key_server_(cryptographic)#Keyserver_examples
|
|
||||||
# We need to make a mapping of the default ports.
|
|
||||||
server = {}
|
|
||||||
protos = {'hkp': [11371, ['tcp', 'udp']],
|
|
||||||
'hkps': [443, ['tcp']], # Yes, same as https
|
|
||||||
'http': [80, ['tcp']],
|
|
||||||
'https': [443, ['tcp']], # SSL/TLS
|
|
||||||
'ldap': [389, ['tcp', 'udp']], # includes TLS negotiation since it runs on the same port
|
|
||||||
'ldaps': [636, ['tcp', 'udp']]} # SSL
|
|
||||||
urlobj = urllib.parse.urlparse(uri)
|
|
||||||
server['proto'] = urlobj.scheme
|
|
||||||
lazy = False
|
|
||||||
if not server['proto']:
|
|
||||||
server['proto'] = 'hkp' # Default
|
|
||||||
server['server'] = urlobj.hostname
|
|
||||||
if not server['server']:
|
|
||||||
server['server'] = re.sub('^([A-Za-z]://)?(.+[^:][^0-9])(:[0-9]+)?$', '\g<2>', uri)
|
|
||||||
lazy = True
|
|
||||||
server['port'] = urlobj.port
|
|
||||||
if not server['port']:
|
|
||||||
if lazy:
|
|
||||||
p = re.sub('.*:([0-9]+)$', '\g<1>', uri)
|
|
||||||
server['port'] = protos[server['proto']] # Default
|
|
||||||
return(server)
|
|
||||||
|
|
||||||
def parseArgs():
|
|
||||||
def getDefGPGDir():
|
|
||||||
try:
|
|
||||||
gpgdir = os.environ['GNUPGHOME']
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
homedir = os.environ['HOME']
|
|
||||||
gpgdchk = os.path.join(homedir, '.gnupg')
|
|
||||||
except KeyError:
|
|
||||||
# There is no reason that this should ever get this far, but... edge cases be crazy.
|
|
||||||
gpgdchk = os.path.join(os.path.expanduser('~'), '.gnupg')
|
|
||||||
if os.path.isdir(gpgdchk):
|
|
||||||
gpgdir = gpgdchk
|
|
||||||
else:
|
|
||||||
gpgdir = None
|
|
||||||
return(gpgdir)
|
|
||||||
def getDefKey(defgpgdir):
|
|
||||||
os.environ['GNUPGHOME'] = defgpgdir
|
|
||||||
if not defgpgdir:
|
|
||||||
return(None)
|
|
||||||
defkey = None
|
|
||||||
gpg = gpgme.Context()
|
|
||||||
for k in gpg.keylist(None, True): # params are query and secret keyring, respectively
|
|
||||||
if k.can_sign and True not in (k.revoked, k.expired, k.disabled):
|
|
||||||
defkey = k.subkeys[0].fpr
|
|
||||||
break # We'll just use the first primary key we find that's valid as the default.
|
|
||||||
return(defkey)
|
|
||||||
def getDefKeyservers(defgpgdir):
|
|
||||||
srvlst = [None]
|
|
||||||
# We don't need these since we use the gpg agent. Requires GPG 2.1 and above, probably.
|
|
||||||
#if os.path.isfile(os.path.join(defgpgdir, 'dirmngr.conf')):
|
|
||||||
# pass
|
|
||||||
dirmgr_out = subprocess.run(['gpg-connect-agent', '--dirmngr', 'keyserver', '/bye'], stdout = subprocess.PIPE)
|
|
||||||
for l in dirmgr_out.stdout.decode('utf-8').splitlines():
|
|
||||||
#if len(l) == 3 and l.lower().startswith('s keyserver'): # It's a keyserver line
|
|
||||||
if l.lower().startswith('s keyserver'): # It's a keyserver line
|
|
||||||
s = l.split()[2]
|
|
||||||
if len(srvlst) == 1 and srvlst[0] == None:
|
|
||||||
srvlst = [s]
|
|
||||||
else:
|
|
||||||
srvlst.append(s)
|
|
||||||
return(','.join(srvlst))
|
|
||||||
defgpgdir = getDefGPGDir()
|
|
||||||
defkey = getDefKey(defgpgdir)
|
|
||||||
defkeyservers = getDefKeyservers(defgpgdir)
|
|
||||||
args = argparse.ArgumentParser(description = 'Keysigning Assistance and Notifying Tool (KANT)',
|
|
||||||
epilog = 'brent s. || 2017 || https://square-r00t.net',
|
|
||||||
formatter_class = argparse.RawTextHelpFormatter)
|
|
||||||
args.add_argument('-k',
|
|
||||||
'--keys',
|
|
||||||
dest = 'keys',
|
|
||||||
required = True,
|
|
||||||
help = 'A single or comma-separated list of keys to sign,\ntrust, and notify. Can also be an email address.')
|
|
||||||
args.add_argument('-K',
|
|
||||||
'--sigkey',
|
|
||||||
dest = 'sigkey',
|
|
||||||
default = defkey,
|
|
||||||
help = 'The key to use when signing other keys.\nDefault is \033[1m{0}\033[0m.'.format(defkey))
|
|
||||||
args.add_argument('-b',
|
|
||||||
'--batch',
|
|
||||||
dest = 'batchfile',
|
|
||||||
default = None,
|
|
||||||
metavar = '/path/to/batchfile',
|
|
||||||
help = 'If specified, a CSV file to use as a batch run\nin the format of (one per line):\n' +
|
|
||||||
'\n\033[1mKEY_FINGERPRINT_OR_EMAIL_ADDRESS,TRUSTLEVEL,PUSH_TO_KEYSERVER\033[0m\n' +
|
|
||||||
'\n\033[1mTRUSTLEVEL\033[0m can be numeric or string:' +
|
|
||||||
'\n\n\t\033[1m0 = Unknown\n\t1 = Untrusted\n\t2 = Marginal\n\t3 = Full\n\t4 = Ultimate\033[0m\n' +
|
|
||||||
'\n\033[1mPUSH_TO_KEYSERVER\033[0m can be \033[1m1/True\033[0m or \033[1m0/False\033[0m. If marked as False,\n' +
|
|
||||||
'the signature will be made local/non-exportable.')
|
|
||||||
|
|
||||||
args.add_argument('-d',
|
|
||||||
'--gpgdir',
|
|
||||||
dest = 'gpgdir',
|
|
||||||
default = defgpgdir,
|
|
||||||
help = 'The GnuPG configuration directory to use (containing\n' +
|
|
||||||
'your keys, etc.); default is \033[1m{0}\033[0m.'.format(defgpgdir))
|
|
||||||
args.add_argument('-s',
|
|
||||||
'--keyservers',
|
|
||||||
dest = 'keyservers',
|
|
||||||
default = defkeyservers,
|
|
||||||
help = 'The comma-separated keyserver(s) to push to. If "None", don\'t\n' +
|
|
||||||
'push signatures (local/non-exportable signatures will be made).\n'
|
|
||||||
'Default keyserver list is: \n\n\033[1m{0}\033[0m\n\n'.format(re.sub(',', '\n', defkeyservers)))
|
|
||||||
args.add_argument('-n',
|
|
||||||
'--netproto',
|
|
||||||
dest = 'netproto',
|
|
||||||
action = 'store',
|
|
||||||
choices = ['4', '6'],
|
|
||||||
default = '4',
|
|
||||||
help = 'Whether to use (IPv)4 or (IPv)6. Default is to use IPv4.')
|
|
||||||
args.add_argument('-t',
|
|
||||||
'--testkeyservers',
|
|
||||||
dest = 'testkeyservers',
|
|
||||||
action = 'store_true',
|
|
||||||
help = 'If specified, initiate a test connection with each\n'
|
|
||||||
'\nkeyserver before anything else. Disabled by default.')
|
|
||||||
return(args)
|
|
||||||
|
|
||||||
def verifyArgs(args):
|
|
||||||
## Some pythonization...
|
|
||||||
# We don't want to only strip the values, we want to remove ALL whitespace.
|
|
||||||
#args['keys'] = [k.strip() for k in args['keys'].split(',')]
|
|
||||||
#args['keyservers'] = [s.strip() for s in args['keyservers'].split(',')]
|
|
||||||
args['keys'] = [re.sub('\s', '', k) for k in args['keys'].split(',')]
|
|
||||||
args['keyservers'] = [re.sub('\s', '', s) for s in args['keyservers'].split(',')]
|
|
||||||
args['keyservers'] = [serverParser(s) for s in args['keyservers']]
|
|
||||||
## Key(s) to sign
|
|
||||||
args['rcpts'] = {}
|
|
||||||
for k in args['keys']:
|
|
||||||
args['rcpts'][k] = {}
|
|
||||||
try:
|
|
||||||
int(k, 16)
|
|
||||||
ktype = 'fpr'
|
|
||||||
except: # If it isn't a valid key ID...
|
|
||||||
if not re.match('^[\w\.\+\-]+\@[\w-]+\.[a-z]{2,3}$', k): # is it an email address?
|
|
||||||
raise ValueError('{0} is not a valid email address'.format(k))
|
|
||||||
else:
|
|
||||||
ktype = 'email'
|
|
||||||
args['rcpts'][k]['type'] = ktype
|
|
||||||
if ktype == 'fpr' and not len(k) == 40: # Security is important. We don't want users getting collisions, so we don't allow shortened key IDs.
|
|
||||||
raise ValueError('{0} is not a full 40-char key ID or key fingerprint'.format(k))
|
|
||||||
del args['keys']
|
|
||||||
## Batch file
|
|
||||||
if args['batchfile']:
|
|
||||||
batchfilepath = os.path.abspath(os.path.expanduser(args['batchfile']))
|
|
||||||
if not os.path.isfile(batchfilepath):
|
|
||||||
raise ValueError('{0} does not exist or is not a regular file.'.format(batchfilepath))
|
|
||||||
else:
|
|
||||||
args['batchfile'] = batchfilepath
|
|
||||||
## Signing key
|
|
||||||
if not args['sigkey']:
|
|
||||||
raise ValueError('A key for signing is required') # We need a key we can sign with.
|
|
||||||
else:
|
|
||||||
if not os.path.lexists(args['gpgdir']):
|
|
||||||
raise FileNotFoundError('{0} does not exist'.format(args['gpgdir']))
|
|
||||||
elif os.path.isfile(args['gpgdir']):
|
|
||||||
raise NotADirectoryError('{0} is not a directory'.format(args['gpgdir']))
|
|
||||||
try:
|
|
||||||
os.environ['GNUPGHOME'] = args['gpgdir']
|
|
||||||
gpg = gpgme.Context()
|
|
||||||
except:
|
|
||||||
raise RuntimeError('Could not use {0} as a GnuPG home'.format(args['gpgdir']))
|
|
||||||
# Now we need to verify that the private key exists...
|
|
||||||
try:
|
|
||||||
sigkey = gpg.get_key(args['sigkey'], True)
|
|
||||||
except GpgmeError:
|
|
||||||
raise ValueError('Cannot use key {0}'.format(args['sigkey']))
|
|
||||||
# And that it is an eligible candidate to use to sign.
|
|
||||||
if not sigkey.can_sign or True in (sigkey.revoked, sigkey.expired, sigkey.disabled):
|
|
||||||
raise ValueError('{0} is not a valid candidate for signing'.format(args['sigkey']))
|
|
||||||
## Keyservers
|
|
||||||
if args['testkeyservers']:
|
|
||||||
for s in args['keyservers']:
|
|
||||||
# Test to make sure the keyserver is accessible.
|
|
||||||
# First we need to construct a way to use python's socket connector
|
|
||||||
# Great. Now we need to just quickly check to make sure it's accessible - if specified.
|
|
||||||
if args['netproto'] == '4':
|
|
||||||
nettype = AF_INET
|
|
||||||
elif args['netproto'] == '6':
|
|
||||||
nettype = AF_INET6
|
|
||||||
for proto in s['port'][1]:
|
|
||||||
if proto == 'udp':
|
|
||||||
netproto = SOCK_DGRAM
|
|
||||||
elif proto == 'tcp':
|
|
||||||
netproto = SOCK_STREAM
|
|
||||||
sock = socket(nettype, netproto)
|
|
||||||
sock.settimeout(10)
|
|
||||||
tests = sock.connect_ex((s['server'], int(s['port'][0])))
|
|
||||||
uristr = '{0}://{1}:{2} ({3})'.format(s['proto'], s['server'], s['port'][0], proto.upper())
|
|
||||||
if not tests == 0:
|
|
||||||
raise RuntimeError('Keyserver {0} is not available'.format(uristr))
|
|
||||||
else:
|
|
||||||
print('Keyserver {0} is accepting connections.'.format(uristr))
|
|
||||||
sock.close()
|
|
||||||
return(args)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
rawargs = parseArgs()
|
|
||||||
args = verifyArgs(vars(rawargs.parse_args()))
|
|
||||||
modifyDirmngr('new', args)
|
|
||||||
fprs = getKeys(args)
|
|
||||||
sigKeys(fprs)
|
|
||||||
modifyDirmngr('old', args)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
2
gpg/kant/.gitignore
vendored
Normal file
2
gpg/kant/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/gpgme.pdf
|
||||||
|
/tests
|
18
gpg/kant/commented.testbatch.kant.csv
Normal file
18
gpg/kant/commented.testbatch.kant.csv
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# NOTE: The python csv module does NOT skip
|
||||||
|
# commented lines!
|
||||||
|
# This is my personal key. Ultimate trust,
|
||||||
|
# push key, careful checking, notify
|
||||||
|
748231EBCBD808A14F5E85D28C004C2F93481F6B,4,1,3,1
|
||||||
|
# This is a testing junk key generated on a completely separate box,
|
||||||
|
# and does not exist on ANY keyservers nor the local keyring.
|
||||||
|
# Never trust, local sig, unknown checking, don't notify
|
||||||
|
A03CACFD7123AF443A3A185298A8A46921C8DDEF,-1,0,0,0
|
||||||
|
# This is jthan's key.
|
||||||
|
# assign full trust, push to keyserver, casual checking, notify
|
||||||
|
EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,full,true,casual,yes
|
||||||
|
# This is paden's key.
|
||||||
|
# assign Marginal trust, push to keyserver, casual checking, notify
|
||||||
|
6FA8AE12AEC90B035EEE444FE70457341A63E830,2,True,Casual,True
|
||||||
|
# This is the email for the Sysadministrivia serverkey.
|
||||||
|
# Assign full trust, push to keyserver, careful checking, don't notify
|
||||||
|
<admin@sysadministrivia.com>, full, yes, careful, false
|
|
15
gpg/kant/docs/README
Normal file
15
gpg/kant/docs/README
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
GENERATING THE MAN PAGE:
|
||||||
|
If you have asciidoctor installed, you can generate the manpage one of two ways.
|
||||||
|
|
||||||
|
The first way:
|
||||||
|
|
||||||
|
asciidoctor -b manpage kant.1.adoc -o- | groff -Tascii -man | gz -c > kant.1.gz
|
||||||
|
|
||||||
|
This will generate a fixed-width man page.
|
||||||
|
|
||||||
|
|
||||||
|
The second way (recommended):
|
||||||
|
|
||||||
|
asciidoctor -b manpage kant.1.adoc -o- | gz -c > kant.1.gz
|
||||||
|
|
||||||
|
This will generate a dynamic-width man page. Most modern versions of man want this version.
|
46
gpg/kant/docs/REF.args.struct.txt
Normal file
46
gpg/kant/docs/REF.args.struct.txt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
The __init__() function of kant.SigSession() takes a single argument: args.
|
||||||
|
|
||||||
|
it should be a dict, structured like this:
|
||||||
|
|
||||||
|
{'batch': False,
|
||||||
|
'checklevel': None,
|
||||||
|
'gpgdir': '/home/bts/.gnupg',
|
||||||
|
'keys': 'EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,admin@sysadministrivia.com',
|
||||||
|
'keyservers': 'hkp://sks.mirror.square-r00t.net:11371,hkps://hkps.pool.sks-keyservers.net:443,http://pgp.mit.edu:80',
|
||||||
|
'local': 'false',
|
||||||
|
'msmtp_profile': None,
|
||||||
|
'notify': True,
|
||||||
|
'sigkey': '748231EBCBD808A14F5E85D28C004C2F93481F6B',
|
||||||
|
'testkeyservers': False,
|
||||||
|
'trustlevel': None}
|
||||||
|
|
||||||
|
The gpgdir, sigkey, and keyservers are set from system defaults in kant.parseArgs() if it's run interactively.
|
||||||
|
This *may* be reworked in the future to provide a mechanism for external calls to kant.SigSession() but for now,
|
||||||
|
it's up to you to provide all the data in the dict in the above format.
|
||||||
|
|
||||||
|
It will then internally verify these items and do various conversions, so that self.args becomes this:
|
||||||
|
(Note that some keys, such as "local", are validated and converted to appropriate values later on
|
||||||
|
e.g. 'false' => False)
|
||||||
|
|
||||||
|
{'batch': False,
|
||||||
|
'checklevel': None,
|
||||||
|
'gpgdir': '/home/bts/.gnupg',
|
||||||
|
'keys': ['EFD9413B17293AFDFE6EA6F1402A088DEDF104CB',
|
||||||
|
'admin@sysadministrivia.com'],
|
||||||
|
'keyservers': [{'port': [11371, ['tcp', 'udp']],
|
||||||
|
'proto': 'hkp',
|
||||||
|
'server': 'sks.mirror.square-r00t.net'},
|
||||||
|
{'port': [443, ['tcp']],
|
||||||
|
'proto': 'hkps',
|
||||||
|
'server': 'hkps.pool.sks-keyservers.net'},
|
||||||
|
{'port': [80, ['tcp']],
|
||||||
|
'proto': 'http',
|
||||||
|
'server': 'pgp.mit.edu'}],
|
||||||
|
'local': 'false',
|
||||||
|
'msmtp_profile': None,
|
||||||
|
'notify': True,
|
||||||
|
'rcpts': {'EFD9413B17293AFDFE6EA6F1402A088DEDF104CB': {'type': 'fpr'},
|
||||||
|
'admin@sysadministrivia.com': {'type': 'email'}},
|
||||||
|
'sigkey': '748231EBCBD808A14F5E85D28C004C2F93481F6B',
|
||||||
|
'testkeyservers': False,
|
||||||
|
'trustlevel': None}
|
33
gpg/kant/docs/REF.funcs.struct.txt
Normal file
33
gpg/kant/docs/REF.funcs.struct.txt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
The following functions are available within the SigSession() class:
|
||||||
|
|
||||||
|
getTpls()
|
||||||
|
Get the user-specified templates if they exist, otherwise set up stock ones.
|
||||||
|
|
||||||
|
modifyDirmngr(op)
|
||||||
|
*op* can be either:
|
||||||
|
new/start/replace - modify dirmngr to use the runtime-specified keyserver(s)
|
||||||
|
old/stop/restore - modify dirmngr back to the keyservers that were defined before modification
|
||||||
|
|
||||||
|
buildKeys()
|
||||||
|
build out the keys dict (see REF.keys.struct.txt).
|
||||||
|
|
||||||
|
getKeys()
|
||||||
|
fetch keys in the keys dict (see REF.keys.struct.txt) from a keyserver if they aren't found in the local keyring.
|
||||||
|
|
||||||
|
trustKeys()
|
||||||
|
set up trusts for the keys in the keys dict (see REF.keys.struct.txt). prompts for each trust not found/specified at runtime.
|
||||||
|
|
||||||
|
sigKeys()
|
||||||
|
sign keys in the keys dict (see REF.keys.struct.txt), either exportable or local depending on runtime specification.
|
||||||
|
|
||||||
|
pushKeys()
|
||||||
|
push keys in the keys dict (see REF.keys.struct.txt) to the keyservers specified at runtime (as long as they weren't specified to be local/non-exportable signatures; then we don't bother).
|
||||||
|
|
||||||
|
sendMails()
|
||||||
|
send emails to each of the recipients specified in the keys dict (see REF.keys.struct.txt).
|
||||||
|
|
||||||
|
serverParser(uri)
|
||||||
|
returns a dict of a keyserver URI broken up into separate components easier for parsing.
|
||||||
|
|
||||||
|
verifyArgs(locargs)
|
||||||
|
does some verifications, classifies certain data, calls serverParser(), etc.
|
127
gpg/kant/docs/REF.keys.struct.txt
Normal file
127
gpg/kant/docs/REF.keys.struct.txt
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
TYPES:
|
||||||
|
d = dict
|
||||||
|
l = list
|
||||||
|
s = string
|
||||||
|
i = int
|
||||||
|
b = binary (True/False)
|
||||||
|
o = object
|
||||||
|
|
||||||
|
- pkey's dict key is the 40-char key ID of the primary key
|
||||||
|
- "==>" indicates the next item is a dict and the current item may contain one or more elements of the same format,
|
||||||
|
"++>" is a list,
|
||||||
|
"-->" is a "flat" item (string, object, int, etc.)
|
||||||
|
-"status" is one of "an UPGRADE", "a DOWNGRADE", or "a NEW TRUST".
|
||||||
|
|
||||||
|
keys(d) ==> (40-char key ID)(s) ==> pkey(d) --> email(s)
|
||||||
|
--> name(s)
|
||||||
|
--> creation (o, datetime)
|
||||||
|
--> key(o, gpg)
|
||||||
|
--> trust(i)
|
||||||
|
--> check(i)
|
||||||
|
--> local(b)
|
||||||
|
--> notify(b)
|
||||||
|
==> subkeys(d) ==> (40-char key ID)(s) --> creation
|
||||||
|
--> change(b)
|
||||||
|
--> sign(b)
|
||||||
|
--> status(s)
|
||||||
|
==> uids(d) ==> email(s) --> name(s)
|
||||||
|
--> comment(s)
|
||||||
|
--> email(s)
|
||||||
|
--> updated(o, datetime)*
|
||||||
|
|
||||||
|
* For many keys, this is unset. In-code, this is represented by having a timestamp of 0, or a
|
||||||
|
datetime object matching UNIX epoch. This is converted to a string, "Never/unknown".
|
||||||
|
|
||||||
|
for email templates, they are looped over for each key dict as "key".
|
||||||
|
so for example, instead of specifying "keys['748231EBCBD808A14F5E85D28C004C2F93481F6B']['pkey']['name']",
|
||||||
|
you instead should specify "key['pkey']['name']". To get the name of e.g. the second uid,
|
||||||
|
you'd use "key['uids'][(uid email)]['name'].
|
||||||
|
|
||||||
|
e.g. in the code, it's this:
|
||||||
|
{'748231EBCBD808A14F5E85D28C004C2F93481F6B': {'change': None,
|
||||||
|
'check': 0,
|
||||||
|
'local': False,
|
||||||
|
'notify': True,
|
||||||
|
'pkey': {'creation': '2013-12-10 '
|
||||||
|
'08:35:52',
|
||||||
|
'email': 'brent.saner@gmail.com',
|
||||||
|
'key': '<GPGME object>',
|
||||||
|
'name': 'Brent Timothy '
|
||||||
|
'Saner'},
|
||||||
|
'sign': True,
|
||||||
|
'status': None,
|
||||||
|
'subkeys': {'748231EBCBD808A14F5E85D28C004C2F93481F6B': '2013-12-10 '
|
||||||
|
'08:35:52'},
|
||||||
|
'trust': 2,
|
||||||
|
'uids': {'brent.saner@gmail.com': {'comment': '',
|
||||||
|
'name': 'Brent '
|
||||||
|
'Timothy '
|
||||||
|
'Saner',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'bts@square-r00t.net': {'comment': 'http://www.square-r00t.net',
|
||||||
|
'name': 'Brent '
|
||||||
|
'S.',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'r00t@sysadministrivia.com': {'comment': 'https://sysadministrivia.com',
|
||||||
|
'name': 'r00t^2',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'squarer00t@keybase.io': {'comment': '',
|
||||||
|
'name': 'keybase.io/squarer00t',
|
||||||
|
'updated': 'Never/unknown'}}}}
|
||||||
|
but this is passed to the email template as:
|
||||||
|
{'change': None,
|
||||||
|
'check': 0,
|
||||||
|
'local': False,
|
||||||
|
'notify': True,
|
||||||
|
'pkey': {'creation': '2013-12-10 08:35:52',
|
||||||
|
'email': 'brent.saner@gmail.com',
|
||||||
|
'key': '<GPGME object>',
|
||||||
|
'name': 'Brent Timothy Saner'},
|
||||||
|
'sign': True,
|
||||||
|
'status': None,
|
||||||
|
'subkeys': {'748231EBCBD808A14F5E85D28C004C2F93481F6B': '2013-12-10 08:35:52'},
|
||||||
|
'trust': 2,
|
||||||
|
'uids': {'brent.saner@gmail.com': {'comment': '',
|
||||||
|
'name': 'Brent Timothy Saner',
|
||||||
|
'updated': '1970-01-01 00:00:00'},
|
||||||
|
'bts@square-r00t.net': {'comment': 'http://www.square-r00t.net',
|
||||||
|
'name': 'Brent S.',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'r00t@sysadministrivia.com': {'comment': 'https://sysadministrivia.com',
|
||||||
|
'name': 'r00t^2',
|
||||||
|
'updated': 'Never/unknown'},
|
||||||
|
'squarer00t@keybase.io': {'comment': '',
|
||||||
|
'name': 'keybase.io/squarer00t',
|
||||||
|
'updated': 'Never/unknown'}}}
|
||||||
|
|
||||||
|
(because the emails are iterated through the keys).
|
||||||
|
|
||||||
|
|
||||||
|
the same structure is available via the "mykey" dictionary (e.g. to get the key ID of *your* key,
|
||||||
|
you can use "mykey['subkeys'][0][0]"):
|
||||||
|
|
||||||
|
{'change': False,
|
||||||
|
'check': None,
|
||||||
|
'local': False,
|
||||||
|
'notify': False,
|
||||||
|
'pkey': {'creation': '2017-09-07 20:54:31',
|
||||||
|
'email': 'test@test.com',
|
||||||
|
'key': '<GPGME object>',
|
||||||
|
'name': 'test user'},
|
||||||
|
'sign': False,
|
||||||
|
'status': None,
|
||||||
|
'subkeys': {'1CD9200637EC587D1F8EB94198748C2879CCE88D': '2017-09-07 20:54:31',
|
||||||
|
'2805EC3D90E2229795AFB73FF85BC40E6E17F339': '2017-09-07 20:54:31'},
|
||||||
|
'trust': 'ultimate',
|
||||||
|
'uids': {'test@test.com': {'comment': 'this is a testing junk key. DO NOT '
|
||||||
|
'IMPORT/SIGN/TRUST.',
|
||||||
|
'name': 'test user',
|
||||||
|
'updated': 'Never/unknown'}}}
|
||||||
|
|
||||||
|
|
||||||
|
you also have the following variables/lists/etc. available for templates (via the Jinja2 templating syntax[0]):
|
||||||
|
- "keyservers", a list of keyservers set.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[0] http://jinja.pocoo.org/docs/2.9/templates/
|
257
gpg/kant/docs/kant.1
Normal file
257
gpg/kant/docs/kant.1
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
'\" t
|
||||||
|
.\" Title: kant
|
||||||
|
.\" Author: Brent Saner
|
||||||
|
.\" Generator: Asciidoctor 1.5.6.1
|
||||||
|
.\" Date: 2017-09-21
|
||||||
|
.\" Manual: KANT - Keysigning and Notification Tool
|
||||||
|
.\" Source: KANT
|
||||||
|
.\" Language: English
|
||||||
|
.\"
|
||||||
|
.TH "KANT" "1" "2017-09-21" "KANT" "KANT \- Keysigning and Notification Tool"
|
||||||
|
.ie \n(.g .ds Aq \(aq
|
||||||
|
.el .ds Aq '
|
||||||
|
.ss \n[.ss] 0
|
||||||
|
.nh
|
||||||
|
.ad l
|
||||||
|
.de URL
|
||||||
|
\\$2 \(laURL: \\$1 \(ra\\$3
|
||||||
|
..
|
||||||
|
.if \n[.g] .mso www.tmac
|
||||||
|
.LINKSTYLE blue R < >
|
||||||
|
.SH "NAME"
|
||||||
|
kant \- Sign GnuPG/OpenPGP/PGP keys and notify the key owner(s)
|
||||||
|
.SH "SYNOPSIS"
|
||||||
|
.sp
|
||||||
|
\fBkant\fP [\fIOPTION\fP] \-k/\-\-key \fI<KEY_IDS|BATCHFILE>\fP
|
||||||
|
.SH "OPTIONS"
|
||||||
|
.sp
|
||||||
|
Keysigning (and keysigning parties) can be a lot of fun, and can offer someone with new keys a way into the WoT (Web\-of\-Trust).
|
||||||
|
Unfortunately, they can be intimidating to those new to the experience.
|
||||||
|
This tool offers a simple and easy\-to\-use interface to sign public keys (normal, local\-only, and/or non\-exportable),
|
||||||
|
set owner trust, specify level of checking done, and push the signatures to a keyserver. It even supports batch operation via a CSV file.
|
||||||
|
On successful completion, information about the keys that were signed and the key used to sign are saved to ~/.kant/cache/YYYY.MM.DD_HH.MM.SS.
|
||||||
|
.sp
|
||||||
|
\fB\-h\fP, \fB\-\-help\fP
|
||||||
|
.RS 4
|
||||||
|
Display brief help/usage and exit.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-k\fP \fIKEY_IDS|BATCHFILE\fP, \fB\-\-key\fP \fIKEY_IDS|BATCHFILE\fP
|
||||||
|
.RS 4
|
||||||
|
A single or comma\-separated list of key IDs (see \fBKEY ID FORMAT\fP) to sign, trust, and notify. Can also be an email address.
|
||||||
|
If \fB\-b\fP/\fB\-\-batch\fP is specified, this should instead be a path to the batch file (see \fBBATCHFILE/Format\fP).
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-K\fP \fIKEY_ID\fP, \fB\-\-sigkey\fP \fIKEY_ID\fP
|
||||||
|
.RS 4
|
||||||
|
The key to use when signing other keys (see \fBKEY ID FORMAT\fP). The default key is automatically determined at runtime
|
||||||
|
(it will be displayed in \fB\-h\fP/\fB\-\-help\fP output).
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-t\fP \fITRUSTLEVEL\fP, \fB\-\-trust\fP \fITRUSTLEVEL\fP
|
||||||
|
.RS 4
|
||||||
|
The trust level to automatically apply to all keys (if not specified, KANT will prompt for each key).
|
||||||
|
See \fBBATCHFILE/TRUSTLEVEL\fP for trust level notations.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-c\fP \fICHECKLEVEL\fP, \fB\-\-check\fP \fICHECKLEVEL\fP
|
||||||
|
.RS 4
|
||||||
|
The level of checking that was done to confirm the validity of ownership for all keys being signed. If not specified,
|
||||||
|
the default is for KANT to prompt for each key we sign. See \fBBATCHFILE/CHECKLEVEL\fP for check level notations.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-l\fP \fILOCAL\fP, \fB\-\-local\fP \fILOCAL\fP
|
||||||
|
.RS 4
|
||||||
|
If specified, make the signature(s) local\-only (i.e. non\-exportable, don\(cqt push to a keyserver).
|
||||||
|
See \fBBATCHFILE/LOCAL\fP for more information on local signatures.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-n\fP, \fB\-\-no\-notify\fP
|
||||||
|
.RS 4
|
||||||
|
This requires some explanation. If you have MSMTP[1] installed and configured for the currently active user,
|
||||||
|
then we will send out emails to recipients letting them know we have signed their key. However, if MSMTP is installed and configured
|
||||||
|
but this flag is given, then we will NOT attempt to send emails. See \fBMAIL\fP for more information.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-s\fP \fIKEYSERVER(S)\fP, \fB\-\-keyservers\fP \fIKEYSERVER(S)\fP
|
||||||
|
.RS 4
|
||||||
|
The comma\-separated keyserver(s) to push to. The default keyserver list is automatically generated at runtime.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-m\fP \fIPROFILE\fP, \fB\-\-msmtp\-profile\fP \fIPROFILE\fP
|
||||||
|
.RS 4
|
||||||
|
If specified, use the msmtp profile named \fIPROFILE\fP. If this is not specified, KANT first looks for an msmtp configuration named KANT (case\-sensitive). If it doesn\(cqt find one, it will use the profile specified as the default profile in your msmtp configuration. See \fBMAIL\fP for more information.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-b\fP, \fB\-\-batch\fP
|
||||||
|
.RS 4
|
||||||
|
If specified, operate in batch mode. See \fBBATCHFILE\fP for more information.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-D\fP \fIGPGDIR\fP, \fB\-\-gpgdir\fP \fIGPGDIR\fP
|
||||||
|
.RS 4
|
||||||
|
The GnuPG configuration directory to use (containing your keys, etc.). The default is automatically generated at runtime,
|
||||||
|
but will probably be \fB/home/<yourusername>/.gnupg\fP or similar.
|
||||||
|
.RE
|
||||||
|
.sp
|
||||||
|
\fB\-T\fP, \fB\-\-testkeyservers\fP
|
||||||
|
.RS 4
|
||||||
|
If specified, initiate a basic test connection with each set keyserver before anything else. Disabled by default.
|
||||||
|
.RE
|
||||||
|
.SH "KEY ID FORMAT"
|
||||||
|
.sp
|
||||||
|
Key IDs can be specified in one of two ways. The first (and preferred) way is to use the full 160\-bit (40\-character, hexadecimal) key ID.
|
||||||
|
A little known fact is the fingerprint of a key:
|
||||||
|
.sp
|
||||||
|
\fBDEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF\fP
|
||||||
|
.sp
|
||||||
|
is actually the full key ID of the primary key; i.e.:
|
||||||
|
.sp
|
||||||
|
\fBDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF\fP
|
||||||
|
.sp
|
||||||
|
The second way to specify a key, as far as KANT is concerned, is to use an email address.
|
||||||
|
Do note that if more than one key is found that matches the email address given (and they usually are), you will be prompted to select the specific
|
||||||
|
correct key ID anyways so it\(cqs usually a better idea to have the owner present their full key ID/fingerprint right from the get\-go.
|
||||||
|
.SH "BATCHFILE"
|
||||||
|
.SS "Format"
|
||||||
|
.sp
|
||||||
|
The batch file is a CSV\-formatted (comma\-delimited) file containing keys to sign and other information about them. It keeps the following format:
|
||||||
|
.sp
|
||||||
|
\fBKEY_ID,TRUSTLEVEL,LOCAL,CHECKLEVEL,NOTIFY\fP
|
||||||
|
.sp
|
||||||
|
For more information on each column, reference the appropriate sub\-section below.
|
||||||
|
.SS "KEY_ID"
|
||||||
|
.sp
|
||||||
|
See \fBKEY ID FORMAT\fP.
|
||||||
|
.SS "TRUSTLEVEL"
|
||||||
|
.sp
|
||||||
|
The \fITRUSTLEVEL\fP is specified by the following levels (you can use either the numeric or string representation):
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
\fB\-1 = Never
|
||||||
|
0 = Unknown
|
||||||
|
1 = Untrusted
|
||||||
|
2 = Marginal
|
||||||
|
3 = Full
|
||||||
|
4 = Ultimate\fP
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
It is how much trust to assign to a key, and the signatures that key makes on other keys.[2]
|
||||||
|
.SS "LOCAL"
|
||||||
|
.sp
|
||||||
|
Whether or not to push to a keyserver. It can be either the numeric or string representation of the following:
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
\fB0 = False
|
||||||
|
1 = True\fP
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
If \fB1/True\fP, KANT will sign the key with a local signature (and the signature will not be pushed to a keyserver or be exportable).[3]
|
||||||
|
.SS "CHECKLEVEL"
|
||||||
|
.sp
|
||||||
|
The amount of checking that has been done to confirm that the owner of the key is who they say they are and that the key matches their provided information.
|
||||||
|
It can be either the numeric or string representation of the following:
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
\fB0 = Unknown
|
||||||
|
1 = None
|
||||||
|
2 = Casual
|
||||||
|
3 = Careful\fP
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
It is up to you to determine the classification of the amount of checking you have done, but the following is recommended (it is the policy
|
||||||
|
the author follows):
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
\fBUnknown:\fP The key is unknown and has not been reviewed
|
||||||
|
|
||||||
|
\fBNone:\fP The key has been signed, but no confirmation of the
|
||||||
|
ownership of the key has been performed (typically
|
||||||
|
a local signature)
|
||||||
|
|
||||||
|
\fBCasual:\fP The key has been presented and the owner is either
|
||||||
|
known to the signer or they have provided some form
|
||||||
|
of non\-government\-issued identification or other
|
||||||
|
proof (website, Keybase.io, etc.)
|
||||||
|
|
||||||
|
\fBCareful:\fP The same as \fBCasual\fP requirements but they have
|
||||||
|
provided a government\-issued ID and all information
|
||||||
|
matches
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
It\(cqs important to check each key you sign carefully. Failure to do so may hurt others\(aq trust in your key.[4]
|
||||||
|
.SH "MAIL"
|
||||||
|
.sp
|
||||||
|
The mailing feature of KANT is very handy; it will let you send notifications to the owners of the keys you sign. This is encouraged because: 1.) it\(cqs courteous to let them know where they can fetch the signature you just made on their key, 2.) it\(cqs courteous to let them know if you did/did not push to a keyserver (some people don\(cqt want their keys pushed, and it\(cqs a good idea to respect that wish), and 3.) the mailer also attaches the pubkey for the key you used to sign with, in case your key isn\(cqt on a keyserver, etc.
|
||||||
|
.sp
|
||||||
|
However, in order to do this since many ISPs block outgoing mail, one would typically use something like msmtp (http://msmtp.sourceforge.net/). Note that you don\(cqt even need msmtp to be installed, you just need to have msmtp configuration files set up via either /etc/msmtprc or ~/.msmtprc. KANT will parse these configuration files and use a purely pythonic implementation for sending the emails (see \fBSENDING\fP).
|
||||||
|
.sp
|
||||||
|
It supports templated mail messages as well (see \fBTEMPLATES\fP). It sends a MIME multipart email, in both plaintext and HTML formatting, for mail clients that may only support one or the other. It will also sign the email message using your signing key (see \fB\-K\fP, \fB\-\-sigkey\fP) and attach a binary (.gpg) and ASCII\-armored (.asc) export of your pubkey.
|
||||||
|
.SS "SENDING"
|
||||||
|
.sp
|
||||||
|
KANT first looks for ~/.msmtprc and, if not found, will look for /etc/msmtprc. If neither are found, mail notifications will not be sent and it will be up to you to contact the key owner(s) and let them know you have signed their key(s). If it does find either, it will use the first configuration file it finds and first look for a profile called "KANT" (without quotation marks). If this is not found, it will use whatever profile is specified for as the default profile (e.g. \fBaccount default: someprofilename\fP in the msmtprc).
|
||||||
|
.SS "TEMPLATES"
|
||||||
|
.sp
|
||||||
|
KANT, on first run (even with a \fB\-h\fP/\fB\-\-help\fP execution), will create the default email templates (which can be found as ~/.kant/email.html.j2 and ~/.kant/email.plain.j2). These support templating via Jinja2 (http://jinja.pocoo.org/docs/2.9/templates/), and the following variables/dictionaries/lists are exported for your use:
|
||||||
|
.sp
|
||||||
|
.if n \{\
|
||||||
|
.RS 4
|
||||||
|
.\}
|
||||||
|
.nf
|
||||||
|
* \fBkey\fP \- a dictionary of information about the recipient\(aqs key (see docs/REF.keys.struct.txt)
|
||||||
|
* \fBmykey\fP \- a dictionary of information about your key (see docs/REF.keys.struct.txt)
|
||||||
|
* \fBkeyservers\fP \- a list of keyservers that the key has been pushed to (if an exportable/non\-local signature was made)
|
||||||
|
.fi
|
||||||
|
.if n \{\
|
||||||
|
.RE
|
||||||
|
.\}
|
||||||
|
.sp
|
||||||
|
And of course you can set your own variables inside the template as well (http://jinja.pocoo.org/docs/2.9/templates/#assignments).
|
||||||
|
.SH "SEE ALSO"
|
||||||
|
.sp
|
||||||
|
gpg(1), gpgconf(1), msmtp(1)
|
||||||
|
.SH "RESOURCES"
|
||||||
|
.sp
|
||||||
|
\fBAuthor\(cqs web site:\fP https://square\-r00t.net/
|
||||||
|
.sp
|
||||||
|
\fBAuthor\(cqs GPG information:\fP https://square\-r00t.net/gpg\-info
|
||||||
|
.SH "COPYING"
|
||||||
|
.sp
|
||||||
|
Copyright (C) 2017 Brent Saner.
|
||||||
|
.sp
|
||||||
|
Free use of this software is granted under the terms of the GPLv3 License.
|
||||||
|
.SH "NOTES"
|
||||||
|
1. http://msmtp.sourceforge.net/
|
||||||
|
2. For more information on trust levels and the Web of Trust, see: https://www.gnupg.org/gph/en/manual/x334.html and https://www.gnupg.org/gph/en/manual/x547.html
|
||||||
|
3. For more information on pushing to keyservers and local signatures, see: https://www.gnupg.org/gph/en/manual/r899.html#LSIGN and https://lists.gnupg.org/pipermail/gnupg-users/2007-January/030242.html
|
||||||
|
4. GnuPG documentation refers to this as "validity"; see https://www.gnupg.org/gph/en/manual/x334.html
|
||||||
|
.SH "AUTHOR(S)"
|
||||||
|
.sp
|
||||||
|
\fBBrent Saner\fP
|
||||||
|
.RS 4
|
||||||
|
Author(s).
|
||||||
|
.RE
|
195
gpg/kant/docs/kant.1.adoc
Normal file
195
gpg/kant/docs/kant.1.adoc
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
= kant(1)
|
||||||
|
Brent Saner
|
||||||
|
v1.0.0
|
||||||
|
:doctype: manpage
|
||||||
|
:manmanual: KANT - Keysigning and Notification Tool
|
||||||
|
:mansource: KANT
|
||||||
|
:man-linkstyle: pass:[blue R < >]
|
||||||
|
|
||||||
|
== NAME
|
||||||
|
|
||||||
|
KANT - Sign GnuPG/OpenPGP/PGP keys and notify the key owner(s)
|
||||||
|
|
||||||
|
== SYNOPSIS
|
||||||
|
|
||||||
|
*kant* [_OPTION_] -k/--key _<KEY_IDS|BATCHFILE>_
|
||||||
|
|
||||||
|
== OPTIONS
|
||||||
|
|
||||||
|
Keysigning (and keysigning parties) can be a lot of fun, and can offer someone with new keys a way into the WoT (Web-of-Trust).
|
||||||
|
Unfortunately, they can be intimidating to those new to the experience.
|
||||||
|
This tool offers a simple and easy-to-use interface to sign public keys (normal, local-only, and/or non-exportable),
|
||||||
|
set owner trust, specify level of checking done, and push the signatures to a keyserver. It even supports batch operation via a CSV file.
|
||||||
|
On successful completion, information about the keys that were signed and the key used to sign are saved to ~/.kant/cache/YYYY.MM.DD_HH.MM.SS.
|
||||||
|
|
||||||
|
*-h*, *--help*::
|
||||||
|
Display brief help/usage and exit.
|
||||||
|
|
||||||
|
*-k* _KEY_IDS|BATCHFILE_, *--key* _KEY_IDS|BATCHFILE_::
|
||||||
|
A single or comma-separated list of key IDs (see *KEY ID FORMAT*) to sign, trust, and notify. Can also be an email address.
|
||||||
|
If *-b*/*--batch* is specified, this should instead be a path to the batch file (see *BATCHFILE/Format*).
|
||||||
|
|
||||||
|
*-K* _KEY_ID_, *--sigkey* _KEY_ID_::
|
||||||
|
The key to use when signing other keys (see *KEY ID FORMAT*). The default key is automatically determined at runtime
|
||||||
|
(it will be displayed in *-h*/*--help* output).
|
||||||
|
|
||||||
|
*-t* _TRUSTLEVEL_, *--trust* _TRUSTLEVEL_::
|
||||||
|
The trust level to automatically apply to all keys (if not specified, KANT will prompt for each key).
|
||||||
|
See *BATCHFILE/TRUSTLEVEL* for trust level notations.
|
||||||
|
|
||||||
|
*-c* _CHECKLEVEL_, *--check* _CHECKLEVEL_::
|
||||||
|
The level of checking that was done to confirm the validity of ownership for all keys being signed. If not specified,
|
||||||
|
the default is for KANT to prompt for each key we sign. See *BATCHFILE/CHECKLEVEL* for check level notations.
|
||||||
|
|
||||||
|
*-l* _LOCAL_, *--local* _LOCAL_::
|
||||||
|
If specified, make the signature(s) local-only (i.e. non-exportable, don't push to a keyserver).
|
||||||
|
See *BATCHFILE/LOCAL* for more information on local signatures.
|
||||||
|
|
||||||
|
*-n*, *--no-notify*::
|
||||||
|
This requires some explanation. If you have MSMTPfootnote:[\http://msmtp.sourceforge.net/] installed and configured for the currently active user,
|
||||||
|
then we will send out emails to recipients letting them know we have signed their key. However, if MSMTP is installed and configured
|
||||||
|
but this flag is given, then we will NOT attempt to send emails. See *MAIL* for more information.
|
||||||
|
|
||||||
|
*-s* _KEYSERVER(S)_, *--keyservers* _KEYSERVER(S)_::
|
||||||
|
The comma-separated keyserver(s) to push to. The default keyserver list is automatically generated at runtime.
|
||||||
|
|
||||||
|
*-m* _PROFILE_, *--msmtp-profile* _PROFILE_::
|
||||||
|
If specified, use the msmtp profile named _PROFILE_. If this is not specified, KANT first looks for an msmtp configuration named KANT (case-sensitive). If it doesn't find one, it will use the profile specified as the default profile in your msmtp configuration. See *MAIL* for more information.
|
||||||
|
|
||||||
|
*-b*, *--batch*::
|
||||||
|
If specified, operate in batch mode. See *BATCHFILE* for more information.
|
||||||
|
|
||||||
|
*-D* _GPGDIR_, *--gpgdir* _GPGDIR_::
|
||||||
|
The GnuPG configuration directory to use (containing your keys, etc.). The default is automatically generated at runtime,
|
||||||
|
but will probably be */home/<yourusername>/.gnupg* or similar.
|
||||||
|
|
||||||
|
*-T*, *--testkeyservers*::
|
||||||
|
If specified, initiate a basic test connection with each set keyserver before anything else. Disabled by default.
|
||||||
|
|
||||||
|
== KEY ID FORMAT
|
||||||
|
Key IDs can be specified in one of two ways. The first (and preferred) way is to use the full 160-bit (40-character, hexadecimal) key ID.
|
||||||
|
A little known fact is the fingerprint of a key:
|
||||||
|
|
||||||
|
*DEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF DEAD BEEF*
|
||||||
|
|
||||||
|
is actually the full key ID of the primary key; i.e.:
|
||||||
|
|
||||||
|
*DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF*
|
||||||
|
|
||||||
|
The second way to specify a key, as far as KANT is concerned, is to use an email address.
|
||||||
|
Do note that if more than one key is found that matches the email address given (and they usually are), you will be prompted to select the specific
|
||||||
|
correct key ID anyways so it's usually a better idea to have the owner present their full key ID/fingerprint right from the get-go.
|
||||||
|
|
||||||
|
== BATCHFILE
|
||||||
|
|
||||||
|
=== Format
|
||||||
|
The batch file is a CSV-formatted (comma-delimited) file containing keys to sign and other information about them. It keeps the following format:
|
||||||
|
|
||||||
|
*KEY_ID,TRUSTLEVEL,LOCAL,CHECKLEVEL,NOTIFY*
|
||||||
|
|
||||||
|
For more information on each column, reference the appropriate sub-section below.
|
||||||
|
|
||||||
|
=== KEY_ID
|
||||||
|
See *KEY ID FORMAT*.
|
||||||
|
|
||||||
|
=== TRUSTLEVEL
|
||||||
|
The _TRUSTLEVEL_ is specified by the following levels (you can use either the numeric or string representation):
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
*-1 = Never
|
||||||
|
0 = Unknown
|
||||||
|
1 = Untrusted
|
||||||
|
2 = Marginal
|
||||||
|
3 = Full
|
||||||
|
4 = Ultimate*
|
||||||
|
....
|
||||||
|
|
||||||
|
It is how much trust to assign to a key, and the signatures that key makes on other keys.footnote:[For more information
|
||||||
|
on trust levels and the Web of Trust, see: \https://www.gnupg.org/gph/en/manual/x334.html and \https://www.gnupg.org/gph/en/manual/x547.html]
|
||||||
|
|
||||||
|
=== LOCAL
|
||||||
|
Whether or not to push to a keyserver. It can be either the numeric or string representation of the following:
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
*0 = False
|
||||||
|
1 = True*
|
||||||
|
....
|
||||||
|
|
||||||
|
If *1/True*, KANT will sign the key with a local signature (and the signature will not be pushed to a keyserver or be exportable).footnote:[For
|
||||||
|
more information on pushing to keyservers and local signatures, see: \https://www.gnupg.org/gph/en/manual/r899.html#LSIGN and
|
||||||
|
\https://lists.gnupg.org/pipermail/gnupg-users/2007-January/030242.html]
|
||||||
|
|
||||||
|
=== CHECKLEVEL
|
||||||
|
The amount of checking that has been done to confirm that the owner of the key is who they say they are and that the key matches their provided information.
|
||||||
|
It can be either the numeric or string representation of the following:
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
*0 = Unknown
|
||||||
|
1 = None
|
||||||
|
2 = Casual
|
||||||
|
3 = Careful*
|
||||||
|
....
|
||||||
|
|
||||||
|
It is up to you to determine the classification of the amount of checking you have done, but the following is recommended (it is the policy
|
||||||
|
the author follows):
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
*Unknown:* The key is unknown and has not been reviewed
|
||||||
|
|
||||||
|
*None:* The key has been signed, but no confirmation of the
|
||||||
|
ownership of the key has been performed (typically
|
||||||
|
a local signature)
|
||||||
|
|
||||||
|
*Casual:* The key has been presented and the owner is either
|
||||||
|
known to the signer or they have provided some form
|
||||||
|
of non-government-issued identification or other
|
||||||
|
proof (website, Keybase.io, etc.)
|
||||||
|
|
||||||
|
*Careful:* The same as *Casual* requirements but they have
|
||||||
|
provided a government-issued ID and all information
|
||||||
|
matches
|
||||||
|
....
|
||||||
|
|
||||||
|
It's important to check each key you sign carefully. Failure to do so may hurt others' trust in your key.footnote:[GnuPG documentation refers
|
||||||
|
to this as "validity"; see \https://www.gnupg.org/gph/en/manual/x334.html]
|
||||||
|
|
||||||
|
== MAIL
|
||||||
|
The mailing feature of KANT is very handy; it will let you send notifications to the owners of the keys you sign. This is encouraged because: 1.) it's courteous to let them know where they can fetch the signature you just made on their key, 2.) it's courteous to let them know if you did/did not push to a keyserver (some people don't want their keys pushed, and it's a good idea to respect that wish), and 3.) the mailer also attaches the pubkey for the key you used to sign with, in case your key isn't on a keyserver, etc.
|
||||||
|
|
||||||
|
However, in order to do this since many ISPs block outgoing mail, one would typically use something like msmtp (\http://msmtp.sourceforge.net/). Note that you don't even need msmtp to be installed, you just need to have msmtp configuration files set up via either /etc/msmtprc or ~/.msmtprc. KANT will parse these configuration files and use a purely pythonic implementation for sending the emails (see *SENDING*).
|
||||||
|
|
||||||
|
It supports templated mail messages as well (see *TEMPLATES*). It sends a MIME multipart email, in both plaintext and HTML formatting, for mail clients that may only support one or the other. It will also sign the email message using your signing key (see *-K*, *--sigkey*) and attach a binary (.gpg) and ASCII-armored (.asc) export of your pubkey.
|
||||||
|
|
||||||
|
=== SENDING
|
||||||
|
KANT first looks for ~/.msmtprc and, if not found, will look for /etc/msmtprc. If neither are found, mail notifications will not be sent and it will be up to you to contact the key owner(s) and let them know you have signed their key(s). If it does find either, it will use the first configuration file it finds and first look for a profile called "KANT" (without quotation marks). If this is not found, it will use whatever profile is specified for as the default profile (e.g. *account default: someprofilename* in the msmtprc).
|
||||||
|
|
||||||
|
=== TEMPLATES
|
||||||
|
KANT, on first run (even with a *-h*/*--help* execution), will create the default email templates (which can be found as ~/.kant/email.html.j2 and ~/.kant/email.plain.j2). These support templating via Jinja2 (\http://jinja.pocoo.org/docs/2.9/templates/), and the following variables/dictionaries/lists are exported for your use:
|
||||||
|
|
||||||
|
[subs=+quotes]
|
||||||
|
....
|
||||||
|
* *key* - a dictionary of information about the recipient's key (see docs/REF.keys.struct.txt)
|
||||||
|
* *mykey* - a dictionary of information about your key (see docs/REF.keys.struct.txt)
|
||||||
|
* *keyservers* - a list of keyservers that the key has been pushed to (if an exportable/non-local signature was made)
|
||||||
|
....
|
||||||
|
|
||||||
|
And of course you can set your own variables inside the template as well (\http://jinja.pocoo.org/docs/2.9/templates/#assignments).
|
||||||
|
|
||||||
|
== SEE ALSO
|
||||||
|
gpg(1), gpgconf(1), msmtp(1)
|
||||||
|
|
||||||
|
== RESOURCES
|
||||||
|
|
||||||
|
*Author's web site:* \https://square-r00t.net/
|
||||||
|
|
||||||
|
*Author's GPG information:* \https://square-r00t.net/gpg-info
|
||||||
|
|
||||||
|
== COPYING
|
||||||
|
|
||||||
|
Copyright \(C) 2017 {author}.
|
||||||
|
|
||||||
|
Free use of this software is granted under the terms of the GPLv3 License.
|
961
gpg/kant/kant.py
Executable file
961
gpg/kant/kant.py
Executable file
@ -0,0 +1,961 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import csv
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import lzma
|
||||||
|
import operator
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import smtplib
|
||||||
|
import subprocess
|
||||||
|
from email.message import Message
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from functools import reduce
|
||||||
|
from io import BytesIO
|
||||||
|
from socket import *
|
||||||
|
import urllib.parse
|
||||||
|
import jinja2 # non-stdlib; Arch package is python-jinja2
|
||||||
|
import gpg # non-stdlib; Arch package is "python-gpgme" - see:
|
||||||
|
import gpg.constants # https://git.archlinux.org/svntogit/packages.git/tree/trunk/PKGBUILD?h=packages/gpgme and
|
||||||
|
import gpg.errors # https://gnupg.org/ftp/gcrypt/gpgme/ (incl. python bindings in build)
|
||||||
|
import pprint # development debug
|
||||||
|
|
||||||
|
|
||||||
|
class SigSession(object): # see docs/REFS.funcs.struct.txt
|
||||||
|
def __init__(self, args):
|
||||||
|
# These are the "stock" templates for emails. It's a PITA, but to save some space since we store them
|
||||||
|
# inline in here, they're XZ'd and base64'd.
|
||||||
|
self.email_tpl = {}
|
||||||
|
self.email_tpl['plain'] = ('/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4ATxAnZdACQZSZhvFgKNdKNXbSf05z0ZPvTvmdQ0mJQg' +
|
||||||
|
'atgzhPVeLKxz22bhxedC813X5I8Gn2g9q9Do2jPPgXOzysImWXoraY4mhz0BAo2Zx1u6AiQQLdN9' +
|
||||||
|
'/jwrDrUEtb8M/QzmRd+8JrYN8s8vhViJZARMNHYnPeQK5GYEoGZEQ8l2ULmpTjAn9edSnrMmNSb2' +
|
||||||
|
'EC86CuyhaWDPsQeIamWW1t+MWmgsggE3xKYADKXHMQyXvhv/TAn987dEbzmrkpg8PCjxWt1wKRAr' +
|
||||||
|
'siDpCGvXLiBwnDtN1D7ocwbZVKty2GELbYt0f0CT7n5Pyu9n0P7QMnErM38kLR1nReopQp41+CsG' +
|
||||||
|
'orb8EpGGVdFa7sSWSANQtGTjx/1JHecpkTN8xX4kAjMWKYujWlZi/HzN7y/W5GDJM3ycVEUTsDRV' +
|
||||||
|
'6AusncRBFbo4/+K6cn5WCrhqd5jY2vDJR6KcO0O3usHUMzvOF0S0CZhUbA3Mil5DmPwFrdFrESby' +
|
||||||
|
'O1xH3uvgHpA5X91qkpEajokOOkY3FZm0oeANh9AMoMfDFTuqi41Nq9Myk4VKNEfzioChn9IfFxX0' +
|
||||||
|
'Luw6OyXtWJdpe3BvO7pWazLhvdIY4poh9brvJ25cG1kDMOlmC3NEb+POeqQ5aUr4XaRqFstk3grb' +
|
||||||
|
'8EjiGBzg18uHsbhjyReXnZprJjwzWUdwpV6j+2JFI13UEp16oTyTwyhHdpAmAg+lQJQxtcMpnUeX' +
|
||||||
|
'/xBkQGs+rqe0e/i8ZQ80XsLAoScxUL+45v9vANYV+lCWRnm/2GZOtCFs1Cb4t9hOeV0P1cwxw7fG' +
|
||||||
|
'b1A921JUkHbASFiv2EFsgf0lkvnMgz2slNXKcLuwB6X0CAAAALypR4JWDUR6AAGSBfIJAABGCaV4' +
|
||||||
|
'scRn+wIAAAAABFla')
|
||||||
|
self.email_tpl['html'] = ('/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4AXfAtVdAB4aCobvStaHNqdVBn1LjZcL+G+98rmZ7eGZ' +
|
||||||
|
'Wqx+3LjENIv37L1aGPICZDRBrsBugzVSasRBHkdHMrWW7SsPfRzw6btQfASTHLr48auPJlJgXTnb' +
|
||||||
|
'vgDd2ELrs6p5m5Wip3qD4NeNuwj4QMcxszWF1vLa1oZiNAmCSunIF8bNTw+lmI50h2M6bXfx80Og' +
|
||||||
|
'T2HGcuTp07Mp+XLyZQJ5lbQyu5BRhwyKpu14sq9qrVkxmYt8AAxgUyhvRkooHSuug4O8ArMFXqqX' +
|
||||||
|
'usX9P3zERAsi/TqWIFaG0xoBdrWf/zpGtsVQ+5TtCGOfUHGfIBaNy9Q+FOvfLJFYEzxac992Fkd0' +
|
||||||
|
'as4RsN31FaySbBmZ8eB3zGbpjS7QH7CA70QYkRcYXcjWE9xHD3Wzxa3DFE0ihKAyVwakxvjgYa2B' +
|
||||||
|
'7G6uYO606c+a6vHfPhgvY7Eph+I7ip0btfBbcKZ+XBSd0DtCd7ZvI7vlGJdW2/OBXHfNmCndMP1W' +
|
||||||
|
'Ujd0ASQAQBbJr4rIxYygckSPWti4nBe9JpKTVWqdWRXWjeYGci1dKIjKs7JfS1PGJR50iuyANBun' +
|
||||||
|
'yQ9oIRafb3nreBqtpXZ4LKM5hC697BaeOIcocXyMALf0a06AUmIaRQfO3AZrPxyOPH3EYOKIMrjM' +
|
||||||
|
'EosihPVVyYuKUVOg3wWq5aeIC9zM7Htw4FNh2NB5QDYY6HxIqIVUfHCGz+4GaPBVaf0eie8kHaQR' +
|
||||||
|
'xj+DkAiWQDmN/JRZeTlsy4d3P8XcArOLmxzql/iDzFqtzpD5d91o8I3HU9BJlDJFPs8bC2eCjYs8' +
|
||||||
|
'o3WJET/UIch6YXQOemXa72aWdBVSytfKBMtL7uekd4ARGbFZYyW2x1agkAZGiWt7gwY8RVEoKyZH' +
|
||||||
|
'bbvIvOhQ/j1BDuJFJO3BEgekeLhBPpG7cEewseXjGjoWZWtGr+qFTI//w+oDtdqGtJaGtELL3WYU' +
|
||||||
|
'/tMiQU9AfXkTsODAjvduAAAAAIixVQ23iBDFAAHxBeALAADIP1EPscRn+wIAAAAABFla')
|
||||||
|
# Set up a dict of some constants and mappings
|
||||||
|
self.maps = {}
|
||||||
|
# Keylist modes
|
||||||
|
self.maps['keylist'] = {'local': gpg.constants.KEYLIST_MODE_LOCAL, # local keyring
|
||||||
|
'remote': gpg.constants.KEYLIST_MODE_EXTERN, # keyserver
|
||||||
|
# both - this is SUPPOSED to work, but doesn't seem to... it's unreliable at best?
|
||||||
|
'both': gpg.constants.KEYLIST_MODE_LOCAL|gpg.constants.KEYLIST_MODE_EXTERN}
|
||||||
|
# Validity/trust levels
|
||||||
|
self.maps['trust'] = {-1: ['never', gpg.constants.VALIDITY_NEVER], # this is... probably? not ideal, but. Never trust the key.
|
||||||
|
0: ['unknown', gpg.constants.VALIDITY_UNKNOWN], # The key's trust is unknown - typically because it hasn't been set yet.
|
||||||
|
1: ['untrusted', gpg.constants.VALIDITY_UNDEFINED], # The key is explicitly set to a blank trust
|
||||||
|
2: ['marginal', gpg.constants.VALIDITY_MARGINAL], # Trust a little.
|
||||||
|
3: ['full', gpg.constants.VALIDITY_FULL], # This is going to be the default for verified key ownership.
|
||||||
|
4: ['ultimate', gpg.constants.VALIDITY_ULTIMATE]} # This should probably only be reserved for keys you directly control.
|
||||||
|
# Validity/trust reverse mappings - see self.maps['trust'] for the meanings of these
|
||||||
|
# Used for fetching display/feedback
|
||||||
|
self.maps['rtrust'] = {gpg.constants.VALIDITY_NEVER: 'Never',
|
||||||
|
gpg.constants.VALIDITY_UNKNOWN: 'Unknown',
|
||||||
|
gpg.constants.VALIDITY_UNDEFINED: 'Untrusted',
|
||||||
|
gpg.constants.VALIDITY_MARGINAL: 'Marginal',
|
||||||
|
gpg.constants.VALIDITY_FULL: 'Full',
|
||||||
|
gpg.constants.VALIDITY_ULTIMATE: 'Ultimate'}
|
||||||
|
# Local signature and other binary (True/False) mappings
|
||||||
|
self.maps['binmap'] = {0: ['no', False],
|
||||||
|
1: ['yes', True]}
|
||||||
|
# Level of care taken when checking key ownership/valid identity
|
||||||
|
self.maps['check'] = {0: ['unknown', 0],
|
||||||
|
1: ['none', 1],
|
||||||
|
2: ['casual', 2],
|
||||||
|
3: ['careful', 3]}
|
||||||
|
# Default protocol/port mappings for keyservers
|
||||||
|
self.maps['proto'] = {'hkp': [11371, ['tcp', 'udp']], # Standard HKP protocol
|
||||||
|
'hkps': [443, ['tcp']], # Yes, same as https
|
||||||
|
'http': [80, ['tcp']], # HTTP (plaintext)
|
||||||
|
'https': [443, ['tcp']], # SSL/TLS
|
||||||
|
'ldap': [389, ['tcp', 'udp']], # Includes TLS negotiation since it runs on the same port
|
||||||
|
'ldaps': [636, ['tcp', 'udp']]} # SSL
|
||||||
|
self.maps['hashalgos'] = {gpg.constants.MD_MD5: 'md5',
|
||||||
|
gpg.constants.MD_SHA1: 'sha1',
|
||||||
|
gpg.constants.MD_RMD160: 'ripemd160',
|
||||||
|
gpg.constants.MD_MD2: 'md2',
|
||||||
|
gpg.constants.MD_TIGER: 'tiger192',
|
||||||
|
gpg.constants.MD_HAVAL: 'haval',
|
||||||
|
gpg.constants.MD_SHA256: 'sha256',
|
||||||
|
gpg.constants.MD_SHA384: 'sha384',
|
||||||
|
gpg.constants.MD_SHA512: 'sha512',
|
||||||
|
gpg.constants.MD_SHA224: 'sha224',
|
||||||
|
gpg.constants.MD_MD4: 'md4',
|
||||||
|
gpg.constants.MD_CRC32: 'crc32',
|
||||||
|
gpg.constants.MD_CRC32_RFC1510: 'crc32rfc1510',
|
||||||
|
gpg.constants.MD_CRC24_RFC2440: 'crc24rfc2440'}
|
||||||
|
# Now that all the static data's set up, we can continue.
|
||||||
|
self.args = self.verifyArgs(args) # Make the args accessible to all functions in the class - see docs/REF.args.struct.txt
|
||||||
|
# Get the GPGME context
|
||||||
|
try:
|
||||||
|
os.environ['GNUPGHOME'] = self.args['gpgdir']
|
||||||
|
self.ctx = gpg.Context()
|
||||||
|
except:
|
||||||
|
raise RuntimeError('Could not use {0} as a GnuPG home'.format(self.args['gpgdir']))
|
||||||
|
self.cfgdir = os.path.join(os.environ['HOME'], '.kant')
|
||||||
|
if not os.path.isdir(self.cfgdir):
|
||||||
|
print('No KANT configuration directory found; creating one at {0}...'.format(self.cfgdir))
|
||||||
|
os.makedirs(self.cfgdir, exist_ok = True)
|
||||||
|
self.keys = {} # See docs/REF.keys.struct.txt
|
||||||
|
self.mykey = {} # ""
|
||||||
|
self.tpls = {} # Email templates will go here
|
||||||
|
self.getTpls() # Build out self.tpls
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
def getEditPrompt(self, key, cmd): # "key" should be the FPR of the primary key
|
||||||
|
# This mapping defines the default "answers" to the gpgme key editing.
|
||||||
|
# https://www.apt-browse.org/browse/debian/wheezy/main/amd64/python-pyme/1:0.8.1-2/file/usr/share/doc/python-pyme/examples/t-edit.py
|
||||||
|
# https://searchcode.com/codesearch/view/20535820/
|
||||||
|
# https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS
|
||||||
|
# You can get the prompt identifiers and status indicators without grokking the source
|
||||||
|
# by first interactively performing the type of edit(s) you want to do with this command:
|
||||||
|
# gpg --status-fd 2 --command-fd 2 --edit-key <KEY_ID>
|
||||||
|
if key['trust'] >= gpg.constants.VALIDITY_FULL: # For tsigning, it only prompts for two trust levels:
|
||||||
|
_loctrust = 2 # "I trust fully"
|
||||||
|
else:
|
||||||
|
_loctrust = 1 # "I trust marginally"
|
||||||
|
# TODO: make the trust depth configurable. 1 is probably the safest, but we try to guess here.
|
||||||
|
# "Full" trust is a pretty big thing.
|
||||||
|
if key['trust'] >= gpg.constants.VALIDITY_FULL:
|
||||||
|
_locdepth = 2 # Allow +1 level of trust extension
|
||||||
|
else:
|
||||||
|
_locdepth = 1 # Only trust this key
|
||||||
|
_map = {'cmds': ['trust', 'fpr', 'sign', 'tsign', 'lsign', 'nrsign', 'grip', 'list', # Valid commands
|
||||||
|
'uid', 'key', 'check', 'deluid', 'delkey', 'delsig', 'pref', 'showpref',
|
||||||
|
'revsig', 'enable', 'disable', 'showphoto', 'clean', 'minimize', 'save',
|
||||||
|
'quit'],
|
||||||
|
'prompts': {'edit_ownertrust': {'value': str(key['trust']), # Pulled at time of call
|
||||||
|
'set_ultimate': {'okay': 'yes'}}, # If confirming ultimate trust, we auto-answer yes
|
||||||
|
'untrusted_key': {'override': 'yes'}, # We don't care if it's untrusted
|
||||||
|
'pklist': {'user_id': {'enter': key['pkey']['email']}}, # Prompt for a user ID - can we change this to key ID?
|
||||||
|
'sign_uid': {'class': str(key['check']), # The certification/"check" level
|
||||||
|
'okay': 'yes'}, # Are you sure that you want to sign this key with your key..."
|
||||||
|
'trustsig_prompt': {'trust_value': str(_loctrust), # This requires some processing; see above
|
||||||
|
'trust_depth': str(_locdepth), # The "depth" of the trust signature.
|
||||||
|
'trust_regexp': None}, # We can "Restrict" trust to certain domains, but this isn't really necessary.
|
||||||
|
'keyedit': {'prompt': cmd, # Initiate trust editing
|
||||||
|
'save': {'okay': 'yes'}}}} # Save if prompted
|
||||||
|
return(_map)
|
||||||
|
|
||||||
|
def getTpls(self):
|
||||||
|
for t in ('plain', 'html'):
|
||||||
|
_tpl_file = os.path.join(self.cfgdir, 'email.{0}.j2'.format(t))
|
||||||
|
if os.path.isfile(_tpl_file):
|
||||||
|
with open(_tpl_file, 'r') as f:
|
||||||
|
self.tpls[t] = f.read()
|
||||||
|
else:
|
||||||
|
self.tpls[t] = lzma.decompress(base64.b64decode(email_tpl[t]),
|
||||||
|
format = lzma.FORMAT_XZ,
|
||||||
|
memlimit = None,
|
||||||
|
filters = None).decode('utf-8')
|
||||||
|
with open(_tpl_file, 'w') as f:
|
||||||
|
f.write('{0}'.format(self.tpls[t]))
|
||||||
|
print('Created: {0}'.format(tpl_file))
|
||||||
|
return(self.tpls)
|
||||||
|
|
||||||
|
def modifyDirmngr(self, op):
|
||||||
|
if not self.args['keyservers']:
|
||||||
|
return()
|
||||||
|
_pid = str(os.getpid())
|
||||||
|
_activecfg = os.path.join(self.args['gpgdir'], 'dirmngr.conf')
|
||||||
|
_activegpgconf = os.path.join(self.args['gpgdir'], 'gpg.conf')
|
||||||
|
_bakcfg = '{0}.{1}'.format(_activecfg, _pid)
|
||||||
|
_bakgpgconf = '{0}.{1}'.format(_activegpgconf, _pid)
|
||||||
|
## Modify files
|
||||||
|
if op in ('new', 'start', 'replace'):
|
||||||
|
# Replace the keyservers
|
||||||
|
if os.path.lexists(_activecfg):
|
||||||
|
shutil.copy2(_activecfg, _bakcfg)
|
||||||
|
with open(_bakcfg, 'r') as read, open(_activecfg, 'w') as write:
|
||||||
|
for line in read:
|
||||||
|
if not line.startswith('keyserver '):
|
||||||
|
write.write(line)
|
||||||
|
with open(_activecfg, 'a') as f:
|
||||||
|
for s in self.args['keyservers']:
|
||||||
|
_uri = '{0}://{1}:{2}'.format(s['proto'],
|
||||||
|
s['server'],
|
||||||
|
s['port'][0])
|
||||||
|
f.write('keyserver {0}\n'.format(_uri))
|
||||||
|
# Use stronger ciphers, etc. and prompt for check/certification levels
|
||||||
|
if os.path.lexists(_activegpgconf):
|
||||||
|
shutil.copy2(_activegpgconf, _bakgpgconf)
|
||||||
|
with open(_activegpgconf, 'w') as f:
|
||||||
|
f.write('cipher-algo AES256\ndigest-algo SHA512\ncert-digest-algo SHA512\ncompress-algo BZIP2\nask-cert-level\n')
|
||||||
|
## Restore files
|
||||||
|
if op in ('old', 'stop', 'restore'):
|
||||||
|
# Restore the keyservers
|
||||||
|
if os.path.lexists(_bakcfg):
|
||||||
|
with open(_bakcfg, 'r') as read, open(_activecfg, 'w') as write:
|
||||||
|
for line in read:
|
||||||
|
write.write(line)
|
||||||
|
os.remove(_bakcfg)
|
||||||
|
else:
|
||||||
|
os.remove(_activecfg)
|
||||||
|
# Restore GPG settings
|
||||||
|
if os.path.lexists(_bakgpgconf):
|
||||||
|
with open(_bakgpgconf, 'r') as read, open(_activegpgconf, 'w') as write:
|
||||||
|
for line in read:
|
||||||
|
write.write(line)
|
||||||
|
os.remove(_bakgpgconf)
|
||||||
|
else:
|
||||||
|
os.remove(_activegpgconf)
|
||||||
|
subprocess.run(['gpgconf', '--reload', 'dirmngr']) # I *really* wish we could do this via GPGME.
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getKeys(self):
|
||||||
|
_keyids = []
|
||||||
|
_keys = {}
|
||||||
|
# Do we have the key already? If not, fetch.
|
||||||
|
for r in list(self.args['rcpts'].keys()):
|
||||||
|
if self.args['rcpts'][r]['type'] == 'fpr':
|
||||||
|
_keyids.append(r)
|
||||||
|
self.ctx.set_keylist_mode(self.maps['keylist']['remote'])
|
||||||
|
try:
|
||||||
|
_k = self.ctx.get_key(r)
|
||||||
|
except:
|
||||||
|
print('{0}: We could not find this key on the keyserver.'.format(r)) # Key not on server
|
||||||
|
del(self.args['rcpts'][r])
|
||||||
|
_keyids.remove(r)
|
||||||
|
continue
|
||||||
|
self.ctx.set_keylist_mode(self.maps['keylist']['local'])
|
||||||
|
_keys[r] = {'fpr': r,
|
||||||
|
'obj': _k,
|
||||||
|
'created': _k.subkeys[0].timestamp}
|
||||||
|
if 'T' in str(_keys[r]['created']):
|
||||||
|
_keys[r]['created'] = int(datetime.datetime.strptime(_keys[r]['created'],
|
||||||
|
'%Y%m%dT%H%M%S').timestamp())
|
||||||
|
if self.args['rcpts'][r]['type'] == 'email':
|
||||||
|
# We need to actually do a lookup on the email address.
|
||||||
|
_keytmp = []
|
||||||
|
for k in self.ctx.keylist(r, mode = self.maps['keylist']['remote']):
|
||||||
|
_keytmp.append(k)
|
||||||
|
for k in _keytmp:
|
||||||
|
_keys[k.fpr] = {'fpr': k.fpr,
|
||||||
|
'obj': k,
|
||||||
|
'created': k.subkeys[0].timestamp,
|
||||||
|
'uids': {}}
|
||||||
|
# Per the docs (<gpg>/docs/DETAILS, "*** Field 6 - Creation date"),
|
||||||
|
# they may change this to ISO 8601...
|
||||||
|
if 'T' in str(_keys[k.fpr]['created']):
|
||||||
|
_keys[k.fpr]['created'] = int(datetime.datetime.strptime(_keys[k.fpr]['created'],
|
||||||
|
'%Y%m%dT%H%M%S').timestamp())
|
||||||
|
for s in k.uids:
|
||||||
|
_keys[k.fpr]['uids'][s.email] = {'comment': s.comment,
|
||||||
|
'updated': s.last_update}
|
||||||
|
if len(_keytmp) > 1: # Print the keys and prompt for a selection.
|
||||||
|
|
||||||
|
print('\nWe found the following keys for {0}...\n\nKEY ID:'.format(r))
|
||||||
|
for s in _keytmp:
|
||||||
|
print('{0}\n{1:6}(Generated at {2}) UIDs:'.format(s.fpr,
|
||||||
|
'',
|
||||||
|
datetime.datetime.utcfromtimestamp(s.subkeys[0].timestamp)))
|
||||||
|
for u in s.uids:
|
||||||
|
if u.last_update == 0:
|
||||||
|
_updated = 'Never/Unknown'
|
||||||
|
else:
|
||||||
|
_updated = datetime.datetime.utcfromtimestamp(u.last_update)
|
||||||
|
print('{0:42}(Updated {3}) <{2}> {1}'.format('',
|
||||||
|
u.comment,
|
||||||
|
u.email,
|
||||||
|
_updated))
|
||||||
|
print()
|
||||||
|
while True:
|
||||||
|
key = input('Please enter the (full) appropriate key: ')
|
||||||
|
if key not in _keys.keys():
|
||||||
|
print('Please enter a full key ID from the list above or hit ctrl-d to exit.')
|
||||||
|
else:
|
||||||
|
_keyids.append(key)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if len(_keytmp) == 0:
|
||||||
|
print('Could not find {0}!'.format(r))
|
||||||
|
del(self.args['rcpts'][r])
|
||||||
|
continue
|
||||||
|
_keyids.append(k.fpr)
|
||||||
|
print('\nFound key {0} for {1} (Generated at {2}):'.format(_keys[k.fpr]['fpr'],
|
||||||
|
r,
|
||||||
|
datetime.datetime.utcfromtimestamp(_keys[k.fpr]['created'])))
|
||||||
|
for email in _keys[k.fpr]['uids']:
|
||||||
|
if _keys[k.fpr]['uids'][email]['updated'] == 0:
|
||||||
|
_updated = 'Never/Unknown'
|
||||||
|
else:
|
||||||
|
_updated = datetime.datetime.utcfromtimestamp(_keys[k.fpr]['uids'][email]['updated'])
|
||||||
|
print('\t(Generated {2}) {0} <{1}>'.format(_keys[k.fpr]['uids'][email]['comment'],
|
||||||
|
email,
|
||||||
|
_updated))
|
||||||
|
print()
|
||||||
|
## And now we can (FINALLY) fetch the key(s).
|
||||||
|
print(_keyids)
|
||||||
|
for g in _keyids:
|
||||||
|
try:
|
||||||
|
self.ctx.op_import_keys([_keys[g]['obj']])
|
||||||
|
except gpg.errors.GPGMEError:
|
||||||
|
print('Key {0} could not be found on the keyserver'.format(g)) # The key isn't on the keyserver
|
||||||
|
self.ctx.set_keylist_mode(self.maps['keylist']['local'])
|
||||||
|
for k in _keys:
|
||||||
|
if k not in _keyids:
|
||||||
|
continue
|
||||||
|
_key = _keys[k]['obj']
|
||||||
|
self.keys[k] = {'pkey': {'email': _key.uids[0].email,
|
||||||
|
'name': _key.uids[0].name,
|
||||||
|
'creation': datetime.datetime.utcfromtimestamp(_keys[k]['created']),
|
||||||
|
'key': _key},
|
||||||
|
'trust': self.args['trustlevel'], # Not set yet; we'll modify this later in buildKeys().
|
||||||
|
'local': self.args['local'], # Not set yet; we'll modify this later in buildKeys().
|
||||||
|
'notify': self.args['notify'], # Same...
|
||||||
|
'sign': True, # We don't need to prompt for this since we detect if we need to sign or not
|
||||||
|
'change': None, # ""
|
||||||
|
'status': None} # Same.
|
||||||
|
# And we add the subkeys in yet another loop.
|
||||||
|
self.keys[k]['subkeys'] = {}
|
||||||
|
self.keys[k]['uids'] = {}
|
||||||
|
for s in _key.subkeys:
|
||||||
|
self.keys[k]['subkeys'][s.fpr] = datetime.datetime.utcfromtimestamp(s.timestamp)
|
||||||
|
for u in _key.uids:
|
||||||
|
self.keys[k]['uids'][u.email] = {'name': u.name,
|
||||||
|
'comment': u.comment,
|
||||||
|
'updated': datetime.datetime.utcfromtimestamp(u.last_update)}
|
||||||
|
del(_keys)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def buildKeys(self):
|
||||||
|
self.getKeys()
|
||||||
|
# Before anything else, let's set up our own key info.
|
||||||
|
_key = self.ctx.get_key(self.args['sigkey'], secret = True)
|
||||||
|
self.mykey = {'pkey': {'email': _key.uids[0].email,
|
||||||
|
'name': _key.uids[0].name,
|
||||||
|
'creation': datetime.datetime.utcfromtimestamp(_key.subkeys[0].timestamp),
|
||||||
|
'key': _key},
|
||||||
|
'trust': 'ultimate', # No duh. This is our own key.
|
||||||
|
'local': False, # We keep our own key array separate, so we don't push it anyways.
|
||||||
|
'notify': False, # ""
|
||||||
|
'check': None, # ""
|
||||||
|
'change': False, # ""
|
||||||
|
'status': None, # ""
|
||||||
|
'sign': False} # ""
|
||||||
|
self.mykey['subkeys'] = {}
|
||||||
|
self.mykey['uids'] = {}
|
||||||
|
for s in _key.subkeys:
|
||||||
|
self.mykey['subkeys'][s.fpr] = datetime.datetime.utcfromtimestamp(s.timestamp)
|
||||||
|
for u in _key.uids:
|
||||||
|
self.mykey['uids'][u.email] = {'name': u.name,
|
||||||
|
'comment': u.comment,
|
||||||
|
'updated': datetime.datetime.utcfromtimestamp(u.last_update)}
|
||||||
|
# Now let's set up our trusts.
|
||||||
|
if self.args['batch']:
|
||||||
|
self.batchParse()
|
||||||
|
else:
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
self.promptTrust(k)
|
||||||
|
self.promptCheck(k)
|
||||||
|
self.promptLocal(k)
|
||||||
|
self.promptNotify(k)
|
||||||
|
# In case we removed any keys, we have to run this outside of the loops
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
for t in ('trust', 'local', 'check', 'notify'):
|
||||||
|
self.keysCleanup(k, t)
|
||||||
|
# TODO: populate self.keys[key]['change']; we use this for trust (but not sigs)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def batchParse(self):
|
||||||
|
# First we grab the info from CSV
|
||||||
|
csvlines = csv.reader(self.csvraw, delimiter = ',', quotechar = '"')
|
||||||
|
for row in csvlines:
|
||||||
|
row[0] = row[0].replace('<', '').replace('>', '')
|
||||||
|
try:
|
||||||
|
if self.args['rcpts'][row[0]]['type'] == 'fpr':
|
||||||
|
k = row[0]
|
||||||
|
else: # It's an email.
|
||||||
|
key_set = False
|
||||||
|
while not key_set:
|
||||||
|
for i in list(self.keys.keys()):
|
||||||
|
if row[0] in list(self.keys[i]['uids'].keys()):
|
||||||
|
k = i
|
||||||
|
key_set = True
|
||||||
|
self.keys[k]['trust'] = row[1].lower().strip()
|
||||||
|
self.keys[k]['local'] = row[2].lower().strip()
|
||||||
|
self.keys[k]['check'] = row[3].lower().strip()
|
||||||
|
self.keys[k]['notify'] = row[4].lower().strip()
|
||||||
|
except KeyError:
|
||||||
|
continue # It was deemed to be an invalid key earlier
|
||||||
|
return()
|
||||||
|
|
||||||
|
def promptTrust(self, k):
|
||||||
|
if 'trust' not in self.keys[k].keys() or not self.keys[k]['trust']:
|
||||||
|
trust_in = input(('\nWhat trust level should we assign to {0}? (The default is '+
|
||||||
|
'Marginal.)\n\t\t\t\t ({1} <{2}>)' +
|
||||||
|
'\n\n\t\033[1m-1 = Never\n\t 0 = Unknown\n\t 1 = Untrusted\n\t 2 = Marginal\n\t 3 = Full' +
|
||||||
|
'\n\t 4 = Ultimate\033[0m\nTrust: ').format(k,
|
||||||
|
self.keys[k]['pkey']['name'],
|
||||||
|
self.keys[k]['pkey']['email']))
|
||||||
|
if trust_in == '':
|
||||||
|
trust_in = 'marginal' # Has to be a str, so we can "pretend" it was entered
|
||||||
|
self.keys[k]['trust'] = trust_in
|
||||||
|
return()
|
||||||
|
|
||||||
|
def promptCheck(self, k):
|
||||||
|
if 'check' not in self.keys[k].keys() or self.keys[k]['check'] == None:
|
||||||
|
check_in = input(('\nHow carefully have you checked {0}\'s validity of identity/ownership of the key? ' +
|
||||||
|
'(Default is Unknown.)\n' +
|
||||||
|
'\n\t\033[1m0 = Unknown\n\t1 = None\n\t2 = Casual\n\t3 = Careful\033[0m\nCheck level: ').format(k))
|
||||||
|
if check_in == '':
|
||||||
|
check_in = 'unknown'
|
||||||
|
self.keys[k]['check'] = check_in
|
||||||
|
return()
|
||||||
|
|
||||||
|
def promptLocal(self, k):
|
||||||
|
if 'local' not in self.keys[k].keys() or self.keys[k]['local'] == None:
|
||||||
|
if self.args['keyservers']:
|
||||||
|
local_in = input(('\nShould we locally sign {0} '+
|
||||||
|
'(if yes, the signature will be non-exportable; if no, we will be able to push to a keyserver) ' +
|
||||||
|
'(Yes/\033[1mNO\033[0m)? ').format(k))
|
||||||
|
if local_in == '':
|
||||||
|
local_in = False
|
||||||
|
self.keys[k]['local'] = local_in
|
||||||
|
return()
|
||||||
|
|
||||||
|
def promptNotify(self, k):
|
||||||
|
if 'notify' not in self.keys[k].keys() or self.keys[k]['notify'] == None:
|
||||||
|
notify_in = input(('\nShould we notify {0} (via <{1}>) (\033[1mYES\033[0m/No)? ').format(k,
|
||||||
|
self.keys[k]['pkey']['email']))
|
||||||
|
if notify_in == '':
|
||||||
|
notify_in = True
|
||||||
|
self.keys[k]['local'] = local_in
|
||||||
|
return()
|
||||||
|
|
||||||
|
def keysCleanup(self, k, t): # At some point, this WHOLE thing would probably be cleaner with bitwise flags...
|
||||||
|
s = t
|
||||||
|
_errs = {'trust': 'trust level',
|
||||||
|
'local': 'local signature option',
|
||||||
|
'check': 'check level',
|
||||||
|
'notify': 'notify flag'}
|
||||||
|
if k not in self.keys.keys():
|
||||||
|
return() # It was deleted already.
|
||||||
|
if t in ('local', 'notify'): # these use a binary mapping
|
||||||
|
t = 'binmap'
|
||||||
|
# We can do some basic stuff right here.
|
||||||
|
if str(self.keys[k][s]).lower() in ('n', 'no', 'false'):
|
||||||
|
self.keys[k][s] = False
|
||||||
|
return()
|
||||||
|
elif str(self.keys[k][s]).lower() in ('y', 'yes', 'true'):
|
||||||
|
self.keys[k][s] = True
|
||||||
|
return()
|
||||||
|
# Make sure we have a known value. These will ALWAYS be str's, either from the CLI or CSV.
|
||||||
|
value_in = str(self.keys[k][s]).lower().strip()
|
||||||
|
for dictk, dictv in self.maps[t].items():
|
||||||
|
if value_in == dictv[0]:
|
||||||
|
self.keys[k][s] = int(dictk)
|
||||||
|
elif value_in == str(dictk):
|
||||||
|
self.keys[k][s] = int(dictk)
|
||||||
|
if not isinstance(self.keys[k][s], int): # It didn't get set
|
||||||
|
print('{0}: "{1}" is not a valid {2}; skipping. Run kant again to fix.'.format(k, self.keys[k][s], _errs[s]))
|
||||||
|
del(self.keys[k])
|
||||||
|
return()
|
||||||
|
# Determine if we need to change the trust.
|
||||||
|
if t == 'trust':
|
||||||
|
cur_trust = self.keys[k]['pkey']['key'].owner_trust
|
||||||
|
if cur_trust == self.keys[k]['trust']:
|
||||||
|
self.keys[k]['change'] = False
|
||||||
|
else:
|
||||||
|
self.keys[k]['change'] = True
|
||||||
|
return()
|
||||||
|
|
||||||
|
def sigKeys(self): # The More Business-End(TM)
|
||||||
|
# NOTE: If the trust level is anything but 2 (the default), we should use op_interact() instead and do a tsign.
|
||||||
|
self.ctx.keylist_mode = gpg.constants.KEYLIST_MODE_SIGS
|
||||||
|
_mkey = self.mykey['pkey']['key']
|
||||||
|
self.ctx.signers = [_mkey]
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
key = self.keys[k]['pkey']['key']
|
||||||
|
for uid in key.uids:
|
||||||
|
for s in uid.signatures:
|
||||||
|
try:
|
||||||
|
signerkey = ctx.get_key(s.keyid).subkeys[0].fpr
|
||||||
|
if signerkey == mkey.subkeys[0].fpr:
|
||||||
|
self.trusts[k]['sign'] = False # We already signed this key
|
||||||
|
except gpgme.GpgError:
|
||||||
|
pass # usually if we get this it means we don't have a signer's key in our keyring
|
||||||
|
# And again, we loop. ALLLLL that buildup for one line.
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
# TODO: configure to allow for user-entered expiration?
|
||||||
|
if self.keys[k]['sign']:
|
||||||
|
self.ctx.key_sign(self.keys[k]['pkey']['key'], local = self.keys[k]['local'])
|
||||||
|
return()
|
||||||
|
|
||||||
|
class KeyEditor(object):
|
||||||
|
def __init__(self, optmap):
|
||||||
|
self.replied_once = False # This is used to handle the first prompt vs. the last
|
||||||
|
self.optmap = optmap
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
def editKey(self, status, args, out):
|
||||||
|
_result = None
|
||||||
|
out.seek(0, 0)
|
||||||
|
def mapDict(m, d):
|
||||||
|
return(reduce(operator.getitem, m, d))
|
||||||
|
if args == 'keyedit.prompt' and self.replied_once:
|
||||||
|
_result = 'quit'
|
||||||
|
elif status == 'KEY_CONSIDERED':
|
||||||
|
_result = None
|
||||||
|
self.replied_once = False
|
||||||
|
elif status == 'GET_LINE':
|
||||||
|
self.replied_once = True
|
||||||
|
_ilist = args.split('.')
|
||||||
|
_result = mapDict(_ilist, self.optmap['prompts'])
|
||||||
|
if not _result:
|
||||||
|
_result = None
|
||||||
|
return(_result)
|
||||||
|
|
||||||
|
def trustKeys(self): # The Son of Business-End(TM)
|
||||||
|
# TODO: add check for change
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
_key = self.keys[k]
|
||||||
|
if _key['change']:
|
||||||
|
_map = self.getEditPrompt(_key, 'trust')
|
||||||
|
out = gpg.Data()
|
||||||
|
self.ctx.interact(_key['pkey']['key'], self.KeyEditor(_map).editKey, sink = out, fnc_value = out)
|
||||||
|
out.seek(0, 0)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def pushKeys(self): # The Last Business-End(TM)
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
if not self.keys[k]['local'] and self.keys[k]['sign']:
|
||||||
|
self.ctx.op_export(k, gpg.constants.EXPORT_MODE_EXTERN, None)
|
||||||
|
return()
|
||||||
|
|
||||||
|
class Mailer(object): # I lied; The Return of the Business-End(TM)
|
||||||
|
def __init__(self):
|
||||||
|
_homeconf = os.path.join(os.environ['HOME'], '.msmtprc')
|
||||||
|
_sysconf = '/etc/msmtprc'
|
||||||
|
self.msmtp = {'conf': None}
|
||||||
|
if not os.path.isfile(_homeconf):
|
||||||
|
if not os.path.isfile(_sysconf):
|
||||||
|
self.msmtp['conf'] = False
|
||||||
|
else:
|
||||||
|
self.msmtp['conf'] = _sysconf
|
||||||
|
else:
|
||||||
|
self.msmtp['conf'] = _homeconf
|
||||||
|
if self.msmtp['conf']:
|
||||||
|
# Okay. So we have a config file, which we're assuming to be set up correctly.
|
||||||
|
# Now we need to parse the config.
|
||||||
|
self.msmtp['cfg'] = self.getCfg()
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
def getCfg(self):
|
||||||
|
cfg = {'default': None, 'defaults': {}}
|
||||||
|
_defaults = False
|
||||||
|
_acct = None
|
||||||
|
with open(self.msmtp['conf'], 'r') as f:
|
||||||
|
_cfg_raw = f.read()
|
||||||
|
for l in _cfg_raw.splitlines():
|
||||||
|
if re.match('^\s?(#.*|)$', l):
|
||||||
|
continue # Skip over blank and commented lines
|
||||||
|
_line = [i.strip() for i in re.split('\s+', l.strip(), maxsplit = 1)]
|
||||||
|
if _line[0] == 'account':
|
||||||
|
if re.match('^default\s?:\s?', _line[1]): # it's the default account specifier
|
||||||
|
cfg['default'] = _line[1].split(':', maxsplit = 1)[1].strip()
|
||||||
|
else:
|
||||||
|
if _line[1] not in cfg.keys(): # it's a new account definition
|
||||||
|
cfg[_line[1]] = {}
|
||||||
|
_acct = _line[1]
|
||||||
|
_defaults = False
|
||||||
|
elif _line[0] == 'defaults': # it's the defaults
|
||||||
|
_acct = 'defaults'
|
||||||
|
else: # it's a config directive
|
||||||
|
cfg[_acct][_line[0]] = _line[1]
|
||||||
|
for a in list(cfg):
|
||||||
|
if a != 'default':
|
||||||
|
for k, v in cfg['defaults'].items():
|
||||||
|
if k not in cfg[a].keys():
|
||||||
|
cfg[a][k] = v
|
||||||
|
del(cfg['defaults'])
|
||||||
|
return(cfg)
|
||||||
|
|
||||||
|
def sendEmail(self, msg, key, profile): # This needs way more parsing to support things like plain ol' port 25 plaintext (ugh), etc.
|
||||||
|
if 'tls-starttls' in self.msmtp['cfg'][profile].keys() and self.msmtp['cfg'][profile]['tls-starttls'] == 'on':
|
||||||
|
smtpserver = smtplib.SMTP(self.msmtp['cfg'][profile]['host'], int(self.msmtp['cfg'][profile]['port']))
|
||||||
|
smtpserver.ehlo()
|
||||||
|
smtpserver.starttls()
|
||||||
|
# we need to EHLO twice with a STARTTLS because email is weird.
|
||||||
|
elif self.msmtp['cfg'][profile]['tls'] == 'on':
|
||||||
|
smtpserver = smtplib.SMTP_SSL(self.msmtp['cfg'][profile]['host'], int(self.msmtp['cfg'][profile]['port']))
|
||||||
|
smtpserver.ehlo()
|
||||||
|
smtpserver.login(self.msmtp['cfg'][profile]['user'], self.msmtp['cfg'][profile]['password'])
|
||||||
|
smtpserver.sendmail(self.msmtp['cfg'][profile]['user'], key['pkey']['email'], msg.as_string())
|
||||||
|
smtpserver.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def postalWorker(self):
|
||||||
|
m = self.Mailer()
|
||||||
|
if 'KANT' in m.msmtp['cfg'].keys():
|
||||||
|
_profile = 'KANT'
|
||||||
|
else:
|
||||||
|
_profile = m.msmtp['cfg']['default'] # TODO: let this be specified on the CLI args?
|
||||||
|
if 'user' not in m.msmtp['cfg'][_profile].keys() or not m.msmtp['cfg'][_profile]['user']:
|
||||||
|
return() # We don't have MSMTP configured.
|
||||||
|
# Reconstruct the keyserver list.
|
||||||
|
_keyservers = []
|
||||||
|
for k in self.args['keyservers']:
|
||||||
|
_keyservers.append('{0}://{1}:{2}'.format(k['proto'], k['server'], k['port'][0]))
|
||||||
|
# Export our key so we can attach it.
|
||||||
|
_pubkeys = {}
|
||||||
|
for e in ('asc', 'gpg'):
|
||||||
|
if e == 'asc':
|
||||||
|
self.ctx.armor = True
|
||||||
|
else:
|
||||||
|
self.ctx.armor = False
|
||||||
|
_pubkeys[e] = gpg.Data() # This is a data buffer to store your ASCII-armored pubkeys
|
||||||
|
self.ctx.op_export_keys([self.mykey['pkey']['key']], 0, _pubkeys[e])
|
||||||
|
_pubkeys[e].seek(0, 0) # Read with e.g. _sigs['asc'].read()
|
||||||
|
for k in list(self.keys.keys()):
|
||||||
|
if self.keys[k]['notify']:
|
||||||
|
_body = {}
|
||||||
|
for t in list(self.tpls.keys()):
|
||||||
|
# There's gotta be a more efficient way of doing this...
|
||||||
|
#_tplenv = jinja2.Environment(loader = jinja2.BaseLoader()).from_string(self.tpls[t])
|
||||||
|
_tplenv = jinja2.Environment().from_string(self.tpls[t])
|
||||||
|
_body[t] = _tplenv.render(key = self.keys[k],
|
||||||
|
mykey = self.mykey,
|
||||||
|
keyservers = _keyservers)
|
||||||
|
b = MIMEMultipart('alternative') # Set up a body
|
||||||
|
for c in _body.keys():
|
||||||
|
b.attach(MIMEText(_body[c], c))
|
||||||
|
bmsg = MIMEMultipart()
|
||||||
|
bmsg.attach(b)
|
||||||
|
for s in _pubkeys.keys():
|
||||||
|
_attchmnt = MIMEApplication(_pubkeys[s].read(), '{0}.{1}'.format(self.mykey['pkey']['key'].fpr, s))
|
||||||
|
_attchmnt['Content-Disposition'] = 'attachment; filename="{0}.{1}"'.format(self.mykey['pkey']['key'].fpr, s)
|
||||||
|
bmsg.attach(_attchmnt)
|
||||||
|
# Now we sign the body. This incomprehensible bit monkey-formats bmsg to be a multi-RFC-compatible
|
||||||
|
# string, which is then passed to our gpgme instance's signing mechanishm, and the output of that is
|
||||||
|
# returned as plaintext. Whew.
|
||||||
|
self.ctx.armor = True
|
||||||
|
|
||||||
|
_sig = self.ctx.sign((bmsg.as_string().replace('\n', '\r\n')).encode('utf-8'),
|
||||||
|
mode = gpg.constants.SIG_MODE_DETACH)
|
||||||
|
imsg = Message() # Build yet another intermediate message...
|
||||||
|
imsg['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
|
||||||
|
imsg['Content-Description'] = 'OpenPGP digital signature'
|
||||||
|
imsg.set_payload(_sig[0].decode('utf-8'))
|
||||||
|
msg = MIMEMultipart(_subtype = 'signed',
|
||||||
|
micalg = "pgp-{0}".format(self.maps['hashalgos'][_sig[1].signatures[0].hash_algo]),
|
||||||
|
protocol = 'application/pgp-signature')
|
||||||
|
msg.attach(bmsg) # Attach the body (plaintext, html, pubkey attachmants)
|
||||||
|
msg.attach(imsg) # Attach the isignature
|
||||||
|
msg['To'] = self.keys[k]['pkey']['email']
|
||||||
|
if 'from' in m.msmtp['cfg'][_profile].keys():
|
||||||
|
msg['From'] = m.msmtp['cfg'][_profile]['from']
|
||||||
|
else:
|
||||||
|
msg['From'] = self.mykey['pkey']['email']
|
||||||
|
msg['Subject'] = 'Your GnuPG/PGP key has been signed'
|
||||||
|
msg['Openpgp'] = 'id={0}'.format(self.mykey['pkey']['key'].fpr)
|
||||||
|
msg['Date'] = datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
|
||||||
|
msg['User-Agent'] = 'KANT (part of the r00t^2 OpTools suite: https://git.square-r00t.net/OpTools)'
|
||||||
|
m.sendEmail(msg, self.keys[k], _profile) # Send the email
|
||||||
|
for d in (msg, imsg, bmsg, b, _body, _tplenv): # Not necessary, but it pays to be paranoid; we do NOT want leaks.
|
||||||
|
del(d)
|
||||||
|
del(m)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def saveResults(self):
|
||||||
|
_cachedir = os.path.join(self.cfgdir, 'cache', datetime.datetime.utcnow().strftime('%Y.%m.%d_%H.%M.%S'))
|
||||||
|
os.makedirs(_cachedir, exist_ok = True)
|
||||||
|
for k in self.keys.keys():
|
||||||
|
_keyout = self.keys[k]
|
||||||
|
# We need to normalize the datetime objects and gpg objects to strings
|
||||||
|
_keyout['pkey']['creation'] = str(self.keys[k]['pkey']['creation'])
|
||||||
|
_keyout['pkey']['key'] = '<GPGME object>'
|
||||||
|
for u in list(_keyout['uids'].keys()):
|
||||||
|
_keyout['uids'][u]['updated'] = str(self.keys[k]['uids'][u]['updated'])
|
||||||
|
for s in list(_keyout['subkeys'].keys()):
|
||||||
|
_keyout['subkeys'][s] = str(self.keys[k]['subkeys'][s])
|
||||||
|
_fname = os.path.join(_cachedir, '{0}.json'.format(k))
|
||||||
|
with open(_fname, 'a') as f:
|
||||||
|
f.write('{0}\n'.format(json.dumps(_keyout, sort_keys = True, indent = 4)))
|
||||||
|
del(_keyout)
|
||||||
|
# And let's grab a copy of our key in the state that it exists in currently
|
||||||
|
_mykey = self.mykey
|
||||||
|
# We need to normalize the datetime objects and gpg objects to strings again
|
||||||
|
_mykey['pkey']['creation'] = str(_mykey['pkey']['creation'])
|
||||||
|
_mykey['pkey']['key'] = '<GPGME object>'
|
||||||
|
for u in list(_mykey['uids'].keys()):
|
||||||
|
_mykey['uids'][u]['updated'] = str(self.mykey['uids'][u]['updated'])
|
||||||
|
for s in list(_mykey['subkeys'].keys()):
|
||||||
|
_mykey['subkeys'][s] = str(self.mykey['subkeys'][s])
|
||||||
|
with open(os.path.join(_cachedir, '_SIGKEY.json'), 'w') as f:
|
||||||
|
f.write('{0}\n'.format(json.dumps(_mykey, sort_keys = True, indent = 4)))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def serverParser(self, uri):
|
||||||
|
# https://en.wikipedia.org/wiki/Key_server_(cryptographic)#Keyserver_examples
|
||||||
|
_server = {}
|
||||||
|
_urlobj = urllib.parse.urlparse(uri)
|
||||||
|
_server['proto'] = _urlobj.scheme
|
||||||
|
_lazy = False
|
||||||
|
if not _server['proto']:
|
||||||
|
_server['proto'] = 'hkp' # Default
|
||||||
|
_server['server'] = _urlobj.hostname
|
||||||
|
if not _server['server']:
|
||||||
|
_server['server'] = re.sub('^([A-Za-z]://)?(.+[^:][^0-9])(:[0-9]+)?$', '\g<2>', uri, re.MULTILINE)
|
||||||
|
_lazy = True
|
||||||
|
_server['port'] = _urlobj.port
|
||||||
|
if not _server['port']:
|
||||||
|
if _lazy:
|
||||||
|
_p = re.sub('.*:([0-9]+)$', '\g<1>', uri, re.MULTILINE)
|
||||||
|
_server['port'] = self.maps['proto'][_server['proto']] # Default
|
||||||
|
return(_server)
|
||||||
|
|
||||||
|
def verifyArgs(self, locargs):
|
||||||
|
## Some pythonization...
|
||||||
|
if not locargs['batch']:
|
||||||
|
locargs['keys'] = [re.sub('\s', '', k) for k in locargs['keys'].split(',')]
|
||||||
|
else:
|
||||||
|
## Batch file
|
||||||
|
_batchfilepath = os.path.abspath(os.path.expanduser(locargs['keys']))
|
||||||
|
if not os.path.isfile(_batchfilepath):
|
||||||
|
raise ValueError('{0} does not exist or is not a regular file.'.format(_batchfilepath))
|
||||||
|
else:
|
||||||
|
with open(_batchfilepath, 'r') as f:
|
||||||
|
self.csvraw = f.readlines()
|
||||||
|
locargs['keys'] = _batchfilepath
|
||||||
|
locargs['keyservers'] = [re.sub('\s', '', s) for s in locargs['keyservers'].split(',')]
|
||||||
|
locargs['keyservers'] = [self.serverParser(s) for s in locargs['keyservers']]
|
||||||
|
## Key(s) to sign
|
||||||
|
locargs['rcpts'] = {}
|
||||||
|
if not locargs['batch']:
|
||||||
|
_keyiter = locargs['keys']
|
||||||
|
else:
|
||||||
|
_keyiter = []
|
||||||
|
for row in csv.reader(self.csvraw, delimiter = ',', quotechar = '"'):
|
||||||
|
_keyiter.append(row[0])
|
||||||
|
for k in _keyiter:
|
||||||
|
locargs['rcpts'][k] = {}
|
||||||
|
try:
|
||||||
|
int(k, 16)
|
||||||
|
_ktype = 'fpr'
|
||||||
|
except: # If it isn't a valid key ID...
|
||||||
|
if not re.match('^<?[\w\.\+\-]+\@[\w-]+\.[a-z]{2,3}>?$', k): # is it an email address?
|
||||||
|
raise ValueError('{0} is not a valid email address'.format(k))
|
||||||
|
else:
|
||||||
|
r = k.replace('<', '').replace('>', '')
|
||||||
|
locargs['rcpts'][r] = locargs['rcpts'][k]
|
||||||
|
if k != r:
|
||||||
|
del(locargs['rcpts'][k])
|
||||||
|
k = r
|
||||||
|
_ktype = 'email'
|
||||||
|
locargs['rcpts'][k]['type'] = _ktype
|
||||||
|
# Security is important. We don't want users getting collisions, so we don't allow shortened key IDs.
|
||||||
|
if _ktype == 'fpr' and not len(k) == 40:
|
||||||
|
raise ValueError('{0} is not a full 40-char key ID or key fingerprint'.format(k))
|
||||||
|
## Signing key
|
||||||
|
if not locargs['sigkey']:
|
||||||
|
raise ValueError('A key for signing is required') # We need a key we can sign with.
|
||||||
|
else:
|
||||||
|
if not os.path.lexists(locargs['gpgdir']):
|
||||||
|
raise FileNotFoundError('{0} does not exist'.format(locargs['gpgdir']))
|
||||||
|
elif os.path.isfile(locargs['gpgdir']):
|
||||||
|
raise NotADirectoryError('{0} is not a directory'.format(locargs['gpgdir']))
|
||||||
|
# Now we need to verify that the private key exists...
|
||||||
|
try:
|
||||||
|
_ctx = gpg.Context()
|
||||||
|
_sigkey = _ctx.get_key(locargs['sigkey'], True)
|
||||||
|
except gpg.errors.GPGMEError or gpg.errors.KeyNotFound:
|
||||||
|
raise ValueError('Cannot use key {0}'.format(locargs['sigkey']))
|
||||||
|
# And that it is an eligible candidate to use to sign.
|
||||||
|
if not _sigkey.can_sign or True in (_sigkey.revoked, _sigkey.expired, _sigkey.disabled):
|
||||||
|
raise ValueError('{0} is not a valid candidate for signing'.format(locargs['sigkey']))
|
||||||
|
## Keyservers
|
||||||
|
if locargs['testkeyservers']:
|
||||||
|
for s in locargs['keyservers']:
|
||||||
|
# Test to make sure the keyserver is accessible.
|
||||||
|
_v6test = socket(AF_INET6, SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
_v6test.connect(('ipv6.square-r00t.net', 0))
|
||||||
|
_nettype = AF_INET6 # We have IPv6 intarwebz
|
||||||
|
except:
|
||||||
|
_nettype = AF_INET # No IPv6, default to IPv4
|
||||||
|
for _proto in locargs['keyservers'][s]['port'][1]:
|
||||||
|
if _proto == 'udp':
|
||||||
|
_netproto = SOCK_DGRAM
|
||||||
|
elif _proto == 'tcp':
|
||||||
|
_netproto = SOCK_STREAM
|
||||||
|
_sock = socket(nettype, netproto)
|
||||||
|
_sock.settimeout(10)
|
||||||
|
_tests = _sock.connect_ex((locargs['keyservers'][s]['server'],
|
||||||
|
int(locargs['keyservers'][s]['port'][0])))
|
||||||
|
_uristr = '{0}://{1}:{2} ({3})'.format(locargs['keyservers'][s]['proto'],
|
||||||
|
locargs['keyservers'][s]['server'],
|
||||||
|
locargs['keyservers'][s]['port'][0],
|
||||||
|
_proto.upper())
|
||||||
|
if not tests == 0:
|
||||||
|
raise OSError('Keyserver {0} is not available'.format(_uristr))
|
||||||
|
else:
|
||||||
|
print('Keyserver {0} is accepting connections.'.format(_uristr))
|
||||||
|
sock.close()
|
||||||
|
return(locargs)
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
def getDefGPGDir():
|
||||||
|
try:
|
||||||
|
gpgdir = os.environ['GNUPGHOME']
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
homedir = os.environ['HOME']
|
||||||
|
gpgdchk = os.path.join(homedir, '.gnupg')
|
||||||
|
except KeyError:
|
||||||
|
# There is no reason that this should ever get this far, but... edge cases be crazy.
|
||||||
|
gpgdchk = os.path.join(os.path.expanduser('~'), '.gnupg')
|
||||||
|
if os.path.isdir(gpgdchk):
|
||||||
|
gpgdir = gpgdchk
|
||||||
|
else:
|
||||||
|
gpgdir = None
|
||||||
|
return(gpgdir)
|
||||||
|
def getDefKey(defgpgdir):
|
||||||
|
os.environ['GNUPGHOME'] = defgpgdir
|
||||||
|
if not defgpgdir:
|
||||||
|
return(None)
|
||||||
|
defkey = None
|
||||||
|
ctx = gpg.Context()
|
||||||
|
for k in ctx.keylist(None, secret = True): # "None" is query string; this grabs all keys in the private keyring
|
||||||
|
if k.can_sign and True not in (k.revoked, k.expired, k.disabled):
|
||||||
|
defkey = k.subkeys[0].fpr
|
||||||
|
break # We'll just use the first primary key we find that's valid as the default.
|
||||||
|
return(defkey)
|
||||||
|
def getDefKeyservers(defgpgdir):
|
||||||
|
srvlst = [None]
|
||||||
|
# We don't need these since we use the gpg agent. Requires GPG 2.1 and above, probably.
|
||||||
|
#if os.path.isfile(os.path.join(defgpgdir, 'dirmngr.conf')):
|
||||||
|
# pass
|
||||||
|
dirmgr_out = subprocess.run(['gpg-connect-agent', '--dirmngr', 'keyserver', '/bye'], stdout = subprocess.PIPE)
|
||||||
|
for l in dirmgr_out.stdout.decode('utf-8').splitlines():
|
||||||
|
#if len(l) == 3 and l.lower().startswith('s keyserver'): # It's a keyserver line
|
||||||
|
if l.lower().startswith('s keyserver'): # It's a keyserver line
|
||||||
|
s = l.split()[2]
|
||||||
|
if len(srvlst) == 1 and srvlst[0] == None:
|
||||||
|
srvlst = [s]
|
||||||
|
else:
|
||||||
|
srvlst.append(s)
|
||||||
|
return(','.join(srvlst))
|
||||||
|
defgpgdir = getDefGPGDir()
|
||||||
|
defkey = getDefKey(defgpgdir)
|
||||||
|
defkeyservers = getDefKeyservers(defgpgdir)
|
||||||
|
args = argparse.ArgumentParser(description = 'Keysigning Assistance and Notifying Tool (KANT)',
|
||||||
|
epilog = 'brent s. || 2017 || https://square-r00t.net')
|
||||||
|
args.add_argument('-k',
|
||||||
|
'--keys',
|
||||||
|
dest = 'keys',
|
||||||
|
metavar = 'KEYS | /path/to/batchfile',
|
||||||
|
required = True,
|
||||||
|
help = 'A single/comma-separated list of keys to sign, ' +
|
||||||
|
'trust, & notify. Can also be an email address. ' +
|
||||||
|
'If -b/--batch is specified, this should instead be ' +
|
||||||
|
'a path to the batch file. See the man page for more info.')
|
||||||
|
args.add_argument('-K',
|
||||||
|
'--sigkey',
|
||||||
|
dest = 'sigkey',
|
||||||
|
default = defkey,
|
||||||
|
help = 'The key to use when signing other keys. Default is \033[1m{0}\033[0m.'.format(defkey))
|
||||||
|
args.add_argument('-t',
|
||||||
|
'--trust',
|
||||||
|
dest = 'trustlevel',
|
||||||
|
default = None,
|
||||||
|
help = 'The trust level to automatically apply to all keys ' +
|
||||||
|
'(if not specified, kant will prompt for each key). ' +
|
||||||
|
'See BATCHFILE/TRUSTLEVEL in the man page for trust ' +
|
||||||
|
'level notations.')
|
||||||
|
args.add_argument('-c',
|
||||||
|
'--check',
|
||||||
|
dest = 'checklevel',
|
||||||
|
default = None,
|
||||||
|
help = 'The level of checking done (if not specified, kant will ' +
|
||||||
|
'prompt for each key). See -b/--batch for check level notations.')
|
||||||
|
args.add_argument('-l',
|
||||||
|
'--local',
|
||||||
|
dest = 'local',
|
||||||
|
default = None,
|
||||||
|
help = 'Make the signature(s) local-only (i.e. don\'t push to a keyserver).')
|
||||||
|
args.add_argument('-n',
|
||||||
|
'--no-notify',
|
||||||
|
dest = 'notify',
|
||||||
|
action = 'store_false',
|
||||||
|
help = 'If specified, do NOT notify any key recipients that you\'ve signed ' +
|
||||||
|
'their key, even if KANT is able to.')
|
||||||
|
args.add_argument('-s',
|
||||||
|
'--keyservers',
|
||||||
|
dest = 'keyservers',
|
||||||
|
default = defkeyservers,
|
||||||
|
help = 'The comma-separated keyserver(s) to push to.\n' +
|
||||||
|
'Default keyserver list is: \n\n\t\033[1m{0}\033[0m\n\n'.format(re.sub(',',
|
||||||
|
'\n\t',
|
||||||
|
defkeyservers)))
|
||||||
|
args.add_argument('-m',
|
||||||
|
'--msmtp',
|
||||||
|
dest = 'msmtp_profile',
|
||||||
|
default = None,
|
||||||
|
help = 'The msmtp profile to use to send the notification emails. See the man page for more information.')
|
||||||
|
args.add_argument('-b',
|
||||||
|
'--batch',
|
||||||
|
dest = 'batch',
|
||||||
|
action = 'store_true',
|
||||||
|
help = 'If specified, -k/--keys is a CSV file to use as a ' +
|
||||||
|
'batch run. See the BATCHFILE section in the man page for more info.')
|
||||||
|
args.add_argument('-D',
|
||||||
|
'--gpgdir',
|
||||||
|
dest = 'gpgdir',
|
||||||
|
default = defgpgdir,
|
||||||
|
help = 'The GnuPG configuration directory to use (containing\n' +
|
||||||
|
'your keys, etc.); default is \033[1m{0}\033[0m.'.format(defgpgdir))
|
||||||
|
args.add_argument('-T',
|
||||||
|
'--testkeyservers',
|
||||||
|
dest = 'testkeyservers',
|
||||||
|
action = 'store_true',
|
||||||
|
help = 'If specified, initiate a test connection with each\n'
|
||||||
|
'set keyserver before anything else. Disabled by default.')
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# This could be cleaner-looking, but we do it this way so the class can be used externally
|
||||||
|
# with a dict instead of an argparser result.
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
sess = SigSession(args)
|
||||||
|
sess.modifyDirmngr('new')
|
||||||
|
sess.buildKeys()
|
||||||
|
sess.sigKeys()
|
||||||
|
sess.trustKeys()
|
||||||
|
sess.pushKeys()
|
||||||
|
sess.postalWorker()
|
||||||
|
sess.saveResults()
|
||||||
|
sess.modifyDirmngr('old')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
282
gpg/kant/test.py
Executable file
282
gpg/kant/test.py
Executable file
@ -0,0 +1,282 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# This is less of a test suite and more of an active documentation on some python-gpgme (https://pypi.python.org/pypi/gpg) examples.
|
||||||
|
# Because their only documentation for the python bindings is in pydoc, and the C API manual is kind of useless.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import gpg
|
||||||
|
import gpg.constants
|
||||||
|
import inspect
|
||||||
|
import jinja2
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
|
import smtplib
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
import subprocess
|
||||||
|
import operator
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
os.environ['GNUPGHOME'] = '/home/bts/tmpgpg'
|
||||||
|
# JUST in case we need to...
|
||||||
|
#subprocess.run(['gpgconf', '--reload', 'dirmngr'])
|
||||||
|
|
||||||
|
# my key ID
|
||||||
|
#mykey = '748231EBCBD808A14F5E85D28C004C2F93481F6B'
|
||||||
|
mykey = '2805EC3D90E2229795AFB73FF85BC40E6E17F339'
|
||||||
|
# a key to test with
|
||||||
|
theirkey = 'CA7D304ABA7A3E24C9414D32FFA0F1361AD82A06'
|
||||||
|
testfetch = [theirkey, '748231EBCBD808A14F5E85D28C004C2F93481F6B']
|
||||||
|
|
||||||
|
# Create a context
|
||||||
|
# Params:
|
||||||
|
#armor -- enable ASCII armoring (default False)
|
||||||
|
#textmode -- enable canonical text mode (default False)
|
||||||
|
#offline -- do not contact external key sources (default False)
|
||||||
|
#signers -- list of keys used for signing (default [])
|
||||||
|
#pinentry_mode -- pinentry mode (default PINENTRY_MODE_DEFAULT)
|
||||||
|
#protocol -- protocol to use (default PROTOCOL_OpenPGP)
|
||||||
|
#home_dir -- state directory (default is the engine default)
|
||||||
|
ctx = gpg.Context()
|
||||||
|
|
||||||
|
# Fetch a key from the keyring
|
||||||
|
#secret -- to request a secret key
|
||||||
|
mkey = ctx.get_key(mykey)
|
||||||
|
tkey = ctx.get_key(theirkey)
|
||||||
|
|
||||||
|
## Print the attributes of our key and other info
|
||||||
|
##https://stackoverflow.com/a/41737776
|
||||||
|
##for k in (mkey, tkey):
|
||||||
|
#for k in [mkey]:
|
||||||
|
# for i in inspect.getmembers(k):
|
||||||
|
# if not i[0].startswith('_'):
|
||||||
|
# pprint.pprint(i)
|
||||||
|
#pprint.pprint(ctx.get_engine_info())
|
||||||
|
|
||||||
|
# Print the constants
|
||||||
|
#pprint.pprint(inspect.getmembers(gpg.constants))
|
||||||
|
|
||||||
|
# Get remote key. Use an OR to search both keyserver and local.
|
||||||
|
#ctx.set_keylist_mode(gpg.constants.KEYLIST_MODE_EXTERN|gpg.constants.KEYLIST_MODE_LOCAL)
|
||||||
|
klmodes = {'local': gpg.constants.KEYLIST_MODE_LOCAL,
|
||||||
|
'remote': gpg.constants.KEYLIST_MODE_EXTERN,
|
||||||
|
'both': gpg.constants.KEYLIST_MODE_LOCAL|gpg.constants.KEYLIST_MODE_EXTERN}
|
||||||
|
|
||||||
|
# List keys
|
||||||
|
#pattern -- return keys matching pattern (default: all keys)
|
||||||
|
#secret -- return only secret keys (default: False)
|
||||||
|
#mode -- keylist mode (default: list local keys)
|
||||||
|
#source -- read keys from source instead from the keyring
|
||||||
|
# (all other options are ignored in this case)
|
||||||
|
tkey2 = None
|
||||||
|
|
||||||
|
# jrdemasi@gmail.com = 0xEFD9413B17293AFDFE6EA6F1402A088DEDF104CB
|
||||||
|
for k in ctx.keylist(pattern = 'jrdemasi', secret = False, mode = klmodes['remote'], source = None):
|
||||||
|
#pprint.pprint(inspect.getmembers(k))
|
||||||
|
tkey2 = k
|
||||||
|
#print(tkey2.fpr)
|
||||||
|
|
||||||
|
# Test fetching from a keyserver - we'll grab the last key from the above iteration
|
||||||
|
try:
|
||||||
|
ctx.op_import_keys([tkey2])
|
||||||
|
except gpg.errors.GPGMEError:
|
||||||
|
pass # key isn't on the keyserver, or it isn't accessible, etc.
|
||||||
|
|
||||||
|
# Test signing
|
||||||
|
ctx.key_tofu_policy(tkey2, gpg.constants.TOFU_POLICY_ASK)
|
||||||
|
ctx.signers = [mkey]
|
||||||
|
days_valid = 4
|
||||||
|
exptime = 4 * 24 * 60 * 60
|
||||||
|
ctx.key_sign(tkey2, expires_in = exptime, local = True)
|
||||||
|
|
||||||
|
# https://www.apt-browse.org/browse/debian/wheezy/main/amd64/python-pyme/1:0.8.1-2/file/usr/share/doc/python-pyme/examples/t-edit.py
|
||||||
|
# https://searchcode.com/codesearch/view/20535820/
|
||||||
|
# https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=0be55f4d64178a5636cbe9f12f63c6f9853f3aa2;hb=refs/heads/master
|
||||||
|
class KeyEditor(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.replied_once = False
|
||||||
|
trust = '3' # this is the level of trust... in this case, marginal.
|
||||||
|
rcptemail = 'test@test.com'
|
||||||
|
# we exclude 'help'
|
||||||
|
self.kprmpt = ['trust', 'fpr', 'sign', 'tsign', 'lsign', 'nrsign', 'grip', 'list',
|
||||||
|
'uid', 'key', 'check', 'deluid', 'delkey', 'delsig', 'pref', 'showpref',
|
||||||
|
'revsig', 'enable', 'disable', 'showphoto', 'clean', 'minimize', 'save',
|
||||||
|
'quit']
|
||||||
|
self.prmpt = {'edit_ownertrust': {'value': trust,
|
||||||
|
'set_ultimate': {'okay': 'yes'}},
|
||||||
|
'untrusted_key': {'override': 'yes'},
|
||||||
|
'pklist': {'user_id': {'enter': rcptemail}},
|
||||||
|
'keyedit': {'prompt': 'trust', # the mode we initiate.
|
||||||
|
'save': {'okay': 'yes'}}}
|
||||||
|
|
||||||
|
def edit_fnc(self, status, args, out):
|
||||||
|
result = None
|
||||||
|
out.seek(0, 0)
|
||||||
|
#print(status, args)
|
||||||
|
#print(out.read().decode('utf-8'))
|
||||||
|
#print('{0} ({1})'.format(status, args))
|
||||||
|
def mapDict(m, d):
|
||||||
|
return(reduce(operator.getitem, m, d))
|
||||||
|
if args == 'keyedit.prompt' and self.replied_once:
|
||||||
|
result = 'quit'
|
||||||
|
elif status == 'KEY_CONSIDERED':
|
||||||
|
result = None
|
||||||
|
self.replied_once = False
|
||||||
|
elif status == 'GET_LINE':
|
||||||
|
#print('DEBUG: looking up mapping...')
|
||||||
|
self.replied_once = True
|
||||||
|
_ilist = args.split('.')
|
||||||
|
result = mapDict(_ilist, self.prmpt)
|
||||||
|
if not result:
|
||||||
|
result = None
|
||||||
|
return(result)
|
||||||
|
|
||||||
|
# Test setting trust
|
||||||
|
out = gpg.Data()
|
||||||
|
ctx.interact(tkey2, KeyEditor().edit_fnc, sink = out, fnc_value = out)
|
||||||
|
out.seek(0, 0)
|
||||||
|
#print(out.read(), end = '\n\n')
|
||||||
|
|
||||||
|
#Test sending to a keyserver
|
||||||
|
buf = gpg.Data()
|
||||||
|
ctx.op_export(tkey2.fpr, gpg.constants.EXPORT_MODE_EXTERN, None)
|
||||||
|
|
||||||
|
# Test writing the pubkey out to a file
|
||||||
|
buf = gpg.Data()
|
||||||
|
ctx.op_export_keys([tkey2], 0, buf) # do i NEED to specify a mode?
|
||||||
|
buf.seek(0, 0)
|
||||||
|
with open('/tmp/pubkeytest.gpg', 'wb') as f:
|
||||||
|
f.write(buf.read())
|
||||||
|
#del(buf)
|
||||||
|
# Let's also test writing out the ascii-armored..
|
||||||
|
ctx.armor = True
|
||||||
|
#buf = gpg.Data()
|
||||||
|
buf.seek(0, 0)
|
||||||
|
ctx.op_export_keys([tkey2], 0, buf) # do i NEED to specify a mode?
|
||||||
|
buf.seek(0, 0)
|
||||||
|
#print(buf.read())
|
||||||
|
#buf.seek(0, 0)
|
||||||
|
with open('/tmp/pubkeytest.asc', 'wb') as f:
|
||||||
|
f.write(buf.read())
|
||||||
|
del(buf)
|
||||||
|
|
||||||
|
# And lastly, let's test msmtprc
|
||||||
|
def getCfg(fname):
|
||||||
|
cfg = {'default': None, 'defaults': {}}
|
||||||
|
_defaults = False
|
||||||
|
_acct = None
|
||||||
|
with open(fname, 'r') as f:
|
||||||
|
cfg_raw = f.read()
|
||||||
|
for l in cfg_raw.splitlines():
|
||||||
|
if re.match('^\s?(#.*|)$', l):
|
||||||
|
continue # skip over blank and commented lines
|
||||||
|
line = [i.strip() for i in re.split('\s+', l.strip(), maxsplit = 1)]
|
||||||
|
if line[0] == 'account':
|
||||||
|
if re.match('^default\s?:\s?', line[1]): # it's the default account specifier
|
||||||
|
cfg['default'] = line[1].split(':', maxsplit = 1)[1].strip()
|
||||||
|
else:
|
||||||
|
if line[1] not in cfg.keys(): # it's a new account definition
|
||||||
|
cfg[line[1]] = {}
|
||||||
|
_acct = line[1]
|
||||||
|
_defaults = False
|
||||||
|
elif line[0] == 'defaults': # it's the defaults
|
||||||
|
_acct = 'defaults'
|
||||||
|
else: # it's a config directive
|
||||||
|
cfg[_acct][line[0]] = line[1]
|
||||||
|
for a in list(cfg):
|
||||||
|
if a != 'default':
|
||||||
|
for k, v in cfg['defaults'].items():
|
||||||
|
if k not in cfg[a].keys():
|
||||||
|
cfg[a][k] = v
|
||||||
|
del(cfg['defaults'])
|
||||||
|
return(cfg)
|
||||||
|
homeconf = os.path.join(os.environ['HOME'], '.msmtprc')
|
||||||
|
sysconf = '/etc/msmtprc'
|
||||||
|
msmtp = {'path': None}
|
||||||
|
if not os.path.isfile(homeconf):
|
||||||
|
if not os.path.isfile(sysconf):
|
||||||
|
msmtp['conf'] = False
|
||||||
|
else:
|
||||||
|
msmtp['conf'] = sysconf
|
||||||
|
else:
|
||||||
|
msmtp['conf'] = homeconf
|
||||||
|
if os.path.isfile(msmtp['conf']):
|
||||||
|
path = os.environ['PATH']
|
||||||
|
for p in path.split(':'):
|
||||||
|
fullpath = os.path.join(p, 'msmtp')
|
||||||
|
if os.path.isfile(fullpath):
|
||||||
|
msmtp['path'] = fullpath
|
||||||
|
break # break out the first instance of it we find since the shell parses PATH first to last and so do we
|
||||||
|
if msmtp['path']:
|
||||||
|
# Okay. So we have a config file, which we're assuming to be set up correctly, and a path to a binary.
|
||||||
|
# Now we need to parse the config.
|
||||||
|
msmtp['cfg'] = getCfg(msmtp['conf'])
|
||||||
|
pprint.pprint(msmtp)
|
||||||
|
if msmtp['path']:
|
||||||
|
# Get the appropriate MSMTP profile
|
||||||
|
profile = msmtp['cfg']['default']
|
||||||
|
# Buuuut i use a different profile when i test, because i use msmtp for production-type stuff.
|
||||||
|
#if os.environ['USER'] == 'bts':
|
||||||
|
# profile = 'gmailtesting'
|
||||||
|
# Now we can try to send an email... yikes.
|
||||||
|
## First we set up the message templates.
|
||||||
|
body_in = {'plain': None, 'html': None}
|
||||||
|
body_in['plain'] = """Hello, person!
|
||||||
|
|
||||||
|
This is a test message.
|
||||||
|
|
||||||
|
Thanks."""
|
||||||
|
body_in['html'] = """\
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<p><b>Hi there, person!</b> This is a test email.</p>
|
||||||
|
<p>It supports fun things like HTML.</p>
|
||||||
|
<p>--<br><a href='https://games.square-r00t.net/'>https://games.square-r00t.net</a><br>
|
||||||
|
Admin: <a href='mailto:bts@square-r00t.net'>r00t^2</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
# Now, some attachments.
|
||||||
|
part = {}
|
||||||
|
ctx.armor = False
|
||||||
|
buf = gpg.Data()
|
||||||
|
ctx.op_export_keys([tkey2], 0, buf)
|
||||||
|
buf.seek(0, 0)
|
||||||
|
part['gpg'] = MIMEApplication(buf.read(), '{0}.gpg'.format(tkey2.fpr))
|
||||||
|
part['gpg']['Content-Disposition'] = 'attachment; filename="{0}.gpg"'.format(tkey2.fpr)
|
||||||
|
ctx.armor = True
|
||||||
|
buf.seek(0, 0)
|
||||||
|
ctx.op_export_keys([tkey2], 0, buf)
|
||||||
|
buf.seek(0, 0)
|
||||||
|
part['asc'] = MIMEApplication(buf.read(), '{0}.asc'.format(tkey2.fpr))
|
||||||
|
part['asc']['Content-Disposition'] = 'attachment; filename="{0}.asc"'.format(tkey2.fpr)
|
||||||
|
#msg = MIMEMultipart('alternative')
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
msg['preamble'] = 'This is a multi-part message in MIME format.\n'
|
||||||
|
msg['From'] = msmtp['cfg'][profile]['from']
|
||||||
|
msg['To'] = msmtp['cfg'][profile]['from'] # to send to more than one: ', '.join(somelist)
|
||||||
|
msg['Date'] = datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
|
||||||
|
msg['Subject'] = 'TEST EMAIL VIA TEST.PY'
|
||||||
|
msg['epilogue'] = ''
|
||||||
|
body = MIMEMultipart('alternative')
|
||||||
|
body.attach(MIMEText(body_in['plain'], 'plain'))
|
||||||
|
body.attach(MIMEText(body_in['html'], 'html'))
|
||||||
|
msg.attach(body)
|
||||||
|
for f in part.keys():
|
||||||
|
msg.attach(part[f])
|
||||||
|
|
||||||
|
# This needs way more parsing to support things like plain ol' port 25 plaintext (ugh), etc.
|
||||||
|
if 'tls-starttls' in msmtp['cfg'][profile].keys() and msmtp['cfg'][profile]['tls-starttls'] == 'on':
|
||||||
|
smtpserver = smtplib.SMTP(msmtp['cfg'][profile]['host'], int(msmtp['cfg'][profile]['port']))
|
||||||
|
smtpserver.ehlo()
|
||||||
|
smtpserver.starttls()
|
||||||
|
# we need to EHLO again after a STARTTLS because email is weird.
|
||||||
|
elif msmtp['cfg'][profile]['tls'] == 'on':
|
||||||
|
smtpserver = smtplib.SMTP_SSL(msmtp['cfg'][profile]['host'], int(msmtp['cfg'][profile]['port']))
|
||||||
|
smtpserver.ehlo()
|
||||||
|
smtpserver.login(msmtp['cfg'][profile]['user'], msmtp['cfg'][profile]['password'])
|
||||||
|
smtpserver.sendmail(msmtp['cfg'][profile]['user'], msg['To'], msg.as_string())
|
||||||
|
smtpserver.close()
|
5
gpg/kant/testbatch.kant.csv
Normal file
5
gpg/kant/testbatch.kant.csv
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
748231EBCBD808A14F5E85D28C004C2F93481F6B,4,1,3,1
|
||||||
|
A03CACFD7123AF443A3A185298A8A46921C8DDEF,-1,0,0,0
|
||||||
|
EFD9413B17293AFDFE6EA6F1402A088DEDF104CB,full,true,casual,yes
|
||||||
|
6FA8AE12AEC90B035EEE444FE70457341A63E830,2,True,Casual,True
|
||||||
|
<admin@sysadministrivia.com>, full, yes, careful, false
|
|
285
gpg/keystats.py
Executable file
285
gpg/keystats.py
Executable file
@ -0,0 +1,285 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Get various information about an SKS keyserver from its status page
|
||||||
|
# without opening a browser.
|
||||||
|
# Requires BeautifulSoup4 and (optional but recommended) the lxml module.
|
||||||
|
|
||||||
|
# stdlib
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
from urllib.request import urlopen, urlparse
|
||||||
|
# pypi/pip
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
try:
|
||||||
|
import lxml
|
||||||
|
bs_parser = 'lxml'
|
||||||
|
except ImportError:
|
||||||
|
bs_parser = 'html.parser'
|
||||||
|
|
||||||
|
socket_orig = socket.getaddrinfo
|
||||||
|
|
||||||
|
def ForceProtov4(host, port, family = 0, socktype = 0, proto = 0,
|
||||||
|
flags = 0):
|
||||||
|
return(socket_orig(host, port, socket.AF_INET, socktype, proto, flags))
|
||||||
|
|
||||||
|
def ForceProtov6(host, port, family = 0, socktype = 0, proto = 0,
|
||||||
|
flags = 0):
|
||||||
|
return(socket_orig(host, port, socket.AF_INET6, socktype, proto, flags))
|
||||||
|
|
||||||
|
class KeyStats(object):
|
||||||
|
def __init__(self, server, port = None, tls = True, netproto = None,
|
||||||
|
proto = 'http', output = 'py', verbose = True):
|
||||||
|
self.stats = {'server': {},
|
||||||
|
'keys': 0}
|
||||||
|
if verbose:
|
||||||
|
self.stats['peers'] = {}
|
||||||
|
self.stats['histograms'] = {}
|
||||||
|
# Currently I only support scraping the stats page of the keyserver.
|
||||||
|
# TODO: Can I do this directly via HKP/HKPS? Is there a python module
|
||||||
|
# for it?
|
||||||
|
self.port_dflts = {'http': {True: 443,
|
||||||
|
False: 80,
|
||||||
|
None: 80}}
|
||||||
|
self.server = server
|
||||||
|
self.tls = tls
|
||||||
|
self.netproto = netproto
|
||||||
|
# We need to do some... ugly, hacky stuff to *force* a particular
|
||||||
|
# network stack (IPv4 vs. IPv6).
|
||||||
|
# https://stackoverflow.com/a/6319043/733214
|
||||||
|
if self.netproto:
|
||||||
|
if self.netproto == 'ipv6':
|
||||||
|
socket.getaddrinfo = ForceProtov6
|
||||||
|
elif self.netproto == 'ipv4':
|
||||||
|
socket.getaddrinfo = ForceProtov4
|
||||||
|
self.verbose = verbose
|
||||||
|
self.output = output
|
||||||
|
self.proto = proto.lower()
|
||||||
|
# TODO: would need to add add'l protocol support here.
|
||||||
|
if self.proto in ('http', 'https'):
|
||||||
|
self.proto = 'http'
|
||||||
|
if not port:
|
||||||
|
self.port = self.port_dflts[self.proto][self.tls]
|
||||||
|
else:
|
||||||
|
self.port = int(port)
|
||||||
|
if self.proto == 'http':
|
||||||
|
self.getStatsPage()
|
||||||
|
|
||||||
|
def getStatsPage(self):
|
||||||
|
if self.proto is not 'http':
|
||||||
|
# Something went wrong; this function shouldn't be used for
|
||||||
|
# non-http.
|
||||||
|
return()
|
||||||
|
_str_map = {'Hostname': 'name',
|
||||||
|
'Nodename': 'hostname',
|
||||||
|
'Version': 'version',
|
||||||
|
'Server contact': 'contact',
|
||||||
|
'HTTP port': 'hkp_port',
|
||||||
|
'Recon port': 'recon_port',
|
||||||
|
'Debug level': 'debug'}
|
||||||
|
_uri = 'pks/lookup?op=stats'
|
||||||
|
_url = '{0}://{1}:{2}/{3}'.format(('https' if self.tls else 'http'),
|
||||||
|
self.server,
|
||||||
|
self.port,
|
||||||
|
_uri)
|
||||||
|
with urlopen(_url) as u:
|
||||||
|
_webdata = u.read()
|
||||||
|
_soup = BeautifulSoup(_webdata, bs_parser)
|
||||||
|
for e in _soup.find_all('h2'):
|
||||||
|
# General server info
|
||||||
|
if e.text == 'Settings':
|
||||||
|
t = e.find_next('table',
|
||||||
|
attrs = {'summary': 'Keyserver Settings'})
|
||||||
|
for r in t.find_all('tr'):
|
||||||
|
h = None
|
||||||
|
row = [re.sub(':$', '',
|
||||||
|
i.text.strip()) for i in r.find_all('td')]
|
||||||
|
h = row[0]
|
||||||
|
if h in _str_map.keys():
|
||||||
|
if _str_map[h] in ('debug', 'hkp_port', 'recon_port'):
|
||||||
|
self.stats['server'][_str_map[h]] = int(row[1])
|
||||||
|
elif _str_map[h] == 'version':
|
||||||
|
self.stats['server'][_str_map[h]] = tuple(
|
||||||
|
row[1].split('.'))
|
||||||
|
else:
|
||||||
|
self.stats['server'][_str_map[h]] = row[1]
|
||||||
|
# "Gossip" (recon) peers list
|
||||||
|
elif e.text == 'Gossip Peers' and self.verbose:
|
||||||
|
self.stats['peers']['recon'] = []
|
||||||
|
t = e.find_next('table',
|
||||||
|
attrs = {'summary': 'Gossip Peers'})
|
||||||
|
for r in t.find_all('tr'):
|
||||||
|
_peer = list(r.children)[0].text.split()
|
||||||
|
# A tuple consisting of host/name, port.
|
||||||
|
self.stats['peers']['recon'].append((_peer[0],
|
||||||
|
int(_peer[1])))
|
||||||
|
# Mailsync peers list
|
||||||
|
elif e.text == 'Outgoing Mailsync Peers' and self.verbose:
|
||||||
|
self.stats['peers']['mailsync'] = []
|
||||||
|
t = e.find_next('table', attrs = {'summary': 'Mailsync Peers'})
|
||||||
|
for r in t.find_all('tr'):
|
||||||
|
_address = list(r.children)[0].text.strip()
|
||||||
|
self.stats['peers']['mailsync'].append(_address)
|
||||||
|
# Number of keys
|
||||||
|
elif e.text == 'Statistics':
|
||||||
|
self.stats['keys'] = int(e.find_next('p').text.split()[-1])
|
||||||
|
# Histograms
|
||||||
|
for e in _soup.find_all('h3'):
|
||||||
|
# Dailies
|
||||||
|
if e.text == 'Daily Histogram' and self.verbose:
|
||||||
|
_dfmt = '%Y-%m-%d'
|
||||||
|
t = e.find_next('table', attrs = {'summary': 'Statistics'})
|
||||||
|
for r in t.find_all('tr'):
|
||||||
|
row = [i.text.strip() for i in r.find_all('td')]
|
||||||
|
if row[0] == 'Time':
|
||||||
|
continue
|
||||||
|
_date = datetime.datetime.strptime(row[0], _dfmt)
|
||||||
|
_new = int(row[1])
|
||||||
|
_updated = int(row[2])
|
||||||
|
# JSON can't convert datetime objects to strings
|
||||||
|
# automatically like PyYAML can.
|
||||||
|
if self.output == 'json':
|
||||||
|
k = str(_date)
|
||||||
|
else:
|
||||||
|
k = _date
|
||||||
|
self.stats['histograms'][k] = {'total': {'new': _new,
|
||||||
|
'updated': \
|
||||||
|
_updated},
|
||||||
|
'hourly': {}}
|
||||||
|
# Hourlies
|
||||||
|
elif e.text == 'Hourly Histogram' and self.verbose:
|
||||||
|
_dfmt = '%Y-%m-%d %H'
|
||||||
|
t = e.find_next('table', attrs = {'summary': 'Statistics'})
|
||||||
|
for r in t.find_all('tr'):
|
||||||
|
row = [i.text.strip() for i in r.find_all('td')]
|
||||||
|
if row[0] == 'Time':
|
||||||
|
continue
|
||||||
|
_date = datetime.datetime.strptime(row[0], _dfmt)
|
||||||
|
_new = int(row[1])
|
||||||
|
_updated = int(row[2])
|
||||||
|
_day = datetime.datetime(year = _date.year,
|
||||||
|
month = _date.month,
|
||||||
|
day = _date.day)
|
||||||
|
if self.output == 'json':
|
||||||
|
k1 = str(_day)
|
||||||
|
k2 = str(_date)
|
||||||
|
else:
|
||||||
|
k1 = _day
|
||||||
|
k2 = _date
|
||||||
|
self.stats['histograms'][k1]['hourly'][k2] = {'new': _new,
|
||||||
|
'updated': \
|
||||||
|
_updated}
|
||||||
|
return()
|
||||||
|
|
||||||
|
def print(self):
|
||||||
|
if self.output == 'json':
|
||||||
|
import json
|
||||||
|
print(json.dumps(self.stats,
|
||||||
|
#indent = 4,
|
||||||
|
default = str))
|
||||||
|
elif self.output == 'yaml':
|
||||||
|
has_yaml = False
|
||||||
|
if 'YAML_MOD' in os.environ.keys():
|
||||||
|
_mod = os.environ['YAML_MOD']
|
||||||
|
try:
|
||||||
|
import importlib
|
||||||
|
yaml = importlib.import_module(_mod)
|
||||||
|
has_yaml = True
|
||||||
|
except (ImportError, ModuleNotFoundError):
|
||||||
|
raise RuntimeError(('Module "{0}" is not ' +
|
||||||
|
'installed').format(_mod))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
has_yaml = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
import pyaml as yaml
|
||||||
|
has_yaml = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
if not has_yaml:
|
||||||
|
raise RuntimeError(('You must have the PyYAML or pyaml ' +
|
||||||
|
'module installed to use YAML ' +
|
||||||
|
'formatting'))
|
||||||
|
print(yaml.dump(self.stats))
|
||||||
|
elif self.output == 'py':
|
||||||
|
import pprint
|
||||||
|
pprint.pprint(self.stats)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
args.add_argument('-i', '--insecure',
|
||||||
|
dest = 'tls',
|
||||||
|
action = 'store_false',
|
||||||
|
help = ('If specified, do not use TLS encryption ' +
|
||||||
|
'querying the server (default is to use TLS)'))
|
||||||
|
args.add_argument('-P', '--port',
|
||||||
|
dest = 'port',
|
||||||
|
type = int,
|
||||||
|
default = None,
|
||||||
|
help = ('The port number to use. If not specified, ' +
|
||||||
|
'use the default port per the normal protocol ' +
|
||||||
|
'(i.e. for HTTPS, use 443)'))
|
||||||
|
fmt = args.add_mutually_exclusive_group()
|
||||||
|
fmt.add_argument('-j', '--json',
|
||||||
|
default = 'py',
|
||||||
|
dest = 'output',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'json',
|
||||||
|
help = ('Output the data in JSON format'))
|
||||||
|
fmt.add_argument('-y', '--yaml',
|
||||||
|
default = 'py',
|
||||||
|
dest = 'output',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'yaml',
|
||||||
|
help = ('Output the data in YAML format (requires ' +
|
||||||
|
'PyYAML or pyaml module). You can prefer which ' +
|
||||||
|
'one by setting an environment variable, ' +
|
||||||
|
'YAML_MOD, to "yaml" or "pyaml" (for PyYAML or ' +
|
||||||
|
'pyaml respectively); otherwise preference ' +
|
||||||
|
'will be PyYAML > pyaml'))
|
||||||
|
fmt.add_argument('-p', '--python',
|
||||||
|
default = 'py',
|
||||||
|
dest = 'output',
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'py',
|
||||||
|
help = ('Output the data in pythonic format (default)'))
|
||||||
|
args.add_argument('-v', '--verbose',
|
||||||
|
dest = 'verbose',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, print out ALL info (peers, ' +
|
||||||
|
'histogram, etc.), not just the settings/' +
|
||||||
|
'number of keys/contact info/server info'))
|
||||||
|
proto_grp = args.add_mutually_exclusive_group()
|
||||||
|
proto_grp.add_argument('-4', '--ipv4',
|
||||||
|
dest = 'netproto',
|
||||||
|
default = None,
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'ipv4',
|
||||||
|
help = ('If specified, force IPv4 (default is ' +
|
||||||
|
'system\'s preference)'))
|
||||||
|
proto_grp.add_argument('-6', '--ipv6',
|
||||||
|
dest = 'netproto',
|
||||||
|
default = None,
|
||||||
|
action = 'store_const',
|
||||||
|
const = 'ipv6',
|
||||||
|
help = ('If specified, force IPv6 (default is ' +
|
||||||
|
'system\'s preference)'))
|
||||||
|
args.add_argument('server',
|
||||||
|
help = ('The keyserver ((sub)domain, IP address, etc.)'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
import pprint
|
||||||
|
#pprint.pprint(args)
|
||||||
|
ks = KeyStats(**args)
|
||||||
|
ks.print()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
402
gpg/sksdump.py
402
gpg/sksdump.py
@ -1,21 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# NOTE: This was written for systemd systems only.
|
|
||||||
# Tweaking would be needed for non-systemd systems
|
|
||||||
# (since every non-systemd uses their own init system callables...)
|
|
||||||
#
|
|
||||||
# Thanks to Matt Rude and https://gist.github.com/mattrude/b0ac735d07b0031bb002 so I can know what the hell I'm doing.
|
# Thanks to Matt Rude and https://gist.github.com/mattrude/b0ac735d07b0031bb002 so I can know what the hell I'm doing.
|
||||||
#
|
|
||||||
# IMPORTANT: This script uses certaion permissions functions that require some forethought. You can either run as root,
|
|
||||||
# which is the "easy" way, OR you can run as the sks user. Has to be one or the other; you'll SERIOUSLY mess things up
|
|
||||||
# otherwise. If you run as the sks user, MAKE SURE the following is set in your sudoers (where SKSUSER is the username sks runs as:
|
|
||||||
# Cmnd_Alias SKSCMDS = /usr/bin/systemctl start sks-db,\
|
|
||||||
# /usr/bin/systemctl stop sks-db,\
|
|
||||||
# /usr/bin/systemctl start sks-recon,\
|
|
||||||
# /usr/bin/systemctl stop sks-recon
|
|
||||||
# SKSUSER ALL = NOPASSWD: SKSCMDS
|
|
||||||
|
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import configparser
|
||||||
import datetime
|
import datetime
|
||||||
import getpass
|
import getpass
|
||||||
import os
|
import os
|
||||||
@ -26,138 +15,361 @@ from grp import getgrnam
|
|||||||
NOW = datetime.datetime.utcnow()
|
NOW = datetime.datetime.utcnow()
|
||||||
NOWstr = NOW.strftime('%Y-%m-%d')
|
NOWstr = NOW.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
sks = {
|
# TODO:
|
||||||
# chowning - MAKE SURE THIS IS THE USER SKS RUNS AS.
|
# - cleanup/rotation should be optional
|
||||||
'user': 'sks',
|
# - turn into a class so we can more easily share vars across functions
|
||||||
# chowning
|
# - also, create the "CURRENT" symlink *AFTER* the dump completes?
|
||||||
'group': 'sks',
|
|
||||||
# Where your SKS DB is
|
|
||||||
'basedir': '/var/lib/sks',
|
|
||||||
# Where the dumps should go. This dir is scrubbed based on mtime, so ONLY use this dir for dumps.
|
|
||||||
'destdir': '/srv/http/sks/dumps',
|
|
||||||
# If None, don't compress dumps. If one of: 'xz', 'gz', 'bz2', or 'lrz' (for lrzip) then use that compression algo.
|
|
||||||
'compress': 'lrz',
|
|
||||||
# The service name(s) to stop for the dump and to start again afterwards.
|
|
||||||
'svcs': ['sks-db', 'sks-recon'],
|
|
||||||
# I would hope this is self-explanatory. If not, this is where we log the outout of the sks dump process. (and any rsync errors, too)
|
|
||||||
'logfile': '/var/log/sksdump.log',
|
|
||||||
# If not None value, where we should push the dumps when done. Can be a local path too, obviously.
|
|
||||||
'rsync': 'root@sks.mirror.square-r00t.net:/srv/http/sks/dumps/.',
|
|
||||||
# How many previous days of dumps should we keep?
|
|
||||||
'days': 1,
|
|
||||||
# How many keys to include per dump file
|
|
||||||
'dumpkeys': 15000
|
|
||||||
}
|
|
||||||
|
|
||||||
|
cfgfile = os.path.join(os.environ['HOME'], '.config', 'optools', 'sksdump.ini')
|
||||||
|
|
||||||
# symlinks? relative path? HOME reference? WE HANDLE IT ALL.
|
def getDefaults():
|
||||||
sks['destdir'] = os.path.realpath(os.path.abspath(os.path.expanduser(sks['destdir'])))
|
# Hardcoded defaults
|
||||||
|
dflt = {'system': {'user': 'sks',
|
||||||
|
'group': 'sks',
|
||||||
|
'compress': 'xz',
|
||||||
|
'svcs': ['sks-db', 'sks-recon'],
|
||||||
|
'logfile': '/var/log/sksdump.log',
|
||||||
|
'days': 1,
|
||||||
|
'dumpkeys': 15000},
|
||||||
|
'sync': {'throttle': 0},
|
||||||
|
'paths': {'basedir': '/var/lib/sks',
|
||||||
|
'destdir': '/srv/http/sks/dumps',
|
||||||
|
'rsync': ('root@mirror.square-r00t.net:' +
|
||||||
|
'/srv/http/sks/dumps'),
|
||||||
|
'sksbin': '/usr/bin/sks'},
|
||||||
|
'runtime': {'nodump': None, 'nocompress': None, 'nosync': None}}
|
||||||
|
## Build out the default .ini.
|
||||||
|
dflt_b64 = ("""IyBJTVBPUlRBTlQ6IFRoaXMgc2NyaXB0IHVzZXMgY2VydGFpbiBwZXJtaXNz
|
||||||
|
aW9ucyBmdW5jdGlvbnMgdGhhdCByZXF1aXJlIHNvbWUKIyBmb3JldGhvdWdo
|
||||||
|
dC4KIyBZb3UgY2FuIGVpdGhlciBydW4gYXMgcm9vdCwgd2hpY2ggaXMgdGhl
|
||||||
|
ICJlYXN5IiB3YXksIE9SIHlvdSBjYW4gcnVuIGFzIHRoZQojIHNrcyB1c2Vy
|
||||||
|
IChvci4uLiB3aGF0ZXZlciB1c2VyIHlvdXIgU0tTIGluc3RhbmNlIHJ1bnMg
|
||||||
|
YXMpLgojIEl0IGhhcyB0byBiZSBvbmUgb3IgdGhlIG90aGVyOyB5b3UnbGwg
|
||||||
|
U0VSSU9VU0xZIG1lc3MgdGhpbmdzIHVwIG90aGVyd2lzZS4KIyBJZiB5b3Ug
|
||||||
|
cnVuIGFzIHRoZSBza3MgdXNlciwgTUFLRSBTVVJFIHRoZSBmb2xsb3dpbmcg
|
||||||
|
aXMgc2V0IGluIHlvdXIgc3Vkb2VycwojICh3aGVyZSBTS1NVU0VSIGlzIHRo
|
||||||
|
ZSB1c2VybmFtZSBza3MgcnVucyBhcyk6CiMJQ21uZF9BbGlhcyBTS1NDTURT
|
||||||
|
ID0gL3Vzci9iaW4vc3lzdGVtY3RsIHN0YXJ0IHNrcy1kYixcCiMJICAgICAg
|
||||||
|
ICAgICAgICAgICAgICAgL3Vzci9iaW4vc3lzdGVtY3RsIHN0b3Agc2tzLWRi
|
||||||
|
LFwKIyAgICAgICAgICAgICAgICAgICAgICAgIC91c3IvYmluL3N5c3RlbWN0
|
||||||
|
bCBzdGFydCBza3MtcmVjb24sXAojCQkgICAgICAgICAgICAgICAgIC91c3Iv
|
||||||
|
YmluL3N5c3RlbWN0bCBzdG9wIHNrcy1yZWNvbgojCVNLU1VTRVIgQUxMID0g
|
||||||
|
Tk9QQVNTV0Q6IFNLU0NNRFMKCiMgVGhpcyB3YXMgd3JpdHRlbiBmb3Igc3lz
|
||||||
|
dGVtZCBzeXN0ZW1zIG9ubHkuIFR3ZWFraW5nIHdvdWxkIGJlIG5lZWRlZCBm
|
||||||
|
b3IKIyBub24tc3lzdGVtZCBzeXN0ZW1zIChzaW5jZSBldmVyeSBub24tc3lz
|
||||||
|
dGVtZCB1c2VzIHRoZWlyIG93biBpbml0IHN5c3RlbQojIGNhbGxhYmxlcy4u
|
||||||
|
LikKCiMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMj
|
||||||
|
IyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMKCiMgVGhp
|
||||||
|
cyBzZWN0aW9uIGNvbnRyb2xzIHZhcmlvdXMgc3lzdGVtIGNvbmZpZ3VyYXRp
|
||||||
|
b24uCltzeXN0ZW1dCgojIFRoaXMgc2hvdWxkIGJlIHRoZSB1c2VyIFNLUyBy
|
||||||
|
dW5zIGFzLgp1c2VyID0gc2tzCgojIFRoaXMgaXMgdGhlIGdyb3VwIHRoYXQg
|
||||||
|
U0tTIHJ1bnMgYXMuCmdyb3VwID0gc2tzCgojIElmIGVtcHR5LCBkb24ndCBj
|
||||||
|
b21wcmVzcyBkdW1wcy4KIyBJZiBvbmUgb2Y6IHh6LCBneiwgYnoyLCBvciBs
|
||||||
|
cnogKGZvciBscnppcCkgdGhlbiB1c2UgdGhhdCBjb21wcmVzc2lvbiBhbGdv
|
||||||
|
LgojIE5vdGUgdGhhdCBscnppcCByZXF1aXJlcyBleHRyYSBpbnN0YWxsYXRp
|
||||||
|
b24uCmNvbXByZXNzID0geHoKCiMgVGhlc2Ugc2VydmljZXMgd2lsbCBiZSBz
|
||||||
|
dG9wcGVkL3N0YXJ0ZWQsIGluIG9yZGVyLCBiZWZvcmUvYWZ0ZXIgZHVtcHMu
|
||||||
|
IElmIG1vcmUKIyB0aGFuIG9uZSwgc2VwZXJhdGUgYnkgY29tbWFzLgpzdmNz
|
||||||
|
ID0gc2tzLWRiLHNrcy1yZWNvbgoKIyBUaGUgcGF0aCB0byB0aGUgbG9nZmls
|
||||||
|
ZS4KbG9nZmlsZSA9IC92YXIvbG9nL3Nrc2R1bXAubG9nCgojIFRoZSBudW1i
|
||||||
|
ZXIgb2YgZGF5cyBvZiByb3RhdGVkIGtleSBkdW1wcy4gSWYgZW1wdHksIGRv
|
||||||
|
bid0IHJvdGF0ZS4KZGF5cyA9IDEKCiMgSG93IG1hbnkga2V5cyB0byBpbmNs
|
||||||
|
dWRlIGluIGVhY2ggZHVtcCBmaWxlLgpkdW1wa2V5cyA9IDE1MDAwCgoKIyBU
|
||||||
|
aGlzIHNlY3Rpb24gY29udHJvbHMgc3luYyBzZXR0aW5ncy4KW3N5bmNdCgoj
|
||||||
|
IFRoaXMgc2V0dGluZyBpcyB3aGF0IHRoZSBzcGVlZCBzaG91bGQgYmUgdGhy
|
||||||
|
b3R0bGVkIHRvLCBpbiBLaUIvcy4gSWYgZW1wdHkgb3IKIyAwLCBwZXJmb3Jt
|
||||||
|
IG5vIHRocm90dGxpbmcuCnRocm90dGxlID0gMAoKCiMgVGhpcyBzZWN0aW9u
|
||||||
|
IGNvbnRyb2xzIHdoZXJlIHN0dWZmIGdvZXMgYW5kIHdoZXJlIHdlIHNob3Vs
|
||||||
|
ZCBmaW5kIGl0LgpbcGF0aHNdCgojIFdoZXJlIHlvdXIgU0tTIERCIGlzLgpi
|
||||||
|
YXNlZGlyID0gL3Zhci9saWIvc2tzCgojIFRoaXMgaXMgdGhlIGJhc2UgZGly
|
||||||
|
ZWN0b3J5IHdoZXJlIHRoZSBkdW1wcyBzaG91bGQgZ28uCiMgVGhlcmUgd2ls
|
||||||
|
bCBiZSBhIHN1Yi1kaXJlY3RvcnkgY3JlYXRlZCBmb3IgZWFjaCBkYXRlLgpk
|
||||||
|
ZXN0ZGlyID0gL3Nydi9odHRwL3Nrcy9kdW1wcwoKIyBUaGUgcGF0aCBmb3Ig
|
||||||
|
cnN5bmNpbmcgdGhlIGR1bXBzLiBJZiBlbXB0eSwgZG9uJ3QgcnN5bmMuCnJz
|
||||||
|
eW5jID0gcm9vdEBtaXJyb3Iuc3F1YXJlLXIwMHQubmV0Oi9zcnYvaHR0cC9z
|
||||||
|
a3MvZHVtcHMKCiMgVGhlIHBhdGggdG8gdGhlIHNrcyBiaW5hcnkgdG8gdXNl
|
||||||
|
Lgpza3NiaW4gPSAvdXNyL2Jpbi9za3MKCgojIFRoaXMgc2VjdGlvbiBjb250
|
||||||
|
cm9scyBydW50aW1lIG9wdGlvbnMuIFRoZXNlIGNhbiBiZSBvdmVycmlkZGVu
|
||||||
|
IGF0IHRoZQojIGNvbW1hbmRsaW5lLiBUaGV5IHRha2Ugbm8gdmFsdWVzOyB0
|
||||||
|
aGV5J3JlIG1lcmVseSBvcHRpb25zLgpbcnVudGltZV0KCiMgRG9uJ3QgZHVt
|
||||||
|
cCBhbnkga2V5cy4KIyBVc2VmdWwgZm9yIGRlZGljYXRlZCBpbi10cmFuc2l0
|
||||||
|
L3ByZXAgYm94ZXMuCjtub2R1bXAKCiMgRG9uJ3QgY29tcHJlc3MgdGhlIGR1
|
||||||
|
bXBzLCBldmVuIGlmIHdlIGhhdmUgYSBjb21wcmVzc2lvbiBzY2hlbWUgc3Bl
|
||||||
|
Y2lmaWVkIGluCiMgdGhlIFtzeXN0ZW06Y29tcHJlc3NdIHNlY3Rpb246ZGly
|
||||||
|
ZWN0aXZlLgo7bm9jb21wcmVzcwoKIyBEb24ndCBzeW5jIHRvIGFub3RoZXIg
|
||||||
|
c2VydmVyL3BhdGgsIGV2ZW4gaWYgb25lIGlzIHNwZWNpZmllZCBpbiBbcGF0
|
||||||
|
aHM6cnN5bmNdLgo7bm9zeW5j""")
|
||||||
|
realcfg = configparser.ConfigParser(defaults = dflt, allow_no_value = True)
|
||||||
|
if not os.path.isfile(cfgfile):
|
||||||
|
with open(cfgfile, 'w') as f:
|
||||||
|
f.write(base64.b64decode(dflt_b64).decode('utf-8'))
|
||||||
|
realcfg.read(cfgfile)
|
||||||
|
return(realcfg)
|
||||||
|
|
||||||
def svcMgmt(op):
|
def svcMgmt(op, args):
|
||||||
if op not in ('start', 'stop'):
|
if op not in ('start', 'stop'):
|
||||||
raise ValueError('Operation must be start or stop')
|
raise ValueError('Operation must be start or stop')
|
||||||
for svc in sks['svcs']:
|
for svc in args['svcs'].split(','):
|
||||||
cmd = ['/usr/bin/systemctl', op, svc]
|
cmd = ['/usr/bin/systemctl', op, svc.strip()]
|
||||||
if getpass.getuser() != 'root':
|
if getpass.getuser() != 'root':
|
||||||
cmd.insert(0, 'sudo')
|
cmd.insert(0, 'sudo')
|
||||||
subprocess.run(cmd)
|
subprocess.run(cmd)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def destPrep():
|
def destPrep(args):
|
||||||
nowdir = os.path.join(sks['destdir'], NOWstr)
|
nowdir = os.path.join(args['destdir'], NOWstr)
|
||||||
curdir = os.path.join(sks['destdir'], 'current')
|
curdir = os.path.join(args['destdir'], 'current')
|
||||||
PAST = NOW - datetime.timedelta(days = sks['days'])
|
PAST = NOW - datetime.timedelta(days = args['days'])
|
||||||
for thisdir, dirs, files in os.walk(sks['destdir']):
|
for thisdir, dirs, files in os.walk(args['destdir'], topdown = False):
|
||||||
for f in files:
|
for f in files:
|
||||||
fstat = os.stat(os.path.join(thisdir, f))
|
try: # we use a try here because if the link's broken, the script bails out.
|
||||||
mtime = fstat.st_mtime
|
fstat = os.stat(os.path.join(thisdir, f))
|
||||||
if int(mtime) < PAST.timestamp():
|
mtime = fstat.st_mtime
|
||||||
os.remove(os.path.join(thisdir, f))
|
if int(mtime) < PAST.timestamp():
|
||||||
try:
|
os.remove(os.path.join(thisdir, f))
|
||||||
os.removedirs(sks['destdir']) # Remove empty dirs
|
except FileNotFoundError: # broken symlink
|
||||||
except:
|
try:
|
||||||
pass # thisisfine.jpg
|
os.remove(os.path.join(thisdir, f))
|
||||||
|
except:
|
||||||
|
pass # just... ignore it. it's fine, whatever.
|
||||||
|
# Delete if empty dir
|
||||||
|
if os.path.isdir(thisdir):
|
||||||
|
if len(os.listdir(thisdir)) == 0:
|
||||||
|
os.rmdir(thisdir)
|
||||||
|
for d in dirs:
|
||||||
|
_dir = os.path.join(thisdir, d)
|
||||||
|
if os.path.isdir(_dir):
|
||||||
|
if len(os.listdir(_dir)) == 0:
|
||||||
|
try:
|
||||||
|
os.rmdir(os.path.join(thisdir, d))
|
||||||
|
except NotADirectoryError:
|
||||||
|
pass # in case it grabs the "current" symlink
|
||||||
|
#try:
|
||||||
|
# os.removedirs(sks['destdir']) # Remove empty dirs
|
||||||
|
#except:
|
||||||
|
# pass # thisisfine.jpg
|
||||||
os.makedirs(nowdir, exist_ok = True)
|
os.makedirs(nowdir, exist_ok = True)
|
||||||
if getpass.getuser() == 'root':
|
if getpass.getuser() == 'root':
|
||||||
uid = getpwnam(sks['user']).pw_uid
|
uid = getpwnam(args['user']).pw_uid
|
||||||
gid = getgrnam(sks['group']).gr_gid
|
gid = getgrnam(args['group']).gr_gid
|
||||||
for d in (sks['destdir'], nowdir):
|
# we COULD set it as part of the os.makedirs, but iirc it doesn't set
|
||||||
|
# it for existing dirs.
|
||||||
|
for d in (args['destdir'], nowdir):
|
||||||
os.chown(d, uid, gid)
|
os.chown(d, uid, gid)
|
||||||
if os.path.isdir(curdir):
|
if os.path.isdir(curdir):
|
||||||
os.remove(curdir)
|
os.remove(curdir)
|
||||||
os.symlink(NOWstr, curdir, target_is_directory = True)
|
try:
|
||||||
|
os.symlink(NOWstr, curdir, target_is_directory = True)
|
||||||
|
except FileExistsError:
|
||||||
|
pass # Ignore if it was set earlier
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def dumpDB():
|
def dumpDB(args):
|
||||||
destPrep()
|
destPrep(args)
|
||||||
os.chdir(sks['basedir'])
|
os.chdir(args['basedir'])
|
||||||
svcMgmt('stop')
|
svcMgmt('stop', args)
|
||||||
cmd = ['sks',
|
cmd = [args['sksbin'],
|
||||||
'dump',
|
'dump',
|
||||||
str(sks['dumpkeys']), # How many keys per dump?
|
str(args['dumpkeys']), # How many keys per dump?
|
||||||
os.path.join(sks['destdir'], NOWstr), # Where should it go?
|
os.path.join(args['destdir'], NOWstr), # Where should it go?
|
||||||
'keydump.{0}'.format(NOWstr)] # What the filename prefix should be
|
'keydump.{0}'.format(NOWstr)] # What the filename prefix should be
|
||||||
if getpass.getuser() == 'root':
|
if getpass.getuser() == 'root':
|
||||||
cmd2 = ['sudo', '-u', sks['user']]
|
cmd2 = ['sudo', '-u', args['user']]
|
||||||
cmd2.extend(cmd)
|
cmd2.extend(cmd)
|
||||||
cmd = cmd2
|
cmd = cmd2
|
||||||
with open(sks['logfile'], 'a') as f:
|
with open(args['logfile'], 'a') as f:
|
||||||
f.write('===== {0} =====\n'.format(str(datetime.datetime.utcnow())))
|
f.write('===== {0} =====\n'.format(str(datetime.datetime.utcnow())))
|
||||||
subprocess.run(cmd, stdout = f, stderr = f)
|
subprocess.run(cmd, stdout = f, stderr = f)
|
||||||
svcMgmt('start')
|
svcMgmt('start', args)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def compressDB():
|
def compressDB(args):
|
||||||
if not sks['compress']:
|
if not args['compress']:
|
||||||
return()
|
return()
|
||||||
curdir = os.path.join(sks['destdir'], NOWstr)
|
curdir = os.path.join(args['destdir'], NOWstr)
|
||||||
for thisdir, dirs, files in os.walk(curdir): # I use os.walk here because we might handle this differently in the future...
|
# I use os.walk here because we might handle this differently in the
|
||||||
|
# future...
|
||||||
|
for thisdir, dirs, files in os.walk(curdir):
|
||||||
|
files.sort()
|
||||||
for f in files:
|
for f in files:
|
||||||
fullpath = os.path.join(thisdir, f)
|
fullpath = os.path.join(thisdir, f)
|
||||||
newfile = '{0}.{1}'.format(fullpath, sks['compress'])
|
newfile = '{0}.{1}'.format(fullpath, args['compress'])
|
||||||
with open(sks['logfile'], 'a') as f:
|
# TODO: add compressed tarball support.
|
||||||
f.write('===== {0} Now compressing {1} =====\n'.format(str(datetime.datetime.utcnow()), fullpath))
|
# However, I can't do this on memory-constrained systems for lrzip.
|
||||||
if sks['compress'].lower() == 'gz':
|
# See: https://github.com/kata198/python-lrzip/issues/1
|
||||||
|
with open(args['logfile'], 'a') as f:
|
||||||
|
f.write('===== {0} Now compressing {1} =====\n'.format(
|
||||||
|
str(datetime.datetime.utcnow()),
|
||||||
|
fullpath))
|
||||||
|
if args['compress'].lower() == 'gz':
|
||||||
import gzip
|
import gzip
|
||||||
with open(fullpath, 'rb') as fh_in, gzip.open(newfile, 'wb') as fh_out:
|
with open(fullpath, 'rb') as fh_in, gzip.open(newfile,
|
||||||
|
'wb') as fh_out:
|
||||||
fh_out.writelines(fh_in)
|
fh_out.writelines(fh_in)
|
||||||
elif sks['compress'].lower() == 'xz':
|
elif args['compress'].lower() == 'xz':
|
||||||
import lzma
|
import lzma
|
||||||
with open(fullpath, 'rb') as fh_in, lzma.open(newfile, 'wb', preset = 9|lzma.PRESET_EXTREME) as fh_out:
|
with open(fullpath, 'rb') as fh_in, \
|
||||||
|
lzma.open(newfile,
|
||||||
|
'wb',
|
||||||
|
preset = 9|lzma.PRESET_EXTREME) as fh_out:
|
||||||
fh_out.writelines(fh_in)
|
fh_out.writelines(fh_in)
|
||||||
elif sks['compress'].lower() == 'bz2':
|
elif args['compress'].lower() == 'bz2':
|
||||||
import bz2
|
import bz2
|
||||||
with open(fullpath, 'rb') as fh_in, bz2.open(newfile, 'wb') as fh_out:
|
with open(fullpath, 'rb') as fh_in, bz2.open(newfile,
|
||||||
|
'wb') as fh_out:
|
||||||
fh_out.writelines(fh_in)
|
fh_out.writelines(fh_in)
|
||||||
elif sks['compress'].lower() == 'lrz':
|
elif args['compress'].lower() == 'lrz':
|
||||||
import lrzip
|
import lrzip
|
||||||
with open(fullpath, 'rb') as fh_in, open(newfile, 'wb') as fh_out:
|
with open(fullpath, 'rb') as fh_in, open(newfile,
|
||||||
|
'wb') as fh_out:
|
||||||
fh_out.write(lrzip.compress(fh_in.read()))
|
fh_out.write(lrzip.compress(fh_in.read()))
|
||||||
os.remove(fullpath)
|
os.remove(fullpath)
|
||||||
if getpass.getuser() == 'root':
|
if getpass.getuser() == 'root':
|
||||||
uid = getpwnam(sks['user']).pw_uid
|
uid = getpwnam(args['user']).pw_uid
|
||||||
gid = getgrnam(sks['group']).gr_gid
|
gid = getgrnam(args['group']).gr_gid
|
||||||
os.chown(newfile, uid, gid)
|
os.chown(newfile, uid, gid)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def syncDB():
|
def syncDB(args):
|
||||||
if not sks['rsync']:
|
if not args['rsync']:
|
||||||
return()
|
return()
|
||||||
cmd = ['rsync',
|
cmd = ['rsync',
|
||||||
'-a',
|
'-a',
|
||||||
'--delete',
|
'--delete',
|
||||||
os.path.join(sks['destdir'], '.'),
|
os.path.join(args['destdir'], '.'),
|
||||||
sks['rsync']]
|
args['rsync']]
|
||||||
with open(sks['logfile'], 'a') as f:
|
if args['throttle'] > 0.0:
|
||||||
|
cmd.insert(-1, '--bwlimit={0}'.format(str(args['throttle'])))
|
||||||
|
with open(args['logfile'], 'a') as f:
|
||||||
|
f.write('===== {0} Rsyncing to mirror =====\n'.format(
|
||||||
|
str(datetime.datetime.utcnow())))
|
||||||
|
with open(args['logfile'], 'a') as f:
|
||||||
subprocess.run(cmd, stdout = f, stderr = f)
|
subprocess.run(cmd, stdout = f, stderr = f)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
cfg = getDefaults()
|
||||||
|
system = cfg['system']
|
||||||
|
paths = cfg['paths']
|
||||||
|
sync = cfg['sync']
|
||||||
|
runtime = cfg['runtime']
|
||||||
|
args = argparse.ArgumentParser(description = ('sksdump - a tool for ' +
|
||||||
|
'dumping an SKS Database'),
|
||||||
|
epilog = ('brent s. || 2018 || ' +
|
||||||
|
'https://square-r00t.net'))
|
||||||
|
args.add_argument('-u',
|
||||||
|
'--user',
|
||||||
|
default = system['user'],
|
||||||
|
dest = 'user',
|
||||||
|
help = 'The user that you run SKS services as.')
|
||||||
|
args.add_argument('-g',
|
||||||
|
'--group',
|
||||||
|
default = system['group'],
|
||||||
|
dest = 'group',
|
||||||
|
help = 'The group that SKS services run as.')
|
||||||
|
args.add_argument('-c',
|
||||||
|
'--compress',
|
||||||
|
default = system['compress'],
|
||||||
|
dest = 'compress',
|
||||||
|
choices = ['xz', 'gz', 'bz2', 'lrz', None],
|
||||||
|
help = 'The compression scheme to apply to the dumps.')
|
||||||
|
args.add_argument('-s',
|
||||||
|
'--services',
|
||||||
|
default = system['svcs'],
|
||||||
|
dest = 'svcs',
|
||||||
|
help = ('A comma-separated list of services that will ' +
|
||||||
|
'be stopped/started for the dump (in the ' +
|
||||||
|
'provided order).'))
|
||||||
|
args.add_argument('-l',
|
||||||
|
'--log',
|
||||||
|
default = system['logfile'],
|
||||||
|
dest = 'logfile',
|
||||||
|
help = 'The path to the logfile.')
|
||||||
|
args.add_argument('-a',
|
||||||
|
'--days',
|
||||||
|
default = system['days'],
|
||||||
|
dest = 'days',
|
||||||
|
type = int,
|
||||||
|
help = 'How many days to keep rotation for.')
|
||||||
|
args.add_argument('-d',
|
||||||
|
'--dumpkeys',
|
||||||
|
default = system['dumpkeys'],
|
||||||
|
dest = 'dumpkeys',
|
||||||
|
type = int,
|
||||||
|
help = 'How many keys to put in each dump.')
|
||||||
|
args.add_argument('-b',
|
||||||
|
'--basedir',
|
||||||
|
default = paths['basedir'],
|
||||||
|
dest = 'basedir',
|
||||||
|
help = 'The directory which holds your SKS DB.')
|
||||||
|
args.add_argument('-x',
|
||||||
|
'--sks-binary',
|
||||||
|
default = paths['sksbin'],
|
||||||
|
dest = 'sksbin',
|
||||||
|
help = ('The path to the SKS binary/executable to use ' +
|
||||||
|
'to perform the dump.'))
|
||||||
|
args.add_argument('-e',
|
||||||
|
'--destdir',
|
||||||
|
default = paths['destdir'],
|
||||||
|
dest = 'destdir',
|
||||||
|
help = ('The directory where the dumps should be ' +
|
||||||
|
'saved (a sub-directory with the date will be ' +
|
||||||
|
'created).'))
|
||||||
|
args.add_argument('-r',
|
||||||
|
'--rsync',
|
||||||
|
default = paths['rsync'],
|
||||||
|
dest = 'rsync',
|
||||||
|
help = ('The remote (user@host:/path/) or local '+
|
||||||
|
'(/path/) path to use to sync the dumps to.'))
|
||||||
|
args.add_argument('-t',
|
||||||
|
'--throttle',
|
||||||
|
default = float(sync['throttle']),
|
||||||
|
dest = 'throttle',
|
||||||
|
type = float,
|
||||||
|
help = ('The amount in KiB/s to throttle the rsync ' +
|
||||||
|
'to. Use 0 for no throttling.'))
|
||||||
|
args.add_argument('-D',
|
||||||
|
'--no-dump',
|
||||||
|
dest = 'nodump',
|
||||||
|
action = 'store_true',
|
||||||
|
default = ('nodump' in runtime),
|
||||||
|
help = 'Don\'t dump the SKS DB (default is to dump)')
|
||||||
|
args.add_argument('-C',
|
||||||
|
'--no-compress',
|
||||||
|
dest = 'nocompress',
|
||||||
|
action = 'store_true',
|
||||||
|
default = ('nocompress' in runtime),
|
||||||
|
help = ('Don\'t compress the DB dumps (default is to ' +
|
||||||
|
'compress)'))
|
||||||
|
args.add_argument('-S',
|
||||||
|
'--no-sync',
|
||||||
|
dest = 'nosync',
|
||||||
|
action = 'store_true',
|
||||||
|
default = ('nosync' in runtime),
|
||||||
|
help = 'Don\'t sync the dumps to the remote server.')
|
||||||
|
varargs = vars(args.parse_args())
|
||||||
|
return(varargs)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if getpass.getuser() not in ('root', sks['user']):
|
args = parseArgs()
|
||||||
exit('ERROR: You must be root or {0}!'.format(sks['user']))
|
if getpass.getuser() not in ('root', args['user']):
|
||||||
dumpDB()
|
exit('ERROR: You must be root or {0}!'.format(args['user']))
|
||||||
compressDB()
|
with open(args['logfile'], 'a') as f:
|
||||||
syncDB()
|
f.write('===== {0} STARTING =====\n'.format(
|
||||||
|
str(datetime.datetime.utcnow())))
|
||||||
|
if not args['nodump']:
|
||||||
|
dumpDB(args)
|
||||||
|
if not args['nocompress']:
|
||||||
|
compressDB(args)
|
||||||
|
if not args['nosync']:
|
||||||
|
syncDB(args)
|
||||||
|
with open(args['logfile'], 'a') as f:
|
||||||
|
f.write('===== {0} DONE =====\n'.format(
|
||||||
|
str(datetime.datetime.utcnow())))
|
||||||
|
with open(os.path.join(args['destdir'], 'LAST_COMPLETED_DUMP.txt'),
|
||||||
|
'w') as f:
|
||||||
|
f.write(str(datetime.datetime.utcnow()) + ' UTC\n')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
4
ldap/loglevel.py
Normal file
4
ldap/loglevel.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# https://www.openldap.org/doc/admin24/slapdconfig.html#loglevel%20%3Clevel%3E
|
||||||
|
# https://www.zytrax.com/books/ldap/ch6/#loglevel
|
109
lib/python/logger.py
Executable file
109
lib/python/logger.py
Executable file
@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# The logfile.
|
||||||
|
dflt_logfile = '/var/log/optools/optools.log'
|
||||||
|
|
||||||
|
# The default log level. Can be one of (in increasing levels of output):
|
||||||
|
# critical
|
||||||
|
# error
|
||||||
|
# warning
|
||||||
|
# info
|
||||||
|
# debug
|
||||||
|
# "debug" may log sensitive information! Do *not* use it unless ABSOLUTELY
|
||||||
|
# NECESSARY.
|
||||||
|
dflt_loglevel = 'warning'
|
||||||
|
|
||||||
|
# stdlib
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
|
||||||
|
class log(object):
|
||||||
|
def __init__(self, loglvl = dflt_loglevel, logfile = dflt_logfile,
|
||||||
|
logname = 'optools'):
|
||||||
|
# Loglevel mappings.
|
||||||
|
self.loglvls = {'critical': logging.CRITICAL,
|
||||||
|
'error': logging.ERROR,
|
||||||
|
'warning': logging.WARNING,
|
||||||
|
'info': logging.INFO,
|
||||||
|
'debug': logging.DEBUG}
|
||||||
|
self.loglvl = loglvl.lower()
|
||||||
|
if self.loglvl not in self.loglvls:
|
||||||
|
raise ValueError(('{0} is not one of: ' +
|
||||||
|
'{1}').format(loglvl,
|
||||||
|
', '.join(self.loglvls.keys())))
|
||||||
|
self.Logger = logging.getLogger(logname)
|
||||||
|
self.logfile = os.path.abspath(os.path.expanduser(logfile))
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(self.logfile),
|
||||||
|
exist_ok = True,
|
||||||
|
mode = 0o700)
|
||||||
|
except Exception as e:
|
||||||
|
# Make this non-fatal since we also log to journal for systemd?
|
||||||
|
raise e
|
||||||
|
self.chkSystemd()
|
||||||
|
self.journald()
|
||||||
|
self.Logger.setLevel(self.loglvls[self.loglvl])
|
||||||
|
self.log_handlers()
|
||||||
|
|
||||||
|
def chkSystemd(self):
|
||||||
|
# Add journald support if we're on systemd.
|
||||||
|
# We probably are since we're most likely on Arch, but we don't want to
|
||||||
|
# make assumptions.
|
||||||
|
self.systemd = False
|
||||||
|
_sysd_chk = ['/run/systemd/system',
|
||||||
|
'/dev/.run/systemd',
|
||||||
|
'/dev/.systemd']
|
||||||
|
for _ in _sysd_chk:
|
||||||
|
if os.path.isdir(_):
|
||||||
|
self.systemd = True
|
||||||
|
break
|
||||||
|
return()
|
||||||
|
|
||||||
|
def journald(self):
|
||||||
|
if not self.systemd:
|
||||||
|
return()
|
||||||
|
try:
|
||||||
|
from systemd import journal
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import pip
|
||||||
|
pip.main(['install', '--user', 'systemd'])
|
||||||
|
from systemd import journal
|
||||||
|
except Exception as e:
|
||||||
|
# Build failed. Missing gcc, disk too full, whatever.
|
||||||
|
self.systemd = False
|
||||||
|
return()
|
||||||
|
|
||||||
|
def log_handlers(self):
|
||||||
|
# Log formats
|
||||||
|
if self.systemd:
|
||||||
|
_jrnlfmt = logging.Formatter(fmt = ('{levelname}: {message} ' +
|
||||||
|
'({filename}:{lineno})'),
|
||||||
|
style = '{',
|
||||||
|
datefmt = '%Y-%m-%d %H:%M:%S')
|
||||||
|
_logfmt = logging.Formatter(fmt = ('{asctime}:{levelname}: {message} (' +
|
||||||
|
'{filename}:{lineno})'),
|
||||||
|
style = '{',
|
||||||
|
datefmt = '%Y-%m-%d %H:%M:%S')
|
||||||
|
# Add handlers
|
||||||
|
_dflthandler = logging.handlers.RotatingFileHandler(self.logfile,
|
||||||
|
encoding = 'utf8',
|
||||||
|
# 1GB
|
||||||
|
maxBytes = 1073741824,
|
||||||
|
backupCount = 5)
|
||||||
|
_dflthandler.setFormatter(_logfmt)
|
||||||
|
_dflthandler.setLevel(self.loglvls[self.loglvl])
|
||||||
|
if self.systemd:
|
||||||
|
from systemd import journal
|
||||||
|
try:
|
||||||
|
h = journal.JournaldLogHandler()
|
||||||
|
except AttributeError: # Uses the other version
|
||||||
|
h = journal.JournalHandler()
|
||||||
|
h.setFormatter(_jrnlfmt)
|
||||||
|
h.setLevel(self.loglvls[self.loglvl])
|
||||||
|
self.Logger.addHandler(h)
|
||||||
|
self.Logger.addHandler(_dflthandler)
|
||||||
|
self.Logger.info('Logging initialized')
|
||||||
|
return()
|
1
libvirt/README
Normal file
1
libvirt/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
These projects/scripts have been moved to https://git.square-r00t.net/LibvirtTools/.
|
2
mumble/.gitignore
vendored
Normal file
2
mumble/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/docs
|
||||||
|
/testcertimport.py
|
563
mumble/Mumble.proto
Normal file
563
mumble/Mumble.proto
Normal file
@ -0,0 +1,563 @@
|
|||||||
|
// Copyright 2005-2017 The Mumble Developers. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file at the root of the
|
||||||
|
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||||
|
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package MumbleProto;
|
||||||
|
|
||||||
|
option optimize_for = SPEED;
|
||||||
|
|
||||||
|
message Version {
|
||||||
|
// 2-byte Major, 1-byte Minor and 1-byte Patch version number.
|
||||||
|
optional uint32 version = 1;
|
||||||
|
// Client release name.
|
||||||
|
optional string release = 2;
|
||||||
|
// Client OS name.
|
||||||
|
optional string os = 3;
|
||||||
|
// Client OS version.
|
||||||
|
optional string os_version = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not used. Not even for tunneling UDP through TCP.
|
||||||
|
message UDPTunnel {
|
||||||
|
// Not used.
|
||||||
|
required bytes packet = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by the client to send the authentication credentials to the server.
|
||||||
|
message Authenticate {
|
||||||
|
// UTF-8 encoded username.
|
||||||
|
optional string username = 1;
|
||||||
|
// Server or user password.
|
||||||
|
optional string password = 2;
|
||||||
|
// Additional access tokens for server ACL groups.
|
||||||
|
repeated string tokens = 3;
|
||||||
|
// A list of CELT bitstream version constants supported by the client.
|
||||||
|
repeated int32 celt_versions = 4;
|
||||||
|
optional bool opus = 5 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the client to notify the server that the client is still alive.
|
||||||
|
// Server must reply to the packet with the same timestamp and its own
|
||||||
|
// good/late/lost/resync numbers. None of the fields is strictly required.
|
||||||
|
message Ping {
|
||||||
|
// Client timestamp. Server should not attempt to decode.
|
||||||
|
optional uint64 timestamp = 1;
|
||||||
|
// The amount of good packets received.
|
||||||
|
optional uint32 good = 2;
|
||||||
|
// The amount of late packets received.
|
||||||
|
optional uint32 late = 3;
|
||||||
|
// The amount of packets never received.
|
||||||
|
optional uint32 lost = 4;
|
||||||
|
// The amount of nonce resyncs.
|
||||||
|
optional uint32 resync = 5;
|
||||||
|
// The total amount of UDP packets received.
|
||||||
|
optional uint32 udp_packets = 6;
|
||||||
|
// The total amount of TCP packets received.
|
||||||
|
optional uint32 tcp_packets = 7;
|
||||||
|
// UDP ping average.
|
||||||
|
optional float udp_ping_avg = 8;
|
||||||
|
// UDP ping variance.
|
||||||
|
optional float udp_ping_var = 9;
|
||||||
|
// TCP ping average.
|
||||||
|
optional float tcp_ping_avg = 10;
|
||||||
|
// TCP ping variance.
|
||||||
|
optional float tcp_ping_var = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the server when it rejects the user connection.
|
||||||
|
message Reject {
|
||||||
|
enum RejectType {
|
||||||
|
// The rejection reason is unknown (details should be available
|
||||||
|
// in Reject.reason).
|
||||||
|
None = 0;
|
||||||
|
// The client attempted to connect with an incompatible version.
|
||||||
|
WrongVersion = 1;
|
||||||
|
// The user name supplied by the client was invalid.
|
||||||
|
InvalidUsername = 2;
|
||||||
|
// The client attempted to authenticate as a user with a password but it
|
||||||
|
// was wrong.
|
||||||
|
WrongUserPW = 3;
|
||||||
|
// The client attempted to connect to a passworded server but the password
|
||||||
|
// was wrong.
|
||||||
|
WrongServerPW = 4;
|
||||||
|
// Supplied username is already in use.
|
||||||
|
UsernameInUse = 5;
|
||||||
|
// Server is currently full and cannot accept more users.
|
||||||
|
ServerFull = 6;
|
||||||
|
// The user did not provide a certificate but one is required.
|
||||||
|
NoCertificate = 7;
|
||||||
|
AuthenticatorFail = 8;
|
||||||
|
}
|
||||||
|
// Rejection type.
|
||||||
|
optional RejectType type = 1;
|
||||||
|
// Human readable rejection reason.
|
||||||
|
optional string reason = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerSync message is sent by the server when it has authenticated the user
|
||||||
|
// and finished synchronizing the server state.
|
||||||
|
message ServerSync {
|
||||||
|
// The session of the current user.
|
||||||
|
optional uint32 session = 1;
|
||||||
|
// Maximum bandwidth that the user should use.
|
||||||
|
optional uint32 max_bandwidth = 2;
|
||||||
|
// Server welcome text.
|
||||||
|
optional string welcome_text = 3;
|
||||||
|
// Current user permissions in the root channel.
|
||||||
|
optional uint64 permissions = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the client when it wants a channel removed. Sent by the server when
|
||||||
|
// a channel has been removed and clients should be notified.
|
||||||
|
message ChannelRemove {
|
||||||
|
required uint32 channel_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to communicate channel properties between the client and the server.
|
||||||
|
// Sent by the server during the login process or when channel properties are
|
||||||
|
// updated. Client may use this message to update said channel properties.
|
||||||
|
message ChannelState {
|
||||||
|
// Unique ID for the channel within the server.
|
||||||
|
optional uint32 channel_id = 1;
|
||||||
|
// channel_id of the parent channel.
|
||||||
|
optional uint32 parent = 2;
|
||||||
|
// UTF-8 encoded channel name.
|
||||||
|
optional string name = 3;
|
||||||
|
// A collection of channel id values of the linked channels. Absent during
|
||||||
|
// the first channel listing.
|
||||||
|
repeated uint32 links = 4;
|
||||||
|
// UTF-8 encoded channel description. Only if the description is less than
|
||||||
|
// 128 bytes
|
||||||
|
optional string description = 5;
|
||||||
|
// A collection of channel_id values that should be added to links.
|
||||||
|
repeated uint32 links_add = 6;
|
||||||
|
// A collection of channel_id values that should be removed from links.
|
||||||
|
repeated uint32 links_remove = 7;
|
||||||
|
// True if the channel is temporary.
|
||||||
|
optional bool temporary = 8 [default = false];
|
||||||
|
// Position weight to tweak the channel position in the channel list.
|
||||||
|
optional int32 position = 9 [default = 0];
|
||||||
|
// SHA1 hash of the description if the description is 128 bytes or more.
|
||||||
|
optional bytes description_hash = 10;
|
||||||
|
// Maximum number of users allowed in the channel. If this value is zero,
|
||||||
|
// the maximum number of users allowed in the channel is given by the
|
||||||
|
// server's "usersperchannel" setting.
|
||||||
|
optional uint32 max_users = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to communicate user leaving or being kicked. May be sent by the client
|
||||||
|
// when it attempts to kick a user. Sent by the server when it informs the
|
||||||
|
// clients that a user is not present anymore.
|
||||||
|
message UserRemove {
|
||||||
|
// The user who is being kicked, identified by their session, not present
|
||||||
|
// when no one is being kicked.
|
||||||
|
required uint32 session = 1;
|
||||||
|
// The user who initiated the removal. Either the user who performs the kick
|
||||||
|
// or the user who is currently leaving.
|
||||||
|
optional uint32 actor = 2;
|
||||||
|
// Reason for the kick, stored as the ban reason if the user is banned.
|
||||||
|
optional string reason = 3;
|
||||||
|
// True if the kick should result in a ban.
|
||||||
|
optional bool ban = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the server when it communicates new and changed users to client.
|
||||||
|
// First seen during login procedure. May be sent by the client when it wishes
|
||||||
|
// to alter its state.
|
||||||
|
message UserState {
|
||||||
|
// Unique user session ID of the user whose state this is, may change on
|
||||||
|
// reconnect.
|
||||||
|
optional uint32 session = 1;
|
||||||
|
// The session of the user who is updating this user.
|
||||||
|
optional uint32 actor = 2;
|
||||||
|
// User name, UTF-8 encoded.
|
||||||
|
optional string name = 3;
|
||||||
|
// Registered user ID if the user is registered.
|
||||||
|
optional uint32 user_id = 4;
|
||||||
|
// Channel on which the user is.
|
||||||
|
optional uint32 channel_id = 5;
|
||||||
|
// True if the user is muted by admin.
|
||||||
|
optional bool mute = 6;
|
||||||
|
// True if the user is deafened by admin.
|
||||||
|
optional bool deaf = 7;
|
||||||
|
// True if the user has been suppressed from talking by a reason other than
|
||||||
|
// being muted.
|
||||||
|
optional bool suppress = 8;
|
||||||
|
// True if the user has muted self.
|
||||||
|
optional bool self_mute = 9;
|
||||||
|
// True if the user has deafened self.
|
||||||
|
optional bool self_deaf = 10;
|
||||||
|
// User image if it is less than 128 bytes.
|
||||||
|
optional bytes texture = 11;
|
||||||
|
// The positional audio plugin identifier.
|
||||||
|
// Positional audio information is only sent to users who share
|
||||||
|
// identical plugin contexts.
|
||||||
|
//
|
||||||
|
// This value is not trasmitted to clients.
|
||||||
|
optional bytes plugin_context = 12;
|
||||||
|
// The user's plugin-specific identity.
|
||||||
|
// This value is not transmitted to clients.
|
||||||
|
optional string plugin_identity = 13;
|
||||||
|
// User comment if it is less than 128 bytes.
|
||||||
|
optional string comment = 14;
|
||||||
|
// The hash of the user certificate.
|
||||||
|
optional string hash = 15;
|
||||||
|
// SHA1 hash of the user comment if it 128 bytes or more.
|
||||||
|
optional bytes comment_hash = 16;
|
||||||
|
// SHA1 hash of the user picture if it 128 bytes or more.
|
||||||
|
optional bytes texture_hash = 17;
|
||||||
|
// True if the user is a priority speaker.
|
||||||
|
optional bool priority_speaker = 18;
|
||||||
|
// True if the user is currently recording.
|
||||||
|
optional bool recording = 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relays information on the bans. The client may send the BanList message to
|
||||||
|
// either modify the list of bans or query them from the server. The server
|
||||||
|
// sends this list only after a client queries for it.
|
||||||
|
message BanList {
|
||||||
|
message BanEntry {
|
||||||
|
// Banned IP address.
|
||||||
|
required bytes address = 1;
|
||||||
|
// The length of the subnet mask for the ban.
|
||||||
|
required uint32 mask = 2;
|
||||||
|
// User name for identification purposes (does not affect the ban).
|
||||||
|
optional string name = 3;
|
||||||
|
// The certificate hash of the banned user.
|
||||||
|
optional string hash = 4;
|
||||||
|
// Reason for the ban (does not affect the ban).
|
||||||
|
optional string reason = 5;
|
||||||
|
// Ban start time.
|
||||||
|
optional string start = 6;
|
||||||
|
// Ban duration in seconds.
|
||||||
|
optional uint32 duration = 7;
|
||||||
|
}
|
||||||
|
// List of ban entries currently in place.
|
||||||
|
repeated BanEntry bans = 1;
|
||||||
|
// True if the server should return the list, false if it should replace old
|
||||||
|
// ban list with the one provided.
|
||||||
|
optional bool query = 2 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to send and broadcast text messages.
|
||||||
|
message TextMessage {
|
||||||
|
// The message sender, identified by its session.
|
||||||
|
optional uint32 actor = 1;
|
||||||
|
// Target users for the message, identified by their session.
|
||||||
|
repeated uint32 session = 2;
|
||||||
|
// The channels to which the message is sent, identified by their
|
||||||
|
// channel_ids.
|
||||||
|
repeated uint32 channel_id = 3;
|
||||||
|
// The root channels when sending message recursively to several channels,
|
||||||
|
// identified by their channel_ids.
|
||||||
|
repeated uint32 tree_id = 4;
|
||||||
|
// The UTF-8 encoded message. May be HTML if the server allows.
|
||||||
|
required string message = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PermissionDenied {
|
||||||
|
enum DenyType {
|
||||||
|
// Operation denied for other reason, see reason field.
|
||||||
|
Text = 0;
|
||||||
|
// Permissions were denied.
|
||||||
|
Permission = 1;
|
||||||
|
// Cannot modify SuperUser.
|
||||||
|
SuperUser = 2;
|
||||||
|
// Invalid channel name.
|
||||||
|
ChannelName = 3;
|
||||||
|
// Text message too long.
|
||||||
|
TextTooLong = 4;
|
||||||
|
// The flux capacitor was spelled wrong.
|
||||||
|
H9K = 5;
|
||||||
|
// Operation not permitted in temporary channel.
|
||||||
|
TemporaryChannel = 6;
|
||||||
|
// Operation requires certificate.
|
||||||
|
MissingCertificate = 7;
|
||||||
|
// Invalid username.
|
||||||
|
UserName = 8;
|
||||||
|
// Channel is full.
|
||||||
|
ChannelFull = 9;
|
||||||
|
NestingLimit = 10;
|
||||||
|
}
|
||||||
|
// The denied permission when type is Permission.
|
||||||
|
optional uint32 permission = 1;
|
||||||
|
// channel_id for the channel where the permission was denied when type is
|
||||||
|
// Permission.
|
||||||
|
optional uint32 channel_id = 2;
|
||||||
|
// The user who was denied permissions, identified by session.
|
||||||
|
optional uint32 session = 3;
|
||||||
|
// Textual reason for the denial.
|
||||||
|
optional string reason = 4;
|
||||||
|
// Type of the denial.
|
||||||
|
optional DenyType type = 5;
|
||||||
|
// The name that is invalid when type is UserName.
|
||||||
|
optional string name = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ACL {
|
||||||
|
message ChanGroup {
|
||||||
|
// Name of the channel group, UTF-8 encoded.
|
||||||
|
required string name = 1;
|
||||||
|
// True if the group has been inherited from the parent (Read only).
|
||||||
|
optional bool inherited = 2 [default = true];
|
||||||
|
// True if the group members are inherited.
|
||||||
|
optional bool inherit = 3 [default = true];
|
||||||
|
// True if the group can be inherited by sub channels.
|
||||||
|
optional bool inheritable = 4 [default = true];
|
||||||
|
// Users explicitly included in this group, identified by user_id.
|
||||||
|
repeated uint32 add = 5;
|
||||||
|
// Users explicitly removed from this group in this channel if the group
|
||||||
|
// has been inherited, identified by user_id.
|
||||||
|
repeated uint32 remove = 6;
|
||||||
|
// Users inherited, identified by user_id.
|
||||||
|
repeated uint32 inherited_members = 7;
|
||||||
|
}
|
||||||
|
message ChanACL {
|
||||||
|
// True if this ACL applies to the current channel.
|
||||||
|
optional bool apply_here = 1 [default = true];
|
||||||
|
// True if this ACL applies to the sub channels.
|
||||||
|
optional bool apply_subs = 2 [default = true];
|
||||||
|
// True if the ACL has been inherited from the parent.
|
||||||
|
optional bool inherited = 3 [default = true];
|
||||||
|
// ID of the user that is affected by this ACL.
|
||||||
|
optional uint32 user_id = 4;
|
||||||
|
// ID of the group that is affected by this ACL.
|
||||||
|
optional string group = 5;
|
||||||
|
// Bit flag field of the permissions granted by this ACL.
|
||||||
|
optional uint32 grant = 6;
|
||||||
|
// Bit flag field of the permissions denied by this ACL.
|
||||||
|
optional uint32 deny = 7;
|
||||||
|
}
|
||||||
|
// Channel ID of the channel this message affects.
|
||||||
|
required uint32 channel_id = 1;
|
||||||
|
// True if the channel inherits its parent's ACLs.
|
||||||
|
optional bool inherit_acls = 2 [default = true];
|
||||||
|
// User group specifications.
|
||||||
|
repeated ChanGroup groups = 3;
|
||||||
|
// ACL specifications.
|
||||||
|
repeated ChanACL acls = 4;
|
||||||
|
// True if the message is a query for ACLs instead of setting them.
|
||||||
|
optional bool query = 5 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client may use this message to refresh its registered user information. The
|
||||||
|
// client should fill the IDs or Names of the users it wants to refresh. The
|
||||||
|
// server fills the missing parts and sends the message back.
|
||||||
|
message QueryUsers {
|
||||||
|
// user_ids.
|
||||||
|
repeated uint32 ids = 1;
|
||||||
|
// User names in the same order as ids.
|
||||||
|
repeated string names = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to initialize and resync the UDP encryption. Either side may request a
|
||||||
|
// resync by sending the message without any values filled. The resync is
|
||||||
|
// performed by sending the message with only the client or server nonce
|
||||||
|
// filled.
|
||||||
|
message CryptSetup {
|
||||||
|
// Encryption key.
|
||||||
|
optional bytes key = 1;
|
||||||
|
// Client nonce.
|
||||||
|
optional bytes client_nonce = 2;
|
||||||
|
// Server nonce.
|
||||||
|
optional bytes server_nonce = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ContextActionModify {
|
||||||
|
enum Context {
|
||||||
|
// Action is applicable to the server.
|
||||||
|
Server = 0x01;
|
||||||
|
// Action can target a Channel.
|
||||||
|
Channel = 0x02;
|
||||||
|
// Action can target a User.
|
||||||
|
User = 0x04;
|
||||||
|
}
|
||||||
|
enum Operation {
|
||||||
|
Add = 0;
|
||||||
|
Remove = 1;
|
||||||
|
}
|
||||||
|
// The action name.
|
||||||
|
required string action = 1;
|
||||||
|
// The display name of the action.
|
||||||
|
optional string text = 2;
|
||||||
|
// Context bit flags defining where the action should be displayed.
|
||||||
|
optional uint32 context = 3;
|
||||||
|
optional Operation operation = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the client when it wants to initiate a Context action.
|
||||||
|
message ContextAction {
|
||||||
|
// The target User for the action, identified by session.
|
||||||
|
optional uint32 session = 1;
|
||||||
|
// The target Channel for the action, identified by channel_id.
|
||||||
|
optional uint32 channel_id = 2;
|
||||||
|
// The action that should be executed.
|
||||||
|
required string action = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lists the registered users.
|
||||||
|
message UserList {
|
||||||
|
message User {
|
||||||
|
// Registered user ID.
|
||||||
|
required uint32 user_id = 1;
|
||||||
|
// Registered user name.
|
||||||
|
optional string name = 2;
|
||||||
|
optional string last_seen = 3;
|
||||||
|
optional uint32 last_channel = 4;
|
||||||
|
}
|
||||||
|
// A list of registered users.
|
||||||
|
repeated User users = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the client when it wants to register or clear whisper targets.
|
||||||
|
//
|
||||||
|
// Note: The first available target ID is 1 as 0 is reserved for normal
|
||||||
|
// talking. Maximum target ID is 30.
|
||||||
|
message VoiceTarget {
|
||||||
|
message Target {
|
||||||
|
// Users that are included as targets.
|
||||||
|
repeated uint32 session = 1;
|
||||||
|
// Channel that is included as a target.
|
||||||
|
optional uint32 channel_id = 2;
|
||||||
|
// ACL group that is included as a target.
|
||||||
|
optional string group = 3;
|
||||||
|
// True if the voice should follow links from the specified channel.
|
||||||
|
optional bool links = 4 [default = false];
|
||||||
|
// True if the voice should also be sent to children of the specific
|
||||||
|
// channel.
|
||||||
|
optional bool children = 5 [default = false];
|
||||||
|
}
|
||||||
|
// Voice target ID.
|
||||||
|
optional uint32 id = 1;
|
||||||
|
// The receivers that this voice target includes.
|
||||||
|
repeated Target targets = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the client when it wants permissions for a certain channel. Sent by
|
||||||
|
// the server when it replies to the query or wants the user to resync all
|
||||||
|
// channel permissions.
|
||||||
|
message PermissionQuery {
|
||||||
|
// channel_id of the channel for which the permissions are queried.
|
||||||
|
optional uint32 channel_id = 1;
|
||||||
|
// Channel permissions.
|
||||||
|
optional uint32 permissions = 2;
|
||||||
|
// True if the client should drop its current permission information for all
|
||||||
|
// channels.
|
||||||
|
optional bool flush = 3 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the server to notify the users of the version of the CELT codec they
|
||||||
|
// should use. This may change during the connection when new users join.
|
||||||
|
message CodecVersion {
|
||||||
|
// The version of the CELT Alpha codec.
|
||||||
|
required int32 alpha = 1;
|
||||||
|
// The version of the CELT Beta codec.
|
||||||
|
required int32 beta = 2;
|
||||||
|
// True if the user should prefer Alpha over Beta.
|
||||||
|
required bool prefer_alpha = 3 [default = true];
|
||||||
|
optional bool opus = 4 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to communicate user stats between the server and clients.
|
||||||
|
message UserStats {
|
||||||
|
message Stats {
|
||||||
|
// The amount of good packets received.
|
||||||
|
optional uint32 good = 1;
|
||||||
|
// The amount of late packets received.
|
||||||
|
optional uint32 late = 2;
|
||||||
|
// The amount of packets never received.
|
||||||
|
optional uint32 lost = 3;
|
||||||
|
// The amount of nonce resyncs.
|
||||||
|
optional uint32 resync = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User whose stats these are.
|
||||||
|
optional uint32 session = 1;
|
||||||
|
// True if the message contains only mutable stats (packets, ping).
|
||||||
|
optional bool stats_only = 2 [default = false];
|
||||||
|
// Full user certificate chain of the user certificate in DER format.
|
||||||
|
repeated bytes certificates = 3;
|
||||||
|
// Packet statistics for packets received from the client.
|
||||||
|
optional Stats from_client = 4;
|
||||||
|
// Packet statistics for packets sent by the server.
|
||||||
|
optional Stats from_server = 5;
|
||||||
|
|
||||||
|
// Amount of UDP packets sent.
|
||||||
|
optional uint32 udp_packets = 6;
|
||||||
|
// Amount of TCP packets sent.
|
||||||
|
optional uint32 tcp_packets = 7;
|
||||||
|
// UDP ping average.
|
||||||
|
optional float udp_ping_avg = 8;
|
||||||
|
// UDP ping variance.
|
||||||
|
optional float udp_ping_var = 9;
|
||||||
|
// TCP ping average.
|
||||||
|
optional float tcp_ping_avg = 10;
|
||||||
|
// TCP ping variance.
|
||||||
|
optional float tcp_ping_var = 11;
|
||||||
|
|
||||||
|
// Client version.
|
||||||
|
optional Version version = 12;
|
||||||
|
// A list of CELT bitstream version constants supported by the client of this
|
||||||
|
// user.
|
||||||
|
repeated int32 celt_versions = 13;
|
||||||
|
// Client IP address.
|
||||||
|
optional bytes address = 14;
|
||||||
|
// Bandwith used by this client.
|
||||||
|
optional uint32 bandwidth = 15;
|
||||||
|
// Connection duration.
|
||||||
|
optional uint32 onlinesecs = 16;
|
||||||
|
// Duration since last activity.
|
||||||
|
optional uint32 idlesecs = 17;
|
||||||
|
// True if the user has a strong certificate.
|
||||||
|
optional bool strong_certificate = 18 [default = false];
|
||||||
|
optional bool opus = 19 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by the client to request binary data from the server. By default large
|
||||||
|
// comments or textures are not sent within standard messages but instead the
|
||||||
|
// hash is. If the client does not recognize the hash it may request the
|
||||||
|
// resource when it needs it. The client does so by sending a RequestBlob
|
||||||
|
// message with the correct fields filled with the user sessions or channel_ids
|
||||||
|
// it wants to receive. The server replies to this by sending a new
|
||||||
|
// UserState/ChannelState message with the resources filled even if they would
|
||||||
|
// normally be transmitted as hashes.
|
||||||
|
message RequestBlob {
|
||||||
|
// sessions of the requested UserState textures.
|
||||||
|
repeated uint32 session_texture = 1;
|
||||||
|
// sessions of the requested UserState comments.
|
||||||
|
repeated uint32 session_comment = 2;
|
||||||
|
// channel_ids of the requested ChannelState descriptions.
|
||||||
|
repeated uint32 channel_description = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the server when it informs the clients on server configuration
|
||||||
|
// details.
|
||||||
|
message ServerConfig {
|
||||||
|
// The maximum bandwidth the clients should use.
|
||||||
|
optional uint32 max_bandwidth = 1;
|
||||||
|
// Server welcome text.
|
||||||
|
optional string welcome_text = 2;
|
||||||
|
// True if the server allows HTML.
|
||||||
|
optional bool allow_html = 3;
|
||||||
|
// Maximum text message length.
|
||||||
|
optional uint32 message_length = 4;
|
||||||
|
// Maximum image message length.
|
||||||
|
optional uint32 image_message_length = 5;
|
||||||
|
// The maximum number of users allowed on the server.
|
||||||
|
optional uint32 max_users = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sent by the server to inform the clients of suggested client configuration
|
||||||
|
// specified by the server administrator.
|
||||||
|
message SuggestConfig {
|
||||||
|
// Suggested client version.
|
||||||
|
optional uint32 version = 1;
|
||||||
|
// True if the administrator suggests positional audio to be used on this
|
||||||
|
// server.
|
||||||
|
optional bool positional = 2;
|
||||||
|
// True if the administrator suggests push to talk to be used on this server.
|
||||||
|
optional bool push_to_talk = 3;
|
||||||
|
}
|
823
mumble/MurmurRPC.proto
Normal file
823
mumble/MurmurRPC.proto
Normal file
@ -0,0 +1,823 @@
|
|||||||
|
// Copyright 2005-2017 The Mumble Developers. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file at the root of the
|
||||||
|
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||||
|
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package MurmurRPC;
|
||||||
|
|
||||||
|
// Note about embedded messages:
|
||||||
|
//
|
||||||
|
// To help save bandwidth, the protocol does not always send complete embedded
|
||||||
|
// messages (i.e. an embeddded message with all of the fields filled in). These
|
||||||
|
// incomplete messages only contain enough identifying information to get more
|
||||||
|
// information from the message's corresponding "Get" method. For example:
|
||||||
|
//
|
||||||
|
// User.server only ever contains the server ID. Calling ServerGet(User.server)
|
||||||
|
// will return a Server message with the server's status and uptime.
|
||||||
|
|
||||||
|
message Void {
|
||||||
|
}
|
||||||
|
|
||||||
|
message Version {
|
||||||
|
// 2-byte Major, 1-byte Minor and 1-byte Patch version number.
|
||||||
|
optional uint32 version = 1;
|
||||||
|
// Client release name.
|
||||||
|
optional string release = 2;
|
||||||
|
// Client OS name.
|
||||||
|
optional string os = 3;
|
||||||
|
// Client OS version.
|
||||||
|
optional string os_version = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Uptime {
|
||||||
|
// The number of seconds from the starting time.
|
||||||
|
optional uint64 secs = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Server {
|
||||||
|
// The unique server ID.
|
||||||
|
required uint32 id = 1;
|
||||||
|
// Is the server currently running?
|
||||||
|
optional bool running = 2;
|
||||||
|
// The update of the server.
|
||||||
|
optional Uptime uptime = 3;
|
||||||
|
|
||||||
|
message Event {
|
||||||
|
enum Type {
|
||||||
|
UserConnected = 0;
|
||||||
|
UserDisconnected = 1;
|
||||||
|
UserStateChanged = 2;
|
||||||
|
UserTextMessage = 3;
|
||||||
|
ChannelCreated = 4;
|
||||||
|
ChannelRemoved = 5;
|
||||||
|
ChannelStateChanged = 6;
|
||||||
|
};
|
||||||
|
// The server on which the event happened.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The type of event that happened.
|
||||||
|
optional Type type = 2;
|
||||||
|
// The user tied to the event (if applicable).
|
||||||
|
optional User user = 3;
|
||||||
|
// The text message tied to the event (if applicable).
|
||||||
|
optional TextMessage message = 4;
|
||||||
|
// The channel tied to the event (if applicable).
|
||||||
|
optional Channel channel = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
}
|
||||||
|
|
||||||
|
message List {
|
||||||
|
// The servers.
|
||||||
|
repeated Server servers = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Event {
|
||||||
|
enum Type {
|
||||||
|
ServerStopped = 0;
|
||||||
|
ServerStarted = 1;
|
||||||
|
};
|
||||||
|
// The server for which the event happened.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The type of event that happened.
|
||||||
|
optional Type type = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ContextAction {
|
||||||
|
enum Context {
|
||||||
|
Server = 0x01;
|
||||||
|
Channel = 0x02;
|
||||||
|
User = 0x04;
|
||||||
|
};
|
||||||
|
// The server on which the action is.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The context in which the action is.
|
||||||
|
optional uint32 context = 2;
|
||||||
|
// The action name.
|
||||||
|
optional string action = 3;
|
||||||
|
// The user-visible descriptive name of the action.
|
||||||
|
optional string text = 4;
|
||||||
|
// The user that triggered the ContextAction.
|
||||||
|
optional User actor = 5;
|
||||||
|
// The user on which the ContextAction was triggered.
|
||||||
|
optional User user = 6;
|
||||||
|
// The channel on which the ContextAction was triggered.
|
||||||
|
optional Channel channel = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TextMessage {
|
||||||
|
// The server on which the TextMessage originates.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The user who sent the message.
|
||||||
|
optional User actor = 2;
|
||||||
|
// The users to whom the message is sent.
|
||||||
|
repeated User users = 3;
|
||||||
|
// The channels to which the message is sent.
|
||||||
|
repeated Channel channels = 4;
|
||||||
|
// The channels to which the message is sent, including the channels'
|
||||||
|
// ancestors.
|
||||||
|
repeated Channel trees = 5;
|
||||||
|
// The message body that is sent.
|
||||||
|
optional string text = 6;
|
||||||
|
|
||||||
|
message Filter {
|
||||||
|
enum Action {
|
||||||
|
// Accept the message.
|
||||||
|
Accept = 0;
|
||||||
|
// Reject the message with a permission error.
|
||||||
|
Reject = 1;
|
||||||
|
// Silently drop the message.
|
||||||
|
Drop = 2;
|
||||||
|
}
|
||||||
|
// The server on which the message originated.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The action to perform for the message.
|
||||||
|
optional Action action = 2;
|
||||||
|
// The text message.
|
||||||
|
optional TextMessage message = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Log {
|
||||||
|
// The server on which the log message was generated.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The unix timestamp of when the message was generated.
|
||||||
|
optional int64 timestamp = 2;
|
||||||
|
// The log message.
|
||||||
|
optional string text = 3;
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
// The server whose logs will be queried.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The minimum log index to receive.
|
||||||
|
optional uint32 min = 2;
|
||||||
|
// The maximum log index to receive.
|
||||||
|
optional uint32 max = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message List {
|
||||||
|
// The server where the log entries are from.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The total number of logs entries on the server.
|
||||||
|
optional uint32 total = 2;
|
||||||
|
// The minimum log index that was sent.
|
||||||
|
optional uint32 min = 3;
|
||||||
|
// The maximum log index that was sent.
|
||||||
|
optional uint32 max = 4;
|
||||||
|
// The log entries.
|
||||||
|
repeated Log entries = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Config {
|
||||||
|
// The server for which the configuration is for.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The configuration keys and values.
|
||||||
|
map<string, string> fields = 2;
|
||||||
|
|
||||||
|
message Field {
|
||||||
|
// The server for which the configuration field is for.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The field key.
|
||||||
|
optional string key = 2;
|
||||||
|
// The field value.
|
||||||
|
optional string value = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Channel {
|
||||||
|
// The server on which the channel exists.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The unique channel identifier.
|
||||||
|
optional uint32 id = 2;
|
||||||
|
// The channel name.
|
||||||
|
optional string name = 3;
|
||||||
|
// The channel's parent.
|
||||||
|
optional Channel parent = 4;
|
||||||
|
// Linked channels.
|
||||||
|
repeated Channel links = 5;
|
||||||
|
// The channel's description.
|
||||||
|
optional string description = 6;
|
||||||
|
// Is the channel temporary?
|
||||||
|
optional bool temporary = 7;
|
||||||
|
// The position in which the channel should appear in a sorted list.
|
||||||
|
optional int32 position = 8;
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
// The server on which the channels are.
|
||||||
|
optional Server server = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message List {
|
||||||
|
// The server on which the channels are.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The channels.
|
||||||
|
repeated Channel channels = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message User {
|
||||||
|
// The server to which the user is connected.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The user's session ID.
|
||||||
|
optional uint32 session = 2;
|
||||||
|
// The user's registered ID.
|
||||||
|
optional uint32 id = 3;
|
||||||
|
// The user's name.
|
||||||
|
optional string name = 4;
|
||||||
|
// Is the user muted?
|
||||||
|
optional bool mute = 5;
|
||||||
|
// Is the user deafened?
|
||||||
|
optional bool deaf = 6;
|
||||||
|
// Is the user suppressed?
|
||||||
|
optional bool suppress = 7;
|
||||||
|
// Is the user a priority speaker?
|
||||||
|
optional bool priority_speaker = 8;
|
||||||
|
// Has the user muted him/herself?
|
||||||
|
optional bool self_mute = 9;
|
||||||
|
// Has the user muted him/herself?
|
||||||
|
optional bool self_deaf = 10;
|
||||||
|
// Is the user recording?
|
||||||
|
optional bool recording = 11;
|
||||||
|
// The channel the user is in.
|
||||||
|
optional Channel channel = 12;
|
||||||
|
// How long the user has been connected to the server.
|
||||||
|
optional uint32 online_secs = 13;
|
||||||
|
// How long the user has been idle on the server.
|
||||||
|
optional uint32 idle_secs = 14;
|
||||||
|
// How many bytes per second is the user transmitting to the server.
|
||||||
|
optional uint32 bytes_per_sec = 15;
|
||||||
|
// The user's client version.
|
||||||
|
optional Version version = 16;
|
||||||
|
// The user's plugin context.
|
||||||
|
optional bytes plugin_context = 17;
|
||||||
|
// The user's plugin identity.
|
||||||
|
optional string plugin_identity = 18;
|
||||||
|
// The user's comment.
|
||||||
|
optional string comment = 19;
|
||||||
|
// The user's texture.
|
||||||
|
optional bytes texture = 20;
|
||||||
|
// The user's IP address.
|
||||||
|
optional bytes address = 21;
|
||||||
|
// Is the user in TCP-only mode?
|
||||||
|
optional bool tcp_only = 22;
|
||||||
|
// The user's UDP ping in milliseconds.
|
||||||
|
optional float udp_ping_msecs = 23;
|
||||||
|
// The user's TCP ping in milliseconds.
|
||||||
|
optional float tcp_ping_msecs = 24;
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
// The server whose users will be queried.
|
||||||
|
optional Server server = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message List {
|
||||||
|
// The server to which the users are connected.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The users.
|
||||||
|
repeated User users = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Kick {
|
||||||
|
// The server to which the user is connected.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The user to kick.
|
||||||
|
optional User user = 2;
|
||||||
|
// The user who performed the kick.
|
||||||
|
optional User actor = 3;
|
||||||
|
// The reason for why the user is being kicked.
|
||||||
|
optional string reason = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Tree {
|
||||||
|
// The server which the tree represents.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The current channel.
|
||||||
|
optional Channel channel = 2;
|
||||||
|
// Channels below the current channel.
|
||||||
|
repeated Tree children = 3;
|
||||||
|
// The users in the current channel.
|
||||||
|
repeated User users = 4;
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
// The server to query.
|
||||||
|
optional Server server = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Ban {
|
||||||
|
// The server on which the ban is applied.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The banned IP address.
|
||||||
|
optional bytes address = 2;
|
||||||
|
// The number of leading bits in the address to which the ban applies.
|
||||||
|
optional uint32 bits = 3;
|
||||||
|
// The name of the banned user.
|
||||||
|
optional string name = 4;
|
||||||
|
// The certificate hash of the banned user.
|
||||||
|
optional string hash = 5;
|
||||||
|
// The reason for the ban.
|
||||||
|
optional string reason = 6;
|
||||||
|
// The ban start time (in epoch form).
|
||||||
|
optional int64 start = 7;
|
||||||
|
// The ban duration.
|
||||||
|
optional int64 duration_secs = 8;
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
// The server whose bans to query.
|
||||||
|
optional Server server = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message List {
|
||||||
|
// The server for which the bans apply.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The bans.
|
||||||
|
repeated Ban bans = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ACL {
|
||||||
|
enum Permission {
|
||||||
|
None = 0x00;
|
||||||
|
Write = 0x01;
|
||||||
|
Traverse = 0x02;
|
||||||
|
Enter = 0x04;
|
||||||
|
Speak = 0x08;
|
||||||
|
Whisper = 0x100;
|
||||||
|
MuteDeafen = 0x10;
|
||||||
|
Move = 0x20;
|
||||||
|
MakeChannel = 0x40;
|
||||||
|
MakeTemporaryChannel = 0x400;
|
||||||
|
LinkChannel = 0x80;
|
||||||
|
TextMessage = 0x200;
|
||||||
|
|
||||||
|
Kick = 0x10000;
|
||||||
|
Ban = 0x20000;
|
||||||
|
Register = 0x40000;
|
||||||
|
RegisterSelf = 0x80000;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Group {
|
||||||
|
// The ACL group name.
|
||||||
|
optional string name = 1;
|
||||||
|
// Is the group inherited?
|
||||||
|
optional bool inherited = 2;
|
||||||
|
// Does the group inherit members?
|
||||||
|
optional bool inherit = 3;
|
||||||
|
// Can this group be inherited by its children?
|
||||||
|
optional bool inheritable = 4;
|
||||||
|
|
||||||
|
// The users explicitly added by this group.
|
||||||
|
repeated DatabaseUser users_add = 5;
|
||||||
|
// The users explicitly removed by this group.
|
||||||
|
repeated DatabaseUser users_remove = 6;
|
||||||
|
// All of the users who are part of this group.
|
||||||
|
repeated DatabaseUser users = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the ACL apply to the current channel?
|
||||||
|
optional bool apply_here = 3;
|
||||||
|
// Does the ACL apply to the current channel's sub-channels?
|
||||||
|
optional bool apply_subs = 4;
|
||||||
|
// Was the ACL inherited?
|
||||||
|
optional bool inherited = 5;
|
||||||
|
|
||||||
|
// The user to whom the ACL applies.
|
||||||
|
optional DatabaseUser user = 6;
|
||||||
|
// The group to whom the ACL applies.
|
||||||
|
optional ACL.Group group = 7;
|
||||||
|
|
||||||
|
// The permissions granted by the ACL (bitmask of ACL.Permission).
|
||||||
|
optional uint32 allow = 8;
|
||||||
|
// The permissions denied by the ACL (bitmask of ACL.Permission).
|
||||||
|
optional uint32 deny = 9;
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
// The server where the user and channel exist.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The user to query.
|
||||||
|
optional User user = 2;
|
||||||
|
// The channel to query.
|
||||||
|
optional Channel channel = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message List {
|
||||||
|
// The server on which the ACLs exist.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The channel to which the ACL refers.
|
||||||
|
optional Channel channel = 2;
|
||||||
|
// The ACLs part of the given channel.
|
||||||
|
repeated ACL acls = 3;
|
||||||
|
// The groups part of the given channel.
|
||||||
|
repeated ACL.Group groups = 4;
|
||||||
|
// Should ACLs be inherited from the parent channel.
|
||||||
|
optional bool inherit = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TemporaryGroup {
|
||||||
|
// The server where the temporary group exists.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The channel to which the temporary user group is added.
|
||||||
|
optional Channel channel = 2;
|
||||||
|
// The user who is added to the group.
|
||||||
|
optional User user = 3;
|
||||||
|
// The name of the temporary group.
|
||||||
|
optional string name = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Authenticator {
|
||||||
|
message Request {
|
||||||
|
// An authentication request for a connecting user.
|
||||||
|
message Authenticate {
|
||||||
|
// The user's name.
|
||||||
|
optional string name = 1;
|
||||||
|
// The user's password.
|
||||||
|
optional string password = 2;
|
||||||
|
// The user's certificate chain in DER format.
|
||||||
|
repeated bytes certificates = 3;
|
||||||
|
// The hexadecimal hash of the user's certificate.
|
||||||
|
optional string certificate_hash = 4;
|
||||||
|
// If the user is connecting with a strong certificate.
|
||||||
|
optional bool strong_certificate = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A request for information about a user, given by either the user's ID
|
||||||
|
// or name.
|
||||||
|
message Find {
|
||||||
|
// The user's ID used for lookup.
|
||||||
|
optional uint32 id = 1;
|
||||||
|
// The user's name used for lookup.
|
||||||
|
optional string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A query of all the registered users, optionally filtered by the given
|
||||||
|
// filter string.
|
||||||
|
message Query {
|
||||||
|
// A user name filter (% is often used as a wildcard)
|
||||||
|
optional string filter = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A request for a new user registration.
|
||||||
|
message Register {
|
||||||
|
// The database user to register.
|
||||||
|
optional DatabaseUser user = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A request for deregistering a registered user.
|
||||||
|
message Deregister {
|
||||||
|
// The database user to deregister.
|
||||||
|
optional DatabaseUser user = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A request to update a registered user's information. The information
|
||||||
|
// provided should be merged with existing data.
|
||||||
|
message Update {
|
||||||
|
// The database user to update.
|
||||||
|
optional DatabaseUser user = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Authenticate authenticate = 1;
|
||||||
|
optional Find find = 2;
|
||||||
|
optional Query query = 3;
|
||||||
|
optional Register register = 4;
|
||||||
|
optional Deregister deregister = 5;
|
||||||
|
optional Update update = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
// The initialization for the authenticator stream. This message must be
|
||||||
|
// sent before authentication requests will start streaming.
|
||||||
|
message Initialize {
|
||||||
|
optional Server server = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
// The request should fallthrough to murmur's default action.
|
||||||
|
Fallthrough = 0;
|
||||||
|
// The request was successful.
|
||||||
|
Success = 1;
|
||||||
|
// The request failed; there was some error.
|
||||||
|
Failure = 2;
|
||||||
|
// A temporary failure prevented the request from succeeding (e.g. a
|
||||||
|
// database was unavailable).
|
||||||
|
TemporaryFailure = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Authenticate {
|
||||||
|
// The status of the request.
|
||||||
|
optional Status status = 1;
|
||||||
|
// The user's registered ID.
|
||||||
|
optional uint32 id = 2;
|
||||||
|
// The corrected user's name;
|
||||||
|
optional string name = 3;
|
||||||
|
// Additional ACL groups that the user belongs too.
|
||||||
|
repeated ACL.Group groups = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Find {
|
||||||
|
// The database user (if found).
|
||||||
|
optional DatabaseUser user = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
// The matched database users.
|
||||||
|
repeated DatabaseUser users = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Register {
|
||||||
|
// The status of the request.
|
||||||
|
optional Status status = 1;
|
||||||
|
// The registered database user (must contain the registered user's ID).
|
||||||
|
optional DatabaseUser user = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Deregister {
|
||||||
|
// The status of the request.
|
||||||
|
optional Status status = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Update {
|
||||||
|
// The status of the request.
|
||||||
|
optional Status status = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Initialize initialize = 1;
|
||||||
|
optional Authenticate authenticate = 2;
|
||||||
|
optional Find find = 3;
|
||||||
|
optional Query query = 4;
|
||||||
|
optional Register register = 5;
|
||||||
|
optional Deregister deregister = 6;
|
||||||
|
optional Update update = 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message DatabaseUser {
|
||||||
|
// The server on which the user is registered.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The unique user ID.
|
||||||
|
optional uint32 id = 2;
|
||||||
|
// The user's name.
|
||||||
|
optional string name = 3;
|
||||||
|
// The user's email address.
|
||||||
|
optional string email = 4;
|
||||||
|
// The user's comment.
|
||||||
|
optional string comment = 5;
|
||||||
|
// The user's certificate hash.
|
||||||
|
optional string hash = 6;
|
||||||
|
// The user's password (never sent; used only when updating).
|
||||||
|
optional string password = 7;
|
||||||
|
// When the user was last on the server.
|
||||||
|
optional string last_active = 8;
|
||||||
|
// The user's texture.
|
||||||
|
optional bytes texture = 9;
|
||||||
|
|
||||||
|
message Query {
|
||||||
|
// The server whose users will be queried.
|
||||||
|
optional Server server = 1;
|
||||||
|
// A string to filter the users by.
|
||||||
|
optional string filter = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message List {
|
||||||
|
// The server on which the users are registered.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The users.
|
||||||
|
repeated DatabaseUser users = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Verify {
|
||||||
|
// The server on which the user-password pair will be authenticated.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The user's name.
|
||||||
|
optional string name = 2;
|
||||||
|
// The user's password.
|
||||||
|
optional string password = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message RedirectWhisperGroup {
|
||||||
|
// The server on which the whisper redirection will take place.
|
||||||
|
optional Server server = 1;
|
||||||
|
// The user to whom the redirection will be applied.
|
||||||
|
optional User user = 2;
|
||||||
|
// The source group.
|
||||||
|
optional ACL.Group source = 3;
|
||||||
|
// The target group.
|
||||||
|
optional ACL.Group target = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
service V1 {
|
||||||
|
//
|
||||||
|
// Meta
|
||||||
|
//
|
||||||
|
|
||||||
|
// GetUptime returns murmur's uptime.
|
||||||
|
rpc GetUptime(Void) returns(Uptime);
|
||||||
|
// GetVersion returns murmur's version.
|
||||||
|
rpc GetVersion(Void) returns(Version);
|
||||||
|
// Events returns a stream of murmur events.
|
||||||
|
rpc Events(Void) returns(stream Event);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Servers
|
||||||
|
//
|
||||||
|
|
||||||
|
// ServerCreate creates a new virtual server. The returned server object
|
||||||
|
// contains the newly created server's ID.
|
||||||
|
rpc ServerCreate(Void) returns(Server);
|
||||||
|
// ServerQuery returns a list of servers that match the given query.
|
||||||
|
rpc ServerQuery(Server.Query) returns(Server.List);
|
||||||
|
// ServerGet returns information about the given server.
|
||||||
|
rpc ServerGet(Server) returns(Server);
|
||||||
|
// ServerStart starts the given stopped server.
|
||||||
|
rpc ServerStart(Server) returns(Void);
|
||||||
|
// ServerStop stops the given virtual server.
|
||||||
|
rpc ServerStop(Server) returns(Void);
|
||||||
|
// ServerRemove removes the given virtual server and its configuration.
|
||||||
|
rpc ServerRemove(Server) returns(Void);
|
||||||
|
// ServerEvents returns a stream of events that happen on the given server.
|
||||||
|
rpc ServerEvents(Server) returns(stream Server.Event);
|
||||||
|
|
||||||
|
//
|
||||||
|
// ContextActions
|
||||||
|
//
|
||||||
|
|
||||||
|
// ContextActionAdd adds a context action to the given user's client. The
|
||||||
|
// following ContextAction fields must be set:
|
||||||
|
// context, action, text, and user.
|
||||||
|
//
|
||||||
|
// Added context actions are valid until:
|
||||||
|
// - The context action is removed with ContextActionRemove, or
|
||||||
|
// - The user disconnects from the server, or
|
||||||
|
// - The server stops.
|
||||||
|
rpc ContextActionAdd(ContextAction) returns(Void);
|
||||||
|
// ContextActionRemove removes a context action from the given user's client.
|
||||||
|
// The following ContextAction must be set:
|
||||||
|
// action
|
||||||
|
// If no user is given, the context action is removed from all users.
|
||||||
|
rpc ContextActionRemove(ContextAction) returns(Void);
|
||||||
|
// ContextActionEvents returns a stream of context action events that are
|
||||||
|
// triggered by users.
|
||||||
|
rpc ContextActionEvents(ContextAction) returns(stream ContextAction);
|
||||||
|
|
||||||
|
//
|
||||||
|
// TextMessage
|
||||||
|
//
|
||||||
|
|
||||||
|
// TextMessageSend sends the given TextMessage to the server.
|
||||||
|
//
|
||||||
|
// If no users, channels, or trees are added to the TextMessage, the message
|
||||||
|
// will be broadcast the entire server. Otherwise, the message will be
|
||||||
|
// targeted to the specified users, channels, and trees.
|
||||||
|
rpc TextMessageSend(TextMessage) returns(Void);
|
||||||
|
// TextMessageFilter filters text messages on a given server.
|
||||||
|
|
||||||
|
// TextMessageFilter filters text messages for a given server.
|
||||||
|
//
|
||||||
|
// When a filter stream is active, text messages sent from users to the
|
||||||
|
// server are sent over the stream. The RPC client then sends a message back
|
||||||
|
// on the same stream, containing an action: whether the message should be
|
||||||
|
// accepted, rejected, or dropped.
|
||||||
|
//
|
||||||
|
// To activate the filter stream, an initial TextMessage.Filter message must
|
||||||
|
// be sent that contains the server on which the filter will be active.
|
||||||
|
rpc TextMessageFilter(stream TextMessage.Filter) returns(stream TextMessage.Filter);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Logs
|
||||||
|
//
|
||||||
|
|
||||||
|
// LogQuery returns a list of log entries from the given server.
|
||||||
|
//
|
||||||
|
// To get the total number of log entries, omit min and/or max from the
|
||||||
|
// query.
|
||||||
|
rpc LogQuery(Log.Query) returns(Log.List);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Config
|
||||||
|
//
|
||||||
|
|
||||||
|
// ConfigGet returns the explicitly set configuration for the given server.
|
||||||
|
rpc ConfigGet(Server) returns(Config);
|
||||||
|
// ConfigGetField returns the configuration value for the given key.
|
||||||
|
rpc ConfigGetField(Config.Field) returns(Config.Field);
|
||||||
|
// ConfigSetField sets the configuration value to the given value.
|
||||||
|
rpc ConfigSetField(Config.Field) returns(Void);
|
||||||
|
// ConfigGetDefault returns the default server configuration.
|
||||||
|
rpc ConfigGetDefault(Void) returns(Config);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Channels
|
||||||
|
//
|
||||||
|
|
||||||
|
// ChannelQuery returns a list of channels that match the given query.
|
||||||
|
rpc ChannelQuery(Channel.Query) returns(Channel.List);
|
||||||
|
// ChannelGet returns the channel with the given ID.
|
||||||
|
rpc ChannelGet(Channel) returns(Channel);
|
||||||
|
// ChannelAdd adds the channel to the given server. The parent and name of
|
||||||
|
// the channel must be set.
|
||||||
|
rpc ChannelAdd(Channel) returns(Channel);
|
||||||
|
// ChannelRemove removes the given channel from the server.
|
||||||
|
rpc ChannelRemove(Channel) returns(Void);
|
||||||
|
// ChannelUpdate updates the given channel's attributes. Only the fields that
|
||||||
|
// are set will be updated.
|
||||||
|
rpc ChannelUpdate(Channel) returns(Channel);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Users
|
||||||
|
//
|
||||||
|
|
||||||
|
// UserQuery returns a list of connected users who match the given query.
|
||||||
|
rpc UserQuery(User.Query) returns(User.List);
|
||||||
|
// UserGet returns information on the connected user, given by the user's
|
||||||
|
// session or name.
|
||||||
|
rpc UserGet(User) returns(User);
|
||||||
|
// UserUpdate changes the given user's state. Only the following fields can
|
||||||
|
// be changed:
|
||||||
|
// name, mute, deaf, suppress, priority_speaker, channel, comment.
|
||||||
|
rpc UserUpdate(User) returns(User);
|
||||||
|
// UserKick kicks the user from the server.
|
||||||
|
rpc UserKick(User.Kick) returns(Void);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Tree
|
||||||
|
//
|
||||||
|
|
||||||
|
// TreeQuery returns a representation of the given server's channel/user
|
||||||
|
// tree.
|
||||||
|
rpc TreeQuery(Tree.Query) returns(Tree);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Bans
|
||||||
|
//
|
||||||
|
|
||||||
|
// BansGet returns a list of bans for the given server.
|
||||||
|
rpc BansGet(Ban.Query) returns(Ban.List);
|
||||||
|
// BansSet replaces the server's ban list with the given list.
|
||||||
|
rpc BansSet(Ban.List) returns(Void);
|
||||||
|
|
||||||
|
//
|
||||||
|
// ACL
|
||||||
|
//
|
||||||
|
|
||||||
|
// ACLGet returns the ACL for the given channel.
|
||||||
|
rpc ACLGet(Channel) returns(ACL.List);
|
||||||
|
// ACLSet overrides the ACL of the given channel to what is provided.
|
||||||
|
rpc ACLSet(ACL.List) returns(Void);
|
||||||
|
// ACLGetEffectivePermissions returns the effective permissions for the given
|
||||||
|
// user in the given channel.
|
||||||
|
rpc ACLGetEffectivePermissions(ACL.Query) returns(ACL);
|
||||||
|
// ACLAddTemporaryGroup adds a user to a temporary group.
|
||||||
|
rpc ACLAddTemporaryGroup(ACL.TemporaryGroup) returns(Void);
|
||||||
|
// ACLRemoveTemporaryGroup removes a user from a temporary group.
|
||||||
|
rpc ACLRemoveTemporaryGroup(ACL.TemporaryGroup) returns(Void);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Authenticator
|
||||||
|
//
|
||||||
|
|
||||||
|
// AuthenticatorStream opens an authentication stream to the server.
|
||||||
|
//
|
||||||
|
// There can only be one RPC client with an open Stream. If a new
|
||||||
|
// authenticator connects, the open connected will be closed.
|
||||||
|
rpc AuthenticatorStream(stream Authenticator.Response) returns(stream Authenticator.Request);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Database
|
||||||
|
//
|
||||||
|
|
||||||
|
// DatabaseUserQuery returns a list of registered users who match given
|
||||||
|
// query.
|
||||||
|
rpc DatabaseUserQuery(DatabaseUser.Query) returns(DatabaseUser.List);
|
||||||
|
// DatabaseUserGet returns the database user with the given ID.
|
||||||
|
rpc DatabaseUserGet(DatabaseUser) returns(DatabaseUser);
|
||||||
|
// DatabaseUserUpdate updates the given database user.
|
||||||
|
rpc DatabaseUserUpdate(DatabaseUser) returns(Void);
|
||||||
|
// DatabaseUserRegister registers a user with the given information on the
|
||||||
|
// server. The returned DatabaseUser will contain the newly registered user's
|
||||||
|
// ID.
|
||||||
|
rpc DatabaseUserRegister(DatabaseUser) returns(DatabaseUser);
|
||||||
|
// DatabaseUserDeregister deregisters the given user.
|
||||||
|
rpc DatabaseUserDeregister(DatabaseUser) returns(Void);
|
||||||
|
// DatabaseUserVerify verifies the that the given user-password pair is
|
||||||
|
// correct.
|
||||||
|
rpc DatabaseUserVerify(DatabaseUser.Verify) returns(DatabaseUser);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Audio
|
||||||
|
//
|
||||||
|
|
||||||
|
// AddRedirectWhisperGroup add a whisper targets redirection for the given
|
||||||
|
// user. Whenever a user whispers to group "source", the whisper will be
|
||||||
|
// redirected to group "target".
|
||||||
|
rpc RedirectWhisperGroupAdd(RedirectWhisperGroup) returns(Void);
|
||||||
|
|
||||||
|
// RemoveRedirectWhisperGroup removes a whisper target redirection for
|
||||||
|
// the the given user.
|
||||||
|
rpc RedirectWhisperGroupRemove(RedirectWhisperGroup) returns(Void);
|
||||||
|
}
|
3881
mumble/MurmurRPC_pb2.py
Normal file
3881
mumble/MurmurRPC_pb2.py
Normal file
File diff suppressed because one or more lines are too long
912
mumble/MurmurRPC_pb2_grpc.py
Normal file
912
mumble/MurmurRPC_pb2_grpc.py
Normal file
@ -0,0 +1,912 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
import MurmurRPC_pb2 as MurmurRPC__pb2
|
||||||
|
|
||||||
|
|
||||||
|
class V1Stub(object):
|
||||||
|
"""
|
||||||
|
Meta
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.GetUptime = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/GetUptime',
|
||||||
|
request_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Uptime.FromString,
|
||||||
|
)
|
||||||
|
self.GetVersion = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/GetVersion',
|
||||||
|
request_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Version.FromString,
|
||||||
|
)
|
||||||
|
self.Events = channel.unary_stream(
|
||||||
|
'/MurmurRPC.V1/Events',
|
||||||
|
request_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Event.FromString,
|
||||||
|
)
|
||||||
|
self.ServerCreate = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ServerCreate',
|
||||||
|
request_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Server.FromString,
|
||||||
|
)
|
||||||
|
self.ServerQuery = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ServerQuery',
|
||||||
|
request_serializer=MurmurRPC__pb2.Server.Query.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Server.List.FromString,
|
||||||
|
)
|
||||||
|
self.ServerGet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ServerGet',
|
||||||
|
request_serializer=MurmurRPC__pb2.Server.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Server.FromString,
|
||||||
|
)
|
||||||
|
self.ServerStart = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ServerStart',
|
||||||
|
request_serializer=MurmurRPC__pb2.Server.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ServerStop = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ServerStop',
|
||||||
|
request_serializer=MurmurRPC__pb2.Server.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ServerRemove = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ServerRemove',
|
||||||
|
request_serializer=MurmurRPC__pb2.Server.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ServerEvents = channel.unary_stream(
|
||||||
|
'/MurmurRPC.V1/ServerEvents',
|
||||||
|
request_serializer=MurmurRPC__pb2.Server.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Server.Event.FromString,
|
||||||
|
)
|
||||||
|
self.ContextActionAdd = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ContextActionAdd',
|
||||||
|
request_serializer=MurmurRPC__pb2.ContextAction.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ContextActionRemove = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ContextActionRemove',
|
||||||
|
request_serializer=MurmurRPC__pb2.ContextAction.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ContextActionEvents = channel.unary_stream(
|
||||||
|
'/MurmurRPC.V1/ContextActionEvents',
|
||||||
|
request_serializer=MurmurRPC__pb2.ContextAction.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.ContextAction.FromString,
|
||||||
|
)
|
||||||
|
self.TextMessageSend = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/TextMessageSend',
|
||||||
|
request_serializer=MurmurRPC__pb2.TextMessage.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.TextMessageFilter = channel.stream_stream(
|
||||||
|
'/MurmurRPC.V1/TextMessageFilter',
|
||||||
|
request_serializer=MurmurRPC__pb2.TextMessage.Filter.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.TextMessage.Filter.FromString,
|
||||||
|
)
|
||||||
|
self.LogQuery = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/LogQuery',
|
||||||
|
request_serializer=MurmurRPC__pb2.Log.Query.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Log.List.FromString,
|
||||||
|
)
|
||||||
|
self.ConfigGet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ConfigGet',
|
||||||
|
request_serializer=MurmurRPC__pb2.Server.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Config.FromString,
|
||||||
|
)
|
||||||
|
self.ConfigGetField = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ConfigGetField',
|
||||||
|
request_serializer=MurmurRPC__pb2.Config.Field.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Config.Field.FromString,
|
||||||
|
)
|
||||||
|
self.ConfigSetField = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ConfigSetField',
|
||||||
|
request_serializer=MurmurRPC__pb2.Config.Field.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ConfigGetDefault = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ConfigGetDefault',
|
||||||
|
request_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Config.FromString,
|
||||||
|
)
|
||||||
|
self.ChannelQuery = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ChannelQuery',
|
||||||
|
request_serializer=MurmurRPC__pb2.Channel.Query.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Channel.List.FromString,
|
||||||
|
)
|
||||||
|
self.ChannelGet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ChannelGet',
|
||||||
|
request_serializer=MurmurRPC__pb2.Channel.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Channel.FromString,
|
||||||
|
)
|
||||||
|
self.ChannelAdd = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ChannelAdd',
|
||||||
|
request_serializer=MurmurRPC__pb2.Channel.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Channel.FromString,
|
||||||
|
)
|
||||||
|
self.ChannelRemove = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ChannelRemove',
|
||||||
|
request_serializer=MurmurRPC__pb2.Channel.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ChannelUpdate = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ChannelUpdate',
|
||||||
|
request_serializer=MurmurRPC__pb2.Channel.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Channel.FromString,
|
||||||
|
)
|
||||||
|
self.UserQuery = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/UserQuery',
|
||||||
|
request_serializer=MurmurRPC__pb2.User.Query.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.User.List.FromString,
|
||||||
|
)
|
||||||
|
self.UserGet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/UserGet',
|
||||||
|
request_serializer=MurmurRPC__pb2.User.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.User.FromString,
|
||||||
|
)
|
||||||
|
self.UserUpdate = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/UserUpdate',
|
||||||
|
request_serializer=MurmurRPC__pb2.User.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.User.FromString,
|
||||||
|
)
|
||||||
|
self.UserKick = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/UserKick',
|
||||||
|
request_serializer=MurmurRPC__pb2.User.Kick.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.TreeQuery = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/TreeQuery',
|
||||||
|
request_serializer=MurmurRPC__pb2.Tree.Query.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Tree.FromString,
|
||||||
|
)
|
||||||
|
self.BansGet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/BansGet',
|
||||||
|
request_serializer=MurmurRPC__pb2.Ban.Query.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Ban.List.FromString,
|
||||||
|
)
|
||||||
|
self.BansSet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/BansSet',
|
||||||
|
request_serializer=MurmurRPC__pb2.Ban.List.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ACLGet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ACLGet',
|
||||||
|
request_serializer=MurmurRPC__pb2.Channel.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.ACL.List.FromString,
|
||||||
|
)
|
||||||
|
self.ACLSet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ACLSet',
|
||||||
|
request_serializer=MurmurRPC__pb2.ACL.List.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ACLGetEffectivePermissions = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ACLGetEffectivePermissions',
|
||||||
|
request_serializer=MurmurRPC__pb2.ACL.Query.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.ACL.FromString,
|
||||||
|
)
|
||||||
|
self.ACLAddTemporaryGroup = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ACLAddTemporaryGroup',
|
||||||
|
request_serializer=MurmurRPC__pb2.ACL.TemporaryGroup.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.ACLRemoveTemporaryGroup = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/ACLRemoveTemporaryGroup',
|
||||||
|
request_serializer=MurmurRPC__pb2.ACL.TemporaryGroup.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.AuthenticatorStream = channel.stream_stream(
|
||||||
|
'/MurmurRPC.V1/AuthenticatorStream',
|
||||||
|
request_serializer=MurmurRPC__pb2.Authenticator.Response.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Authenticator.Request.FromString,
|
||||||
|
)
|
||||||
|
self.DatabaseUserQuery = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/DatabaseUserQuery',
|
||||||
|
request_serializer=MurmurRPC__pb2.DatabaseUser.Query.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.DatabaseUser.List.FromString,
|
||||||
|
)
|
||||||
|
self.DatabaseUserGet = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/DatabaseUserGet',
|
||||||
|
request_serializer=MurmurRPC__pb2.DatabaseUser.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.DatabaseUser.FromString,
|
||||||
|
)
|
||||||
|
self.DatabaseUserUpdate = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/DatabaseUserUpdate',
|
||||||
|
request_serializer=MurmurRPC__pb2.DatabaseUser.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.DatabaseUserRegister = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/DatabaseUserRegister',
|
||||||
|
request_serializer=MurmurRPC__pb2.DatabaseUser.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.DatabaseUser.FromString,
|
||||||
|
)
|
||||||
|
self.DatabaseUserDeregister = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/DatabaseUserDeregister',
|
||||||
|
request_serializer=MurmurRPC__pb2.DatabaseUser.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.DatabaseUserVerify = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/DatabaseUserVerify',
|
||||||
|
request_serializer=MurmurRPC__pb2.DatabaseUser.Verify.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.DatabaseUser.FromString,
|
||||||
|
)
|
||||||
|
self.RedirectWhisperGroupAdd = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/RedirectWhisperGroupAdd',
|
||||||
|
request_serializer=MurmurRPC__pb2.RedirectWhisperGroup.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
self.RedirectWhisperGroupRemove = channel.unary_unary(
|
||||||
|
'/MurmurRPC.V1/RedirectWhisperGroupRemove',
|
||||||
|
request_serializer=MurmurRPC__pb2.RedirectWhisperGroup.SerializeToString,
|
||||||
|
response_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class V1Servicer(object):
|
||||||
|
"""
|
||||||
|
Meta
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def GetUptime(self, request, context):
|
||||||
|
"""GetUptime returns murmur's uptime.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def GetVersion(self, request, context):
|
||||||
|
"""GetVersion returns murmur's version.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def Events(self, request, context):
|
||||||
|
"""Events returns a stream of murmur events.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ServerCreate(self, request, context):
|
||||||
|
"""
|
||||||
|
Servers
|
||||||
|
|
||||||
|
|
||||||
|
ServerCreate creates a new virtual server. The returned server object
|
||||||
|
contains the newly created server's ID.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ServerQuery(self, request, context):
|
||||||
|
"""ServerQuery returns a list of servers that match the given query.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ServerGet(self, request, context):
|
||||||
|
"""ServerGet returns information about the given server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ServerStart(self, request, context):
|
||||||
|
"""ServerStart starts the given stopped server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ServerStop(self, request, context):
|
||||||
|
"""ServerStop stops the given virtual server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ServerRemove(self, request, context):
|
||||||
|
"""ServerRemove removes the given virtual server and its configuration.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ServerEvents(self, request, context):
|
||||||
|
"""ServerEvents returns a stream of events that happen on the given server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ContextActionAdd(self, request, context):
|
||||||
|
"""
|
||||||
|
ContextActions
|
||||||
|
|
||||||
|
|
||||||
|
ContextActionAdd adds a context action to the given user's client. The
|
||||||
|
following ContextAction fields must be set:
|
||||||
|
context, action, text, and user.
|
||||||
|
|
||||||
|
Added context actions are valid until:
|
||||||
|
- The context action is removed with ContextActionRemove, or
|
||||||
|
- The user disconnects from the server, or
|
||||||
|
- The server stops.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ContextActionRemove(self, request, context):
|
||||||
|
"""ContextActionRemove removes a context action from the given user's client.
|
||||||
|
The following ContextAction must be set:
|
||||||
|
action
|
||||||
|
If no user is given, the context action is removed from all users.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ContextActionEvents(self, request, context):
|
||||||
|
"""ContextActionEvents returns a stream of context action events that are
|
||||||
|
triggered by users.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def TextMessageSend(self, request, context):
|
||||||
|
"""
|
||||||
|
TextMessage
|
||||||
|
|
||||||
|
|
||||||
|
TextMessageSend sends the given TextMessage to the server.
|
||||||
|
|
||||||
|
If no users, channels, or trees are added to the TextMessage, the message
|
||||||
|
will be broadcast the entire server. Otherwise, the message will be
|
||||||
|
targeted to the specified users, channels, and trees.
|
||||||
|
TextMessageFilter filters text messages on a given server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def TextMessageFilter(self, request_iterator, context):
|
||||||
|
"""TextMessageFilter filters text messages for a given server.
|
||||||
|
|
||||||
|
When a filter stream is active, text messages sent from users to the
|
||||||
|
server are sent over the stream. The RPC client then sends a message back
|
||||||
|
on the same stream, containing an action: whether the message should be
|
||||||
|
accepted, rejected, or dropped.
|
||||||
|
|
||||||
|
To activate the filter stream, an initial TextMessage.Filter message must
|
||||||
|
be sent that contains the server on which the filter will be active.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def LogQuery(self, request, context):
|
||||||
|
"""
|
||||||
|
Logs
|
||||||
|
|
||||||
|
|
||||||
|
LogQuery returns a list of log entries from the given server.
|
||||||
|
|
||||||
|
To get the total number of log entries, omit min and/or max from the
|
||||||
|
query.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ConfigGet(self, request, context):
|
||||||
|
"""
|
||||||
|
Config
|
||||||
|
|
||||||
|
|
||||||
|
ConfigGet returns the explicitly set configuration for the given server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ConfigGetField(self, request, context):
|
||||||
|
"""ConfigGetField returns the configuration value for the given key.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ConfigSetField(self, request, context):
|
||||||
|
"""ConfigSetField sets the configuration value to the given value.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ConfigGetDefault(self, request, context):
|
||||||
|
"""ConfigGetDefault returns the default server configuration.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ChannelQuery(self, request, context):
|
||||||
|
"""
|
||||||
|
Channels
|
||||||
|
|
||||||
|
|
||||||
|
ChannelQuery returns a list of channels that match the given query.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ChannelGet(self, request, context):
|
||||||
|
"""ChannelGet returns the channel with the given ID.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ChannelAdd(self, request, context):
|
||||||
|
"""ChannelAdd adds the channel to the given server. The parent and name of
|
||||||
|
the channel must be set.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ChannelRemove(self, request, context):
|
||||||
|
"""ChannelRemove removes the given channel from the server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ChannelUpdate(self, request, context):
|
||||||
|
"""ChannelUpdate updates the given channel's attributes. Only the fields that
|
||||||
|
are set will be updated.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def UserQuery(self, request, context):
|
||||||
|
"""
|
||||||
|
Users
|
||||||
|
|
||||||
|
|
||||||
|
UserQuery returns a list of connected users who match the given query.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def UserGet(self, request, context):
|
||||||
|
"""UserGet returns information on the connected user, given by the user's
|
||||||
|
session or name.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def UserUpdate(self, request, context):
|
||||||
|
"""UserUpdate changes the given user's state. Only the following fields can
|
||||||
|
be changed:
|
||||||
|
name, mute, deaf, suppress, priority_speaker, channel, comment.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def UserKick(self, request, context):
|
||||||
|
"""UserKick kicks the user from the server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def TreeQuery(self, request, context):
|
||||||
|
"""
|
||||||
|
Tree
|
||||||
|
|
||||||
|
|
||||||
|
TreeQuery returns a representation of the given server's channel/user
|
||||||
|
tree.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def BansGet(self, request, context):
|
||||||
|
"""
|
||||||
|
Bans
|
||||||
|
|
||||||
|
|
||||||
|
BansGet returns a list of bans for the given server.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def BansSet(self, request, context):
|
||||||
|
"""BansSet replaces the server's ban list with the given list.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ACLGet(self, request, context):
|
||||||
|
"""
|
||||||
|
ACL
|
||||||
|
|
||||||
|
|
||||||
|
ACLGet returns the ACL for the given channel.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ACLSet(self, request, context):
|
||||||
|
"""ACLSet overrides the ACL of the given channel to what is provided.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ACLGetEffectivePermissions(self, request, context):
|
||||||
|
"""ACLGetEffectivePermissions returns the effective permissions for the given
|
||||||
|
user in the given channel.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ACLAddTemporaryGroup(self, request, context):
|
||||||
|
"""ACLAddTemporaryGroup adds a user to a temporary group.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def ACLRemoveTemporaryGroup(self, request, context):
|
||||||
|
"""ACLRemoveTemporaryGroup removes a user from a temporary group.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def AuthenticatorStream(self, request_iterator, context):
|
||||||
|
"""
|
||||||
|
Authenticator
|
||||||
|
|
||||||
|
|
||||||
|
AuthenticatorStream opens an authentication stream to the server.
|
||||||
|
|
||||||
|
There can only be one RPC client with an open Stream. If a new
|
||||||
|
authenticator connects, the open connected will be closed.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def DatabaseUserQuery(self, request, context):
|
||||||
|
"""
|
||||||
|
Database
|
||||||
|
|
||||||
|
|
||||||
|
DatabaseUserQuery returns a list of registered users who match given
|
||||||
|
query.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def DatabaseUserGet(self, request, context):
|
||||||
|
"""DatabaseUserGet returns the database user with the given ID.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def DatabaseUserUpdate(self, request, context):
|
||||||
|
"""DatabaseUserUpdate updates the given database user.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def DatabaseUserRegister(self, request, context):
|
||||||
|
"""DatabaseUserRegister registers a user with the given information on the
|
||||||
|
server. The returned DatabaseUser will contain the newly registered user's
|
||||||
|
ID.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def DatabaseUserDeregister(self, request, context):
|
||||||
|
"""DatabaseUserDeregister deregisters the given user.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def DatabaseUserVerify(self, request, context):
|
||||||
|
"""DatabaseUserVerify verifies the that the given user-password pair is
|
||||||
|
correct.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def RedirectWhisperGroupAdd(self, request, context):
|
||||||
|
"""
|
||||||
|
Audio
|
||||||
|
|
||||||
|
|
||||||
|
AddRedirectWhisperGroup add a whisper targets redirection for the given
|
||||||
|
user. Whenever a user whispers to group "source", the whisper will be
|
||||||
|
redirected to group "target".
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def RedirectWhisperGroupRemove(self, request, context):
|
||||||
|
"""RemoveRedirectWhisperGroup removes a whisper target redirection for
|
||||||
|
the the given user.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
|
||||||
|
def add_V1Servicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
'GetUptime': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.GetUptime,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Uptime.SerializeToString,
|
||||||
|
),
|
||||||
|
'GetVersion': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.GetVersion,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Version.SerializeToString,
|
||||||
|
),
|
||||||
|
'Events': grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.Events,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Event.SerializeToString,
|
||||||
|
),
|
||||||
|
'ServerCreate': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ServerCreate,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Server.SerializeToString,
|
||||||
|
),
|
||||||
|
'ServerQuery': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ServerQuery,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Server.Query.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Server.List.SerializeToString,
|
||||||
|
),
|
||||||
|
'ServerGet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ServerGet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Server.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Server.SerializeToString,
|
||||||
|
),
|
||||||
|
'ServerStart': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ServerStart,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Server.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ServerStop': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ServerStop,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Server.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ServerRemove': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ServerRemove,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Server.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ServerEvents': grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.ServerEvents,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Server.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Server.Event.SerializeToString,
|
||||||
|
),
|
||||||
|
'ContextActionAdd': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ContextActionAdd,
|
||||||
|
request_deserializer=MurmurRPC__pb2.ContextAction.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ContextActionRemove': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ContextActionRemove,
|
||||||
|
request_deserializer=MurmurRPC__pb2.ContextAction.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ContextActionEvents': grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.ContextActionEvents,
|
||||||
|
request_deserializer=MurmurRPC__pb2.ContextAction.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.ContextAction.SerializeToString,
|
||||||
|
),
|
||||||
|
'TextMessageSend': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.TextMessageSend,
|
||||||
|
request_deserializer=MurmurRPC__pb2.TextMessage.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'TextMessageFilter': grpc.stream_stream_rpc_method_handler(
|
||||||
|
servicer.TextMessageFilter,
|
||||||
|
request_deserializer=MurmurRPC__pb2.TextMessage.Filter.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.TextMessage.Filter.SerializeToString,
|
||||||
|
),
|
||||||
|
'LogQuery': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.LogQuery,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Log.Query.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Log.List.SerializeToString,
|
||||||
|
),
|
||||||
|
'ConfigGet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ConfigGet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Server.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Config.SerializeToString,
|
||||||
|
),
|
||||||
|
'ConfigGetField': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ConfigGetField,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Config.Field.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Config.Field.SerializeToString,
|
||||||
|
),
|
||||||
|
'ConfigSetField': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ConfigSetField,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Config.Field.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ConfigGetDefault': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ConfigGetDefault,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Void.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Config.SerializeToString,
|
||||||
|
),
|
||||||
|
'ChannelQuery': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ChannelQuery,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Channel.Query.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Channel.List.SerializeToString,
|
||||||
|
),
|
||||||
|
'ChannelGet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ChannelGet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Channel.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Channel.SerializeToString,
|
||||||
|
),
|
||||||
|
'ChannelAdd': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ChannelAdd,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Channel.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Channel.SerializeToString,
|
||||||
|
),
|
||||||
|
'ChannelRemove': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ChannelRemove,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Channel.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ChannelUpdate': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ChannelUpdate,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Channel.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Channel.SerializeToString,
|
||||||
|
),
|
||||||
|
'UserQuery': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.UserQuery,
|
||||||
|
request_deserializer=MurmurRPC__pb2.User.Query.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.User.List.SerializeToString,
|
||||||
|
),
|
||||||
|
'UserGet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.UserGet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.User.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.User.SerializeToString,
|
||||||
|
),
|
||||||
|
'UserUpdate': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.UserUpdate,
|
||||||
|
request_deserializer=MurmurRPC__pb2.User.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.User.SerializeToString,
|
||||||
|
),
|
||||||
|
'UserKick': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.UserKick,
|
||||||
|
request_deserializer=MurmurRPC__pb2.User.Kick.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'TreeQuery': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.TreeQuery,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Tree.Query.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Tree.SerializeToString,
|
||||||
|
),
|
||||||
|
'BansGet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.BansGet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Ban.Query.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Ban.List.SerializeToString,
|
||||||
|
),
|
||||||
|
'BansSet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.BansSet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Ban.List.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ACLGet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ACLGet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Channel.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.ACL.List.SerializeToString,
|
||||||
|
),
|
||||||
|
'ACLSet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ACLSet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.ACL.List.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ACLGetEffectivePermissions': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ACLGetEffectivePermissions,
|
||||||
|
request_deserializer=MurmurRPC__pb2.ACL.Query.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.ACL.SerializeToString,
|
||||||
|
),
|
||||||
|
'ACLAddTemporaryGroup': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ACLAddTemporaryGroup,
|
||||||
|
request_deserializer=MurmurRPC__pb2.ACL.TemporaryGroup.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'ACLRemoveTemporaryGroup': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.ACLRemoveTemporaryGroup,
|
||||||
|
request_deserializer=MurmurRPC__pb2.ACL.TemporaryGroup.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'AuthenticatorStream': grpc.stream_stream_rpc_method_handler(
|
||||||
|
servicer.AuthenticatorStream,
|
||||||
|
request_deserializer=MurmurRPC__pb2.Authenticator.Response.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Authenticator.Request.SerializeToString,
|
||||||
|
),
|
||||||
|
'DatabaseUserQuery': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.DatabaseUserQuery,
|
||||||
|
request_deserializer=MurmurRPC__pb2.DatabaseUser.Query.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.DatabaseUser.List.SerializeToString,
|
||||||
|
),
|
||||||
|
'DatabaseUserGet': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.DatabaseUserGet,
|
||||||
|
request_deserializer=MurmurRPC__pb2.DatabaseUser.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.DatabaseUser.SerializeToString,
|
||||||
|
),
|
||||||
|
'DatabaseUserUpdate': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.DatabaseUserUpdate,
|
||||||
|
request_deserializer=MurmurRPC__pb2.DatabaseUser.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'DatabaseUserRegister': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.DatabaseUserRegister,
|
||||||
|
request_deserializer=MurmurRPC__pb2.DatabaseUser.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.DatabaseUser.SerializeToString,
|
||||||
|
),
|
||||||
|
'DatabaseUserDeregister': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.DatabaseUserDeregister,
|
||||||
|
request_deserializer=MurmurRPC__pb2.DatabaseUser.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'DatabaseUserVerify': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.DatabaseUserVerify,
|
||||||
|
request_deserializer=MurmurRPC__pb2.DatabaseUser.Verify.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.DatabaseUser.SerializeToString,
|
||||||
|
),
|
||||||
|
'RedirectWhisperGroupAdd': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.RedirectWhisperGroupAdd,
|
||||||
|
request_deserializer=MurmurRPC__pb2.RedirectWhisperGroup.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
'RedirectWhisperGroupRemove': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.RedirectWhisperGroupRemove,
|
||||||
|
request_deserializer=MurmurRPC__pb2.RedirectWhisperGroup.FromString,
|
||||||
|
response_serializer=MurmurRPC__pb2.Void.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
'MurmurRPC.V1', rpc_method_handlers)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
8
mumble/TODO
Normal file
8
mumble/TODO
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-add lsChans()
|
||||||
|
-lsACL? lsBans? edit these?
|
||||||
|
-find out some way to use the ICE/GRPC interface completely
|
||||||
|
|
||||||
|
-i need to learn way more about GRPC:
|
||||||
|
https://wiki.mumble.info/wiki/GRPC
|
||||||
|
https://github.com/mumble-voip/mumble/issues/1196
|
||||||
|
https://grpc.io/docs/tutorials/basic/python.html
|
242
mumble/gencerthash.py
Executable file
242
mumble/gencerthash.py
Executable file
@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# TODO: can we use struct instead for blobParser?
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
try:
|
||||||
|
import OpenSSL # "python-pyopenssl" package on Arch
|
||||||
|
except ImportError:
|
||||||
|
exit('You need to install PyOpenSSL ("pip3 install --user PyOpenSSL" if pip3 is installed)')
|
||||||
|
|
||||||
|
## DEFINE SOME PRETTY STUFF ##
|
||||||
|
class color(object):
|
||||||
|
# Windows doesn't support ANSI color escapes like sh does.
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
# Gorram it, Windows.
|
||||||
|
# https://bugs.python.org/issue29059
|
||||||
|
# https://bugs.python.org/issue30075
|
||||||
|
# https://github.com/Microsoft/WSL/issues/1173
|
||||||
|
import subprocess
|
||||||
|
subprocess.call('', shell=True)
|
||||||
|
PURPLE = '\033[95m'
|
||||||
|
CYAN = '\033[96m'
|
||||||
|
DARKCYAN = '\033[36m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
|
END = '\033[0m'
|
||||||
|
|
||||||
|
class Hasher(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
self.args = args
|
||||||
|
self.blobGetter(self.args['cert'])
|
||||||
|
self.blobParser()
|
||||||
|
|
||||||
|
def getPass(self):
|
||||||
|
# Do we need to get the passphrase?
|
||||||
|
if self.args['passphrase']:
|
||||||
|
if self.args['passphrase'] == 'stdin':
|
||||||
|
self.args['passphrase'] = sys.stdin.read().replace('\n', '')
|
||||||
|
elif self.args['passphrase'] == 'prompt':
|
||||||
|
_colorargs = (color.BOLD, color.RED, self.args['cert'], color.END)
|
||||||
|
_repeat = True
|
||||||
|
while _repeat == True:
|
||||||
|
_pass_in = getpass.getpass(('\n{0}What is the encryption password ' +
|
||||||
|
'for {1}{2}{0}{3}{0} ?{3} ').format(*_colorargs))
|
||||||
|
if not _pass_in or _pass_in == '':
|
||||||
|
print(('\n{0}Invalid passphrase for {1}{2}{0}{3}{0} ; ' +
|
||||||
|
'please enter a valid passphrase!{3} ').format(*_colorargs))
|
||||||
|
else:
|
||||||
|
_repeat = False
|
||||||
|
self.args['passphrase'] = _pass_in.replace('\n', '')
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
self.args['passphrase'] = None
|
||||||
|
return()
|
||||||
|
|
||||||
|
def importCert(self):
|
||||||
|
self.getPass()
|
||||||
|
# Try loading the certificate
|
||||||
|
try:
|
||||||
|
self.pkcs = OpenSSL.crypto.load_pkcs12(self.cert, self.args['passphrase'])
|
||||||
|
except OpenSSL.crypto.Error:
|
||||||
|
exit('Could not load certificate! (Wrong passphrase? Wrong file?)')
|
||||||
|
return()
|
||||||
|
|
||||||
|
def hashCert(self):
|
||||||
|
self.crt_in = self.pkcs.get_certificate()
|
||||||
|
self.der = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1,
|
||||||
|
self.crt_in)
|
||||||
|
self.hash = hashlib.sha1(self.der).hexdigest().lower()
|
||||||
|
return(self.hash)
|
||||||
|
|
||||||
|
def blobGetter(self, blobpath):
|
||||||
|
self.cert = None
|
||||||
|
self.blob = None
|
||||||
|
_blst = blobpath.split(':')
|
||||||
|
if len(_blst) == 2:
|
||||||
|
blob = _blst[1]
|
||||||
|
self.certtype = _blst[0].lower()
|
||||||
|
elif len(_blst) == 1:
|
||||||
|
blob = _blst[0]
|
||||||
|
self.certtype = 'file'
|
||||||
|
else:
|
||||||
|
raise ValueError('{0} is not a supported path'.format(blobpath))
|
||||||
|
self.certtype = None
|
||||||
|
if self.certtype:
|
||||||
|
_hexblob = None
|
||||||
|
if self.certtype in ('plist', 'ini', 'file'):
|
||||||
|
blob = os.path.abspath(os.path.expanduser(blob))
|
||||||
|
if not os.path.isfile(blob):
|
||||||
|
raise FileNotFoundError('{0} does not exist'.format(blob))
|
||||||
|
if self.certtype == 'reg': # Only supported on Windows machines, obviously.
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
import winreg
|
||||||
|
elif sys.platform == 'cygwin':
|
||||||
|
# https://bitbucket.org/sfllaw/cygwinreg/issues/5/support-python3
|
||||||
|
exit(('Python 3 under Cygwin does not support reading the registry. ' +
|
||||||
|
'Please use native-Windows Python 3 (for now) or ' +
|
||||||
|
'specify an actual PKCS #12 certificate file.'))
|
||||||
|
#try:
|
||||||
|
# import cygwinreg as winreg
|
||||||
|
#except ImportError:
|
||||||
|
# exit('You must install the cygwinreg python module in your cygwin environment to read the registry.')
|
||||||
|
_keypath = blob.split('\\')
|
||||||
|
_hkey = getattr(winreg, _keypath[0])
|
||||||
|
_skeypath = _keypath[1:-1]
|
||||||
|
_ckey = _keypath[-1]
|
||||||
|
_r = winreg.OpenKey(_hkey, '\\'.join(_skeypath))
|
||||||
|
_hexblob, _ = winreg.QueryValueEx(_r, _ckey)
|
||||||
|
winreg.CloseKey(_r)
|
||||||
|
elif self.certtype == 'plist': # plistlib, however, is thankfully cross-platform.
|
||||||
|
import plistlib
|
||||||
|
with open(blob, 'rb') as f:
|
||||||
|
_pdata = plistlib.loads(f.read())
|
||||||
|
_hexblob = _pdata['net.certificate']
|
||||||
|
elif self.certtype == 'ini':
|
||||||
|
import configparser
|
||||||
|
_parser = configparser.RawConfigParser()
|
||||||
|
_parser.read(blob)
|
||||||
|
_cfg = defaultdict(dict)
|
||||||
|
for s in _parser.sections():
|
||||||
|
_cfg[s] = {}
|
||||||
|
for k in _parser.options(s):
|
||||||
|
_cfg[s][k] = _parser.get(s, k)
|
||||||
|
self.blob = _cfg['net']['certificate']
|
||||||
|
else: # It's (supposedly) a PKCS #12 file - obviously, cross-platform.
|
||||||
|
with open(blob, 'rb') as f:
|
||||||
|
self.cert = f.read()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def blobParser(self):
|
||||||
|
if not self.blob:
|
||||||
|
return()
|
||||||
|
if self.blob == '':
|
||||||
|
raise ValueError('We could not find an embedded certificate.')
|
||||||
|
# A pox upon the house of Mumble for not using base64. A POX, I SAY.
|
||||||
|
# So instead we need to straight up de-byte-array the mess.
|
||||||
|
# The below is an eldritch horror, bound to twist the mind of any sane man
|
||||||
|
# into the depths of madness.
|
||||||
|
# I probably might have been able to use a struct here, but meh.
|
||||||
|
blob = re.sub('^"?@ByteArray\(0(.*)\)"?$',
|
||||||
|
'\g<1>',
|
||||||
|
self.blob,
|
||||||
|
re.MULTILINE, re.DOTALL)
|
||||||
|
_bytes = b'0'
|
||||||
|
for s in blob.split('\\x'):
|
||||||
|
if s == '':
|
||||||
|
continue
|
||||||
|
_chunk = list(s)
|
||||||
|
# Skip the first two chars for string interpolation - they're hex.
|
||||||
|
_start = 2
|
||||||
|
try:
|
||||||
|
_hex = ''.join(_chunk[0:2])
|
||||||
|
_bytes += bytes.fromhex(_hex)
|
||||||
|
except ValueError:
|
||||||
|
# We need to zero-pad, and alter the starting index
|
||||||
|
# because yep, you guessed it - their bytearray hex vals
|
||||||
|
# (in plaintext) aren't zero-padded, either.
|
||||||
|
_hex = ''.join(_chunk[0]).zfill(2)
|
||||||
|
_bytes += bytes.fromhex(_hex)
|
||||||
|
_start = 1
|
||||||
|
# And then append the rest as-is. "Mostly."
|
||||||
|
# Namely, we need to change the single-digit null byte notation
|
||||||
|
# to actual python null bytes, and then de-escape the escapes.
|
||||||
|
# (i.e. '\t' => ' ')
|
||||||
|
_str = re.sub('\\\\0([^0])',
|
||||||
|
'\00\g<1>',
|
||||||
|
''.join(_chunk[_start:])).encode('utf-8').decode('unicode_escape')
|
||||||
|
_bytes += _str.encode('utf-8')
|
||||||
|
self.cert = _bytes
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
# Set the default cert path
|
||||||
|
_certpath = '~/Documents/MumbleAutomaticCertificateBackup.p12'
|
||||||
|
# This catches ALL versions of macOS/OS X.
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
_cfgpath = 'PLIST:~/Library/Preferences/net.sourceforge.mumble.Mumble.plist'
|
||||||
|
# ALL versions of windows, even Win10, on x86. Even 64-bit. I know.
|
||||||
|
# And Cygwin, which currently doesn't even suppport registry reading (see blobGetter()).
|
||||||
|
elif sys.platform in ('win32', 'cygwin'):
|
||||||
|
_cfgpath = r'REG:HKEY_CURRENT_USER\Software\Mumble\Mumble\net\certificate'
|
||||||
|
elif (sys.platform == 'linux') or (re.match('.*bsd.*', sys.platform)): # duh
|
||||||
|
_cfgpath = 'INI:~/.config/Mumble/Mumble.conf'
|
||||||
|
else:
|
||||||
|
# WHO KNOWS what we're running on
|
||||||
|
_cfgpath = None
|
||||||
|
if not os.path.isfile(os.path.abspath(os.path.expanduser(_certpath))):
|
||||||
|
_defcrt = _cfgpath
|
||||||
|
else:
|
||||||
|
_defcrt = 'FILE:{0}'.format(_certpath)
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
args.add_argument('-p',
|
||||||
|
'--passphrase',
|
||||||
|
choices = ['stdin', 'prompt'],
|
||||||
|
dest = 'passphrase',
|
||||||
|
default = None,
|
||||||
|
help = ('The default is to behave as if your certificate does not have ' +
|
||||||
|
'a passphrase attached (as this is Mumble\'s default); however, ' +
|
||||||
|
'if you specify \'stdin\' we will expect the passphrase to be given as a stdin pipe, ' +
|
||||||
|
'if you specify \'prompt\', we will prompt you for a passphrase (it will not be echoed back' +
|
||||||
|
'to the console)'))
|
||||||
|
args.add_argument('-c', '--cert',
|
||||||
|
dest = 'cert',
|
||||||
|
default = _defcrt,
|
||||||
|
metavar = 'path/to/mumblecert.p12',
|
||||||
|
help = ('The path to your exported PKCS #12 Mumble certificate. ' +
|
||||||
|
'Special prefixes are ' +
|
||||||
|
'{0} (it is a PKCS #12 file, default), ' +
|
||||||
|
'{1} (it is embedded in a macOS/OS X PLIST file), ' +
|
||||||
|
'{2} (it is a Mumble.conf with embedded PKCS#12), or ' +
|
||||||
|
'{3} (it is a path to a Windows registry object). ' +
|
||||||
|
'Default: {4}').format('{0}FILE{1}'.format(color.BOLD, color.END),
|
||||||
|
'{0}PLIST{1}'.format(color.BOLD, color.END),
|
||||||
|
'{0}INI{1}'.format(color.BOLD, color.END),
|
||||||
|
'{0}REG{1}'.format(color.BOLD, color.END),
|
||||||
|
'{0}{1}{2}'.format(color.BOLD, _defcrt, color.END)))
|
||||||
|
# this ^ currently prints "0m" at the end of the help message,
|
||||||
|
# all the way on the left on Windows.
|
||||||
|
# Why? Who knows; Microsoft is a mystery even to themselves.
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
cert = Hasher(args)
|
||||||
|
cert.importCert()
|
||||||
|
h = cert.hashCert()
|
||||||
|
print(('\n\t{0}Your certificate\'s public hash is: ' +
|
||||||
|
'{1}{2}{3}\n\n\t{0}Please provide this to the Mumble server administrator ' +
|
||||||
|
'that has requested it.{3}').format(color.BOLD, color.BLUE, h, color.END))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
58
mumble/getusers.py
Executable file
58
mumble/getusers.py
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import pprint
|
||||||
|
import usrmgmt2
|
||||||
|
|
||||||
|
# NOTE: THIS IS ONLY FOR TESTING/DEVELOPMENT PURPOSES.
|
||||||
|
# IT WILL BE REMOVED ONCE THE ACTUAL STUFF IS FINISHED.
|
||||||
|
|
||||||
|
args = vars(usrmgmt2.parseArgs().parse_args())
|
||||||
|
args['operation'] = 'ls'
|
||||||
|
args['verbose'] = True
|
||||||
|
args['cfgfile'] = '/home/bts/.config/optools/mumbleadmin.ini'
|
||||||
|
#if not args['operation']:
|
||||||
|
#raise RuntimeError('You must specify an operation to perform. Try running with -h/--help.')
|
||||||
|
# exit('You must specify an operation to perform. Try running with -h/--help.')
|
||||||
|
|
||||||
|
mgmt = usrmgmt2.IceMgr(args)
|
||||||
|
|
||||||
|
def dictify(obj):
|
||||||
|
# thanks, https://github.com/alfg/murmur-rest/blob/master/app/utils.py
|
||||||
|
_rv = {'_type': str(type(obj))}
|
||||||
|
if type(obj) in (bool, int, float, str, bytes):
|
||||||
|
return(obj)
|
||||||
|
if type(obj) in (list, tuple):
|
||||||
|
return([dictify(i) for i in obj])
|
||||||
|
if type(obj) == dict:
|
||||||
|
return(dict((str(k), dictify(v)) for k, v in obj.items()))
|
||||||
|
return(dictify(obj.__dict__))
|
||||||
|
|
||||||
|
|
||||||
|
# Here we actually print users
|
||||||
|
#print(inspect.getmembers(Murmur.UserInfo))
|
||||||
|
#for s in mgmt.conn['read'].getAllServers(): # iterate through all servers
|
||||||
|
#userattrs = [Murmur.UserInfo.Username, Murmur.UserInfo.UserEmail,
|
||||||
|
# Murmur.UserInfo.UserHash, Murmur.UserInfo.UserLastActive,
|
||||||
|
# Murmur.UserInfo.UserComment]
|
||||||
|
#print(type(s))
|
||||||
|
#pprint.pprint(s.getRegisteredUsers('')) # either print a UID:username map...
|
||||||
|
# for uid, uname in s.getRegisteredUsers('').items(): # or let's try to get full info on them
|
||||||
|
#print('user: {0}\nusername: {1}\n'.format(uid, uname))
|
||||||
|
# _u = dictify(s.getRegistration(uid))
|
||||||
|
# if uid == 3:
|
||||||
|
# print(_u)
|
||||||
|
|
||||||
|
print(mgmt.conn['read'])
|
||||||
|
_server = mgmt.conn['read'].getServer(1)
|
||||||
|
|
||||||
|
print(_server.getACL(0))
|
||||||
|
|
||||||
|
#acl = _server.getACL(0)
|
||||||
|
#print(acl[0])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#pprint.pprint(dictify(acl), indent = 4)
|
||||||
|
|
||||||
|
mgmt.close()
|
7
mumble/grpctest.py
Executable file
7
mumble/grpctest.py
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
from grpc.tools import protoc
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
channel = grpc.insecure_channel('sysadministrivia.com:50051')
|
888
mumble/murmur.ice
Normal file
888
mumble/murmur.ice
Normal file
@ -0,0 +1,888 @@
|
|||||||
|
// https://raw.githubusercontent.com/mumble-voip/mumble/master/src/murmur/Murmur.ice
|
||||||
|
// http://mumble.sourceforge.net/slice/1.3.0/Murmur.html
|
||||||
|
|
||||||
|
// Copyright 2005-2017 The Mumble Developers. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file at the root of the
|
||||||
|
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Information and control of the murmur server. Each server has
|
||||||
|
* one {@link Meta} interface that controls global information, and
|
||||||
|
* each virtual server has a {@link Server} interface.
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <Ice/SliceChecksumDict.ice>
|
||||||
|
|
||||||
|
module Murmur
|
||||||
|
{
|
||||||
|
|
||||||
|
/** A network address in IPv6 format.
|
||||||
|
**/
|
||||||
|
["python:seq:tuple"] sequence<byte> NetAddress;
|
||||||
|
|
||||||
|
/** A connected user.
|
||||||
|
**/
|
||||||
|
struct User {
|
||||||
|
/** Session ID. This identifies the connection to the server. */
|
||||||
|
int session;
|
||||||
|
/** User ID. -1 if the user is anonymous. */
|
||||||
|
int userid;
|
||||||
|
/** Is user muted by the server? */
|
||||||
|
bool mute;
|
||||||
|
/** Is user deafened by the server? If true, this implies mute. */
|
||||||
|
bool deaf;
|
||||||
|
/** Is the user suppressed by the server? This means the user is not muted, but does not have speech privileges in the current channel. */
|
||||||
|
bool suppress;
|
||||||
|
/** Is the user a priority speaker? */
|
||||||
|
bool prioritySpeaker;
|
||||||
|
/** Is the user self-muted? */
|
||||||
|
bool selfMute;
|
||||||
|
/** Is the user self-deafened? If true, this implies mute. */
|
||||||
|
bool selfDeaf;
|
||||||
|
/** Is the User recording? (This flag is read-only and cannot be changed using setState().) **/
|
||||||
|
bool recording;
|
||||||
|
/** Channel ID the user is in. Matches {@link Channel.id}. */
|
||||||
|
int channel;
|
||||||
|
/** The name of the user. */
|
||||||
|
string name;
|
||||||
|
/** Seconds user has been online. */
|
||||||
|
int onlinesecs;
|
||||||
|
/** Average transmission rate in bytes per second over the last few seconds. */
|
||||||
|
int bytespersec;
|
||||||
|
/** Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203. */
|
||||||
|
int version;
|
||||||
|
/** Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else. */
|
||||||
|
string release;
|
||||||
|
/** Client OS. */
|
||||||
|
string os;
|
||||||
|
/** Client OS Version. */
|
||||||
|
string osversion;
|
||||||
|
/** Plugin Identity. This will be the user's unique ID inside the current game. */
|
||||||
|
string identity;
|
||||||
|
/**
|
||||||
|
Base64-encoded Plugin context. This is a binary blob identifying the game and team the user is on.
|
||||||
|
|
||||||
|
The used Base64 alphabet is the one specified in RFC 2045.
|
||||||
|
|
||||||
|
Before Mumble 1.3.0, this string was not Base64-encoded. This could cause problems for some Ice
|
||||||
|
implementations, such as the .NET implementation.
|
||||||
|
|
||||||
|
If you need the exact string that is used by Mumble, you can get it by Base64-decoding this string.
|
||||||
|
|
||||||
|
If you simply need to detect whether two users are in the same game world, string comparisons will
|
||||||
|
continue to work as before.
|
||||||
|
*/
|
||||||
|
string context;
|
||||||
|
/** User comment. Shown as tooltip for this user. */
|
||||||
|
string comment;
|
||||||
|
/** Client address. */
|
||||||
|
NetAddress address;
|
||||||
|
/** TCP only. True until UDP connectivity is established. */
|
||||||
|
bool tcponly;
|
||||||
|
/** Idle time. This is how many seconds it is since the user last spoke. Other activity is not counted. */
|
||||||
|
int idlesecs;
|
||||||
|
/** UDP Ping Average. This is the average ping for the user via UDP over the duration of the connection. */
|
||||||
|
float udpPing;
|
||||||
|
/** TCP Ping Average. This is the average ping for the user via TCP over the duration of the connection. */
|
||||||
|
float tcpPing;
|
||||||
|
};
|
||||||
|
|
||||||
|
sequence<int> IntList;
|
||||||
|
|
||||||
|
/** A text message between users.
|
||||||
|
**/
|
||||||
|
struct TextMessage {
|
||||||
|
/** Sessions (connected users) who were sent this message. */
|
||||||
|
IntList sessions;
|
||||||
|
/** Channels who were sent this message. */
|
||||||
|
IntList channels;
|
||||||
|
/** Trees of channels who were sent this message. */
|
||||||
|
IntList trees;
|
||||||
|
/** The contents of the message. */
|
||||||
|
string text;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A channel.
|
||||||
|
**/
|
||||||
|
struct Channel {
|
||||||
|
/** Channel ID. This is unique per channel, and the root channel is always id 0. */
|
||||||
|
int id;
|
||||||
|
/** Name of the channel. There can not be two channels with the same parent that has the same name. */
|
||||||
|
string name;
|
||||||
|
/** ID of parent channel, or -1 if this is the root channel. */
|
||||||
|
int parent;
|
||||||
|
/** List of id of linked channels. */
|
||||||
|
IntList links;
|
||||||
|
/** Description of channel. Shown as tooltip for this channel. */
|
||||||
|
string description;
|
||||||
|
/** Channel is temporary, and will be removed when the last user leaves it. */
|
||||||
|
bool temporary;
|
||||||
|
/** Position of the channel which is used in Client for sorting. */
|
||||||
|
int position;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A group. Groups are defined per channel, and can inherit members from parent channels.
|
||||||
|
**/
|
||||||
|
struct Group {
|
||||||
|
/** Group name */
|
||||||
|
string name;
|
||||||
|
/** Is this group inherited from a parent channel? Read-only. */
|
||||||
|
bool inherited;
|
||||||
|
/** Does this group inherit members from parent channels? */
|
||||||
|
bool inherit;
|
||||||
|
/** Can subchannels inherit members from this group? */
|
||||||
|
bool inheritable;
|
||||||
|
/** List of users to add to the group. */
|
||||||
|
IntList add;
|
||||||
|
/** List of inherited users to remove from the group. */
|
||||||
|
IntList remove;
|
||||||
|
/** Current members of the group, including inherited members. Read-only. */
|
||||||
|
IntList members;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Write access to channel control. Implies all other permissions (except Speak). */
|
||||||
|
const int PermissionWrite = 0x01;
|
||||||
|
/** Traverse channel. Without this, a client cannot reach subchannels, no matter which privileges he has there. */
|
||||||
|
const int PermissionTraverse = 0x02;
|
||||||
|
/** Enter channel. */
|
||||||
|
const int PermissionEnter = 0x04;
|
||||||
|
/** Speak in channel. */
|
||||||
|
const int PermissionSpeak = 0x08;
|
||||||
|
/** Whisper to channel. This is different from Speak, so you can set up different permissions. */
|
||||||
|
const int PermissionWhisper = 0x100;
|
||||||
|
/** Mute and deafen other users in this channel. */
|
||||||
|
const int PermissionMuteDeafen = 0x10;
|
||||||
|
/** Move users from channel. You need this permission in both the source and destination channel to move another user. */
|
||||||
|
const int PermissionMove = 0x20;
|
||||||
|
/** Make new channel as a subchannel of this channel. */
|
||||||
|
const int PermissionMakeChannel = 0x40;
|
||||||
|
/** Make new temporary channel as a subchannel of this channel. */
|
||||||
|
const int PermissionMakeTempChannel = 0x400;
|
||||||
|
/** Link this channel. You need this permission in both the source and destination channel to link channels, or in either channel to unlink them. */
|
||||||
|
const int PermissionLinkChannel = 0x80;
|
||||||
|
/** Send text message to channel. */
|
||||||
|
const int PermissionTextMessage = 0x200;
|
||||||
|
/** Kick user from server. Only valid on root channel. */
|
||||||
|
const int PermissionKick = 0x10000;
|
||||||
|
/** Ban user from server. Only valid on root channel. */
|
||||||
|
const int PermissionBan = 0x20000;
|
||||||
|
/** Register and unregister users. Only valid on root channel. */
|
||||||
|
const int PermissionRegister = 0x40000;
|
||||||
|
/** Register and unregister users. Only valid on root channel. */
|
||||||
|
const int PermissionRegisterSelf = 0x80000;
|
||||||
|
|
||||||
|
|
||||||
|
/** Access Control List for a channel. ACLs are defined per channel, and can be inherited from parent channels.
|
||||||
|
**/
|
||||||
|
struct ACL {
|
||||||
|
/** Does the ACL apply to this channel? */
|
||||||
|
bool applyHere;
|
||||||
|
/** Does the ACL apply to subchannels? */
|
||||||
|
bool applySubs;
|
||||||
|
/** Is this ACL inherited from a parent channel? Read-only. */
|
||||||
|
bool inherited;
|
||||||
|
/** ID of user this ACL applies to. -1 if using a group name. */
|
||||||
|
int userid;
|
||||||
|
/** Group this ACL applies to. Blank if using userid. */
|
||||||
|
string group;
|
||||||
|
/** Binary mask of privileges to allow. */
|
||||||
|
int allow;
|
||||||
|
/** Binary mask of privileges to deny. */
|
||||||
|
int deny;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A single ip mask for a ban.
|
||||||
|
**/
|
||||||
|
struct Ban {
|
||||||
|
/** Address to ban. */
|
||||||
|
NetAddress address;
|
||||||
|
/** Number of bits in ban to apply. */
|
||||||
|
int bits;
|
||||||
|
/** Username associated with ban. */
|
||||||
|
string name;
|
||||||
|
/** Hash of banned user. */
|
||||||
|
string hash;
|
||||||
|
/** Reason for ban. */
|
||||||
|
string reason;
|
||||||
|
/** Date ban was applied in unix time format. */
|
||||||
|
int start;
|
||||||
|
/** Duration of ban. */
|
||||||
|
int duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A entry in the log.
|
||||||
|
**/
|
||||||
|
struct LogEntry {
|
||||||
|
/** Timestamp in UNIX time_t */
|
||||||
|
int timestamp;
|
||||||
|
/** The log message. */
|
||||||
|
string txt;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Tree;
|
||||||
|
sequence<Tree> TreeList;
|
||||||
|
|
||||||
|
enum ChannelInfo { ChannelDescription, ChannelPosition };
|
||||||
|
enum UserInfo { UserName, UserEmail, UserComment, UserHash, UserPassword, UserLastActive };
|
||||||
|
|
||||||
|
dictionary<int, User> UserMap;
|
||||||
|
dictionary<int, Channel> ChannelMap;
|
||||||
|
sequence<Channel> ChannelList;
|
||||||
|
sequence<User> UserList;
|
||||||
|
sequence<Group> GroupList;
|
||||||
|
sequence<ACL> ACLList;
|
||||||
|
sequence<LogEntry> LogList;
|
||||||
|
sequence<Ban> BanList;
|
||||||
|
sequence<int> IdList;
|
||||||
|
sequence<string> NameList;
|
||||||
|
dictionary<int, string> NameMap;
|
||||||
|
dictionary<string, int> IdMap;
|
||||||
|
sequence<byte> Texture;
|
||||||
|
dictionary<string, string> ConfigMap;
|
||||||
|
sequence<string> GroupNameList;
|
||||||
|
sequence<byte> CertificateDer;
|
||||||
|
sequence<CertificateDer> CertificateList;
|
||||||
|
|
||||||
|
/** User information map.
|
||||||
|
* Older versions of ice-php can't handle enums as keys. If you are using one of these, replace 'UserInfo' with 'byte'.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dictionary<UserInfo, string> UserInfoMap;
|
||||||
|
|
||||||
|
/** User and subchannel state. Read-only.
|
||||||
|
**/
|
||||||
|
class Tree {
|
||||||
|
/** Channel definition of current channel. */
|
||||||
|
Channel c;
|
||||||
|
/** List of subchannels. */
|
||||||
|
TreeList children;
|
||||||
|
/** Users in this channel. */
|
||||||
|
UserList users;
|
||||||
|
};
|
||||||
|
|
||||||
|
exception MurmurException {};
|
||||||
|
/** This is thrown when you specify an invalid session. This may happen if the user has disconnected since your last call to {@link Server.getUsers}. See {@link User.session} */
|
||||||
|
exception InvalidSessionException extends MurmurException {};
|
||||||
|
/** This is thrown when you specify an invalid channel id. This may happen if the channel was removed by another provess. It can also be thrown if you try to add an invalid channel. */
|
||||||
|
exception InvalidChannelException extends MurmurException {};
|
||||||
|
/** This is thrown when you try to do an operation on a server that does not exist. This may happen if someone has removed the server. */
|
||||||
|
exception InvalidServerException extends MurmurException {};
|
||||||
|
/** This happens if you try to fetch user or channel state on a stopped server, if you try to stop an already stopped server or start an already started server. */
|
||||||
|
exception ServerBootedException extends MurmurException {};
|
||||||
|
/** This is thrown if {@link Server.start} fails, and should generally be the cause for some concern. */
|
||||||
|
exception ServerFailureException extends MurmurException {};
|
||||||
|
/** This is thrown when you specify an invalid userid. */
|
||||||
|
exception InvalidUserException extends MurmurException {};
|
||||||
|
/** This is thrown when you try to set an invalid texture. */
|
||||||
|
exception InvalidTextureException extends MurmurException {};
|
||||||
|
/** This is thrown when you supply an invalid callback. */
|
||||||
|
exception InvalidCallbackException extends MurmurException {};
|
||||||
|
/** This is thrown when you supply the wrong secret in the calling context. */
|
||||||
|
exception InvalidSecretException extends MurmurException {};
|
||||||
|
/** This is thrown when the channel operation would excede the channel nesting limit */
|
||||||
|
exception NestingLimitException extends MurmurException {};
|
||||||
|
/** This is thrown when you ask the server to disclose something that should be secret. */
|
||||||
|
exception WriteOnlyException extends MurmurException {};
|
||||||
|
/** This is thrown when invalid input data was specified. */
|
||||||
|
exception InvalidInputDataException extends MurmurException {};
|
||||||
|
|
||||||
|
/** Callback interface for servers. You can supply an implementation of this to receive notification
|
||||||
|
* messages from the server.
|
||||||
|
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||||
|
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||||
|
* complete before continuing processing.
|
||||||
|
* Note that callbacks are removed when a server is stopped, so you should have a callback for
|
||||||
|
* {@link MetaCallback.started} which calls {@link Server.addCallback}.
|
||||||
|
* @see MetaCallback
|
||||||
|
* @see Server.addCallback
|
||||||
|
*/
|
||||||
|
interface ServerCallback {
|
||||||
|
/** Called when a user connects to the server.
|
||||||
|
* @param state State of connected user.
|
||||||
|
*/
|
||||||
|
idempotent void userConnected(User state);
|
||||||
|
/** Called when a user disconnects from the server. The user has already been removed, so you can no longer use methods like {@link Server.getState}
|
||||||
|
* to retrieve the user's state.
|
||||||
|
* @param state State of disconnected user.
|
||||||
|
*/
|
||||||
|
idempotent void userDisconnected(User state);
|
||||||
|
/** Called when a user state changes. This is called if the user moves, is renamed, is muted, deafened etc.
|
||||||
|
* @param state New state of user.
|
||||||
|
*/
|
||||||
|
idempotent void userStateChanged(User state);
|
||||||
|
/** Called when user writes a text message
|
||||||
|
* @param state the User sending the message
|
||||||
|
* @param message the TextMessage the user has sent
|
||||||
|
*/
|
||||||
|
idempotent void userTextMessage(User state, TextMessage message);
|
||||||
|
/** Called when a new channel is created.
|
||||||
|
* @param state State of new channel.
|
||||||
|
*/
|
||||||
|
idempotent void channelCreated(Channel state);
|
||||||
|
/** Called when a channel is removed. The channel has already been removed, you can no longer use methods like {@link Server.getChannelState}
|
||||||
|
* @param state State of removed channel.
|
||||||
|
*/
|
||||||
|
idempotent void channelRemoved(Channel state);
|
||||||
|
/** Called when a new channel state changes. This is called if the channel is moved, renamed or if new links are added.
|
||||||
|
* @param state New state of channel.
|
||||||
|
*/
|
||||||
|
idempotent void channelStateChanged(Channel state);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Context for actions in the Server menu. */
|
||||||
|
const int ContextServer = 0x01;
|
||||||
|
/** Context for actions in the Channel menu. */
|
||||||
|
const int ContextChannel = 0x02;
|
||||||
|
/** Context for actions in the User menu. */
|
||||||
|
const int ContextUser = 0x04;
|
||||||
|
|
||||||
|
/** Callback interface for context actions. You need to supply one of these for {@link Server.addContext}.
|
||||||
|
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||||
|
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||||
|
* complete before continuing processing.
|
||||||
|
*/
|
||||||
|
interface ServerContextCallback {
|
||||||
|
/** Called when a context action is performed.
|
||||||
|
* @param action Action to be performed.
|
||||||
|
* @param usr User which initiated the action.
|
||||||
|
* @param session If nonzero, session of target user.
|
||||||
|
* @param channelid If not -1, id of target channel.
|
||||||
|
*/
|
||||||
|
idempotent void contextAction(string action, User usr, int session, int channelid);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Callback interface for server authentication. You need to supply one of these for {@link Server.setAuthenticator}.
|
||||||
|
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||||
|
* Please note that unlike {@link ServerCallback} and {@link ServerContextCallback}, these methods are called
|
||||||
|
* synchronously. If the response lags, the entire murmur server will lag.
|
||||||
|
* Also note that, as the method calls are synchronous, making a call to {@link Server} or {@link Meta} will
|
||||||
|
* deadlock the server.
|
||||||
|
*/
|
||||||
|
interface ServerAuthenticator {
|
||||||
|
/** Called to authenticate a user. If you do not know the username in question, always return -2 from this
|
||||||
|
* method to fall through to normal database authentication.
|
||||||
|
* Note that if authentication succeeds, murmur will create a record of the user in it's database, reserving
|
||||||
|
* the username and id so it cannot be used for normal database authentication.
|
||||||
|
* The data in the certificate (name, email addresses etc), as well as the list of signing certificates,
|
||||||
|
* should only be trusted if certstrong is true.
|
||||||
|
*
|
||||||
|
* Internally, Murmur treats usernames as case-insensitive. It is recommended
|
||||||
|
* that authenticators do the same. Murmur checks if a username is in use when
|
||||||
|
* a user connects. If the connecting user is registered, the other username is
|
||||||
|
* kicked. If the connecting user is not registered, the connecting user is not
|
||||||
|
* allowed to join the server.
|
||||||
|
*
|
||||||
|
* @param name Username to authenticate.
|
||||||
|
* @param pw Password to authenticate with.
|
||||||
|
* @param certificates List of der encoded certificates the user connected with.
|
||||||
|
* @param certhash Hash of user certificate, as used by murmur internally when matching.
|
||||||
|
* @param certstrong True if certificate was valid and signed by a trusted CA.
|
||||||
|
* @param newname Set this to change the username from the supplied one.
|
||||||
|
* @param groups List of groups on the root channel that the user will be added to for the duration of the connection.
|
||||||
|
* @return UserID of authenticated user, -1 for authentication failures, -2 for unknown user (fallthrough),
|
||||||
|
* -3 for authentication failures where the data could (temporarily) not be verified.
|
||||||
|
*/
|
||||||
|
idempotent int authenticate(string name, string pw, CertificateList certificates, string certhash, bool certstrong, out string newname, out GroupNameList groups);
|
||||||
|
|
||||||
|
/** Fetch information about a user. This is used to retrieve information like email address, keyhash etc. If you
|
||||||
|
* want murmur to take care of this information itself, simply return false to fall through.
|
||||||
|
* @param id User id.
|
||||||
|
* @param info Information about user. This needs to include at least "name".
|
||||||
|
* @return true if information is present, false to fall through.
|
||||||
|
*/
|
||||||
|
idempotent bool getInfo(int id, out UserInfoMap info);
|
||||||
|
|
||||||
|
/** Map a name to a user id.
|
||||||
|
* @param name Username to map.
|
||||||
|
* @return User id or -2 for unknown name.
|
||||||
|
*/
|
||||||
|
idempotent int nameToId(string name);
|
||||||
|
|
||||||
|
/** Map a user id to a username.
|
||||||
|
* @param id User id to map.
|
||||||
|
* @return Name of user or empty string for unknown id.
|
||||||
|
*/
|
||||||
|
idempotent string idToName(int id);
|
||||||
|
|
||||||
|
/** Map a user to a custom Texture.
|
||||||
|
* @param id User id to map.
|
||||||
|
* @return User texture or an empty texture for unknwon users or users without textures.
|
||||||
|
*/
|
||||||
|
idempotent Texture idToTexture(int id);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Callback interface for server authentication and registration. This allows you to support both authentication
|
||||||
|
* and account updating.
|
||||||
|
* You do not need to implement this if all you want is authentication, you only need this if other scripts
|
||||||
|
* connected to the same server calls e.g. {@link Server.setTexture}.
|
||||||
|
* Almost all of these methods support fall through, meaning murmur should continue the operation against its
|
||||||
|
* own database.
|
||||||
|
*/
|
||||||
|
interface ServerUpdatingAuthenticator extends ServerAuthenticator {
|
||||||
|
/** Register a new user.
|
||||||
|
* @param info Information about user to register.
|
||||||
|
* @return User id of new user, -1 for registration failure, or -2 to fall through.
|
||||||
|
*/
|
||||||
|
int registerUser(UserInfoMap info);
|
||||||
|
|
||||||
|
/** Unregister a user.
|
||||||
|
* @param id Userid to unregister.
|
||||||
|
* @return 1 for successfull unregistration, 0 for unsuccessfull unregistration, -1 to fall through.
|
||||||
|
*/
|
||||||
|
int unregisterUser(int id);
|
||||||
|
|
||||||
|
/** Get a list of registered users matching filter.
|
||||||
|
* @param filter Substring usernames must contain. If empty, return all registered users.
|
||||||
|
* @return List of matching registered users.
|
||||||
|
*/
|
||||||
|
idempotent NameMap getRegisteredUsers(string filter);
|
||||||
|
|
||||||
|
/** Set additional information for user registration.
|
||||||
|
* @param id Userid of registered user.
|
||||||
|
* @param info Information to set about user. This should be merged with existing information.
|
||||||
|
* @return 1 for successfull update, 0 for unsuccessfull update, -1 to fall through.
|
||||||
|
*/
|
||||||
|
idempotent int setInfo(int id, UserInfoMap info);
|
||||||
|
|
||||||
|
/** Set texture (now called avatar) of user registration.
|
||||||
|
* @param id registrationId of registered user.
|
||||||
|
* @param tex New texture.
|
||||||
|
* @return 1 for successfull update, 0 for unsuccessfull update, -1 to fall through.
|
||||||
|
*/
|
||||||
|
idempotent int setTexture(int id, Texture tex);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Per-server interface. This includes all methods for configuring and altering
|
||||||
|
* the state of a single virtual server. You can retrieve a pointer to this interface
|
||||||
|
* from one of the methods in {@link Meta}.
|
||||||
|
**/
|
||||||
|
["amd"] interface Server {
|
||||||
|
/** Shows if the server currently running (accepting users).
|
||||||
|
*
|
||||||
|
* @return Run-state of server.
|
||||||
|
*/
|
||||||
|
idempotent bool isRunning() throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Start server. */
|
||||||
|
void start() throws ServerBootedException, ServerFailureException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Stop server.
|
||||||
|
* Note: Server will be restarted on Murmur restart unless explicitly disabled
|
||||||
|
* with setConf("boot", false)
|
||||||
|
*/
|
||||||
|
void stop() throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Delete server and all it's configuration. */
|
||||||
|
void delete() throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch the server id.
|
||||||
|
*
|
||||||
|
* @return Unique server id.
|
||||||
|
*/
|
||||||
|
idempotent int id() throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Add a callback. The callback will receive notifications about changes to users and channels.
|
||||||
|
*
|
||||||
|
* @param cb Callback interface which will receive notifications.
|
||||||
|
* @see removeCallback
|
||||||
|
*/
|
||||||
|
void addCallback(ServerCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Remove a callback.
|
||||||
|
*
|
||||||
|
* @param cb Callback interface to be removed.
|
||||||
|
* @see addCallback
|
||||||
|
*/
|
||||||
|
void removeCallback(ServerCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Set external authenticator. If set, all authentications from clients are forwarded to this
|
||||||
|
* proxy.
|
||||||
|
*
|
||||||
|
* @param auth Authenticator object to perform subsequent authentications.
|
||||||
|
*/
|
||||||
|
void setAuthenticator(ServerAuthenticator *auth) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Retrieve configuration item.
|
||||||
|
* @param key Configuration key.
|
||||||
|
* @return Configuration value. If this is empty, see {@link Meta.getDefaultConf}
|
||||||
|
*/
|
||||||
|
idempotent string getConf(string key) throws InvalidSecretException, WriteOnlyException;
|
||||||
|
|
||||||
|
/** Retrieve all configuration items.
|
||||||
|
* @return All configured values. If a value isn't set here, the value from {@link Meta.getDefaultConf} is used.
|
||||||
|
*/
|
||||||
|
idempotent ConfigMap getAllConf() throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Set a configuration item.
|
||||||
|
* @param key Configuration key.
|
||||||
|
* @param value Configuration value.
|
||||||
|
*/
|
||||||
|
idempotent void setConf(string key, string value) throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Set superuser password. This is just a convenience for using {@link updateRegistration} on user id 0.
|
||||||
|
* @param pw Password.
|
||||||
|
*/
|
||||||
|
idempotent void setSuperuserPassword(string pw) throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch log entries.
|
||||||
|
* @param first Lowest numbered entry to fetch. 0 is the most recent item.
|
||||||
|
* @param last Last entry to fetch.
|
||||||
|
* @return List of log entries.
|
||||||
|
*/
|
||||||
|
idempotent LogList getLog(int first, int last) throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch length of log
|
||||||
|
* @return Number of entries in log
|
||||||
|
*/
|
||||||
|
idempotent int getLogLen() throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch all users. This returns all currently connected users on the server.
|
||||||
|
* @return List of connected users.
|
||||||
|
* @see getState
|
||||||
|
*/
|
||||||
|
idempotent UserMap getUsers() throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch all channels. This returns all defined channels on the server. The root channel is always channel 0.
|
||||||
|
* @return List of defined channels.
|
||||||
|
* @see getChannelState
|
||||||
|
*/
|
||||||
|
idempotent ChannelMap getChannels() throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch certificate of user. This returns the complete certificate chain of a user.
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @return Certificate list of user.
|
||||||
|
*/
|
||||||
|
idempotent CertificateList getCertificateList(int session) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch all channels and connected users as a tree. This retrieves an easy-to-use representation of the server
|
||||||
|
* as a tree. This is primarily used for viewing the state of the server on a webpage.
|
||||||
|
* @return Recursive tree of all channels and connected users.
|
||||||
|
*/
|
||||||
|
idempotent Tree getTree() throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch all current IP bans on the server.
|
||||||
|
* @return List of bans.
|
||||||
|
*/
|
||||||
|
idempotent BanList getBans() throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Set all current IP bans on the server. This will replace any bans already present, so if you want to add a ban, be sure to call {@link getBans} and then
|
||||||
|
* append to the returned list before calling this method.
|
||||||
|
* @param bans List of bans.
|
||||||
|
*/
|
||||||
|
idempotent void setBans(BanList bans) throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Kick a user. The user is not banned, and is free to rejoin the server.
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @param reason Text message to show when user is kicked.
|
||||||
|
*/
|
||||||
|
void kickUser(int session, string reason) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Get state of a single connected user.
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @return State of connected user.
|
||||||
|
* @see setState
|
||||||
|
* @see getUsers
|
||||||
|
*/
|
||||||
|
idempotent User getState(int session) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Set user state. You can use this to move, mute and deafen users.
|
||||||
|
* @param state User state to set.
|
||||||
|
* @see getState
|
||||||
|
*/
|
||||||
|
idempotent void setState(User state) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Send text message to a single user.
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @param text Message to send.
|
||||||
|
* @see sendMessageChannel
|
||||||
|
*/
|
||||||
|
void sendMessage(int session, string text) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Check if user is permitted to perform action.
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||||
|
* @param perm Permission bits to check.
|
||||||
|
* @return true if any of the permissions in perm were set for the user.
|
||||||
|
*/
|
||||||
|
bool hasPermission(int session, int channelid, int perm) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Return users effective permissions
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||||
|
* @return bitfield of allowed actions
|
||||||
|
*/
|
||||||
|
idempotent int effectivePermissions(int session, int channelid) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Add a context callback. This is done per user, and will add a context menu action for the user.
|
||||||
|
*
|
||||||
|
* @param session Session of user which should receive context entry.
|
||||||
|
* @param action Action string, a unique name to associate with the action.
|
||||||
|
* @param text Name of action shown to user.
|
||||||
|
* @param cb Callback interface which will receive notifications.
|
||||||
|
* @param ctx Context this should be used in. Needs to be one or a combination of {@link ContextServer}, {@link ContextChannel} and {@link ContextUser}.
|
||||||
|
* @see removeContextCallback
|
||||||
|
*/
|
||||||
|
void addContextCallback(int session, string action, string text, ServerContextCallback *cb, int ctx) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Remove a callback.
|
||||||
|
*
|
||||||
|
* @param cb Callback interface to be removed. This callback will be removed from all from all users.
|
||||||
|
* @see addContextCallback
|
||||||
|
*/
|
||||||
|
void removeContextCallback(ServerContextCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Get state of single channel.
|
||||||
|
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||||
|
* @return State of channel.
|
||||||
|
* @see setChannelState
|
||||||
|
* @see getChannels
|
||||||
|
*/
|
||||||
|
idempotent Channel getChannelState(int channelid) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Set state of a single channel. You can use this to move or relink channels.
|
||||||
|
* @param state Channel state to set.
|
||||||
|
* @see getChannelState
|
||||||
|
*/
|
||||||
|
idempotent void setChannelState(Channel state) throws ServerBootedException, InvalidChannelException, InvalidSecretException, NestingLimitException;
|
||||||
|
|
||||||
|
/** Remove a channel and all its subchannels.
|
||||||
|
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||||
|
*/
|
||||||
|
void removeChannel(int channelid) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Add a new channel.
|
||||||
|
* @param name Name of new channel.
|
||||||
|
* @param parent Channel ID of parent channel. See {@link Channel.id}.
|
||||||
|
* @return ID of newly created channel.
|
||||||
|
*/
|
||||||
|
int addChannel(string name, int parent) throws ServerBootedException, InvalidChannelException, InvalidSecretException, NestingLimitException;
|
||||||
|
|
||||||
|
/** Send text message to channel or a tree of channels.
|
||||||
|
* @param channelid Channel ID of channel to send to. See {@link Channel.id}.
|
||||||
|
* @param tree If true, the message will be sent to the channel and all its subchannels.
|
||||||
|
* @param text Message to send.
|
||||||
|
* @see sendMessage
|
||||||
|
*/
|
||||||
|
void sendMessageChannel(int channelid, bool tree, string text) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Retrieve ACLs and Groups on a channel.
|
||||||
|
* @param channelid Channel ID of channel to fetch from. See {@link Channel.id}.
|
||||||
|
* @param acls List of ACLs on the channel. This will include inherited ACLs.
|
||||||
|
* @param groups List of groups on the channel. This will include inherited groups.
|
||||||
|
* @param inherit Does this channel inherit ACLs from the parent channel?
|
||||||
|
*/
|
||||||
|
idempotent void getACL(int channelid, out ACLList acls, out GroupList groups, out bool inherit) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Set ACLs and Groups on a channel. Note that this will replace all existing ACLs and groups on the channel.
|
||||||
|
* @param channelid Channel ID of channel to fetch from. See {@link Channel.id}.
|
||||||
|
* @param acls List of ACLs on the channel.
|
||||||
|
* @param groups List of groups on the channel.
|
||||||
|
* @param inherit Should this channel inherit ACLs from the parent channel?
|
||||||
|
*/
|
||||||
|
idempotent void setACL(int channelid, ACLList acls, GroupList groups, bool inherit) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Temporarily add a user to a group on a channel. This state is not saved, and is intended for temporary memberships.
|
||||||
|
* @param channelid Channel ID of channel to add to. See {@link Channel.id}.
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @param group Group name to add to.
|
||||||
|
*/
|
||||||
|
idempotent void addUserToGroup(int channelid, int session, string group) throws ServerBootedException, InvalidChannelException, InvalidSessionException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Remove a user from a temporary group membership on a channel. This state is not saved, and is intended for temporary memberships.
|
||||||
|
* @param channelid Channel ID of channel to add to. See {@link Channel.id}.
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @param group Group name to remove from.
|
||||||
|
*/
|
||||||
|
idempotent void removeUserFromGroup(int channelid, int session, string group) throws ServerBootedException, InvalidChannelException, InvalidSessionException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Redirect whisper targets for user. If set, whenever a user tries to whisper to group "source", the whisper will be redirected to group "target".
|
||||||
|
* To remove a redirect pass an empty target string. This is intended for context groups.
|
||||||
|
* @param session Connection ID of user. See {@link User.session}.
|
||||||
|
* @param source Group name to redirect from.
|
||||||
|
* @param target Group name to redirect to.
|
||||||
|
*/
|
||||||
|
idempotent void redirectWhisperGroup(int session, string source, string target) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Map a list of {@link User.userid} to a matching name.
|
||||||
|
* @param List of ids.
|
||||||
|
* @return Matching list of names, with an empty string representing invalid or unknown ids.
|
||||||
|
*/
|
||||||
|
idempotent NameMap getUserNames(IdList ids) throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Map a list of user names to a matching id.
|
||||||
|
* @param List of names.
|
||||||
|
* @reuturn List of matching ids, with -1 representing invalid or unknown user names.
|
||||||
|
*/
|
||||||
|
idempotent IdMap getUserIds(NameList names) throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Register a new user.
|
||||||
|
* @param info Information about new user. Must include at least "name".
|
||||||
|
* @return The ID of the user. See {@link RegisteredUser.userid}.
|
||||||
|
*/
|
||||||
|
int registerUser(UserInfoMap info) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Remove a user registration.
|
||||||
|
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||||
|
*/
|
||||||
|
void unregisterUser(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Update the registration for a user. You can use this to set the email or password of a user,
|
||||||
|
* and can also use it to change the user's name.
|
||||||
|
* @param registration Updated registration record.
|
||||||
|
*/
|
||||||
|
idempotent void updateRegistration(int userid, UserInfoMap info) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch registration for a single user.
|
||||||
|
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||||
|
* @return Registration record.
|
||||||
|
*/
|
||||||
|
idempotent UserInfoMap getRegistration(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch a group of registered users.
|
||||||
|
* @param filter Substring of user name. If blank, will retrieve all registered users.
|
||||||
|
* @return List of registration records.
|
||||||
|
*/
|
||||||
|
idempotent NameMap getRegisteredUsers(string filter) throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Verify the password of a user. You can use this to verify a user's credentials.
|
||||||
|
* @param name User name. See {@link RegisteredUser.name}.
|
||||||
|
* @param pw User password.
|
||||||
|
* @return User ID of registered user (See {@link RegisteredUser.userid}), -1 for failed authentication or -2 for unknown usernames.
|
||||||
|
*/
|
||||||
|
idempotent int verifyPassword(string name, string pw) throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch user texture. Textures are stored as zlib compress()ed 600x60 32-bit BGRA data.
|
||||||
|
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||||
|
* @return Custom texture associated with user or an empty texture.
|
||||||
|
*/
|
||||||
|
idempotent Texture getTexture(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Set a user texture (now called avatar).
|
||||||
|
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||||
|
* @param tex Texture (as a Byte-Array) to set for the user, or an empty texture to remove the existing texture.
|
||||||
|
*/
|
||||||
|
idempotent void setTexture(int userid, Texture tex) throws ServerBootedException, InvalidUserException, InvalidTextureException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Get virtual server uptime.
|
||||||
|
* @return Uptime of the virtual server in seconds
|
||||||
|
*/
|
||||||
|
idempotent int getUptime() throws ServerBootedException, InvalidSecretException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the server's certificate information.
|
||||||
|
*
|
||||||
|
* Reconfigure the running server's TLS socket with the given
|
||||||
|
* certificate and private key.
|
||||||
|
*
|
||||||
|
* The certificate and and private key must be PEM formatted.
|
||||||
|
*
|
||||||
|
* New clients will see the new certificate.
|
||||||
|
* Existing clients will continue to see the certificate the server
|
||||||
|
* was using when they connected to it.
|
||||||
|
*
|
||||||
|
* This method throws InvalidInputDataException if any of the
|
||||||
|
* following errors happen:
|
||||||
|
* - Unable to decode the PEM certificate and/or private key.
|
||||||
|
* - Unable to decrypt the private key with the given passphrase.
|
||||||
|
* - The certificate and/or private key do not contain RSA keys.
|
||||||
|
* - The certificate is not usable with the given private key.
|
||||||
|
*/
|
||||||
|
idempotent void updateCertificate(string certificate, string privateKey, string passphrase) throws ServerBootedException, InvalidSecretException, InvalidInputDataException;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Callback interface for Meta. You can supply an implementation of this to receive notifications
|
||||||
|
* when servers are stopped or started.
|
||||||
|
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||||
|
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||||
|
* complete before continuing processing.
|
||||||
|
* @see ServerCallback
|
||||||
|
* @see Meta.addCallback
|
||||||
|
*/
|
||||||
|
interface MetaCallback {
|
||||||
|
/** Called when a server is started. The server is up and running when this event is sent, so all methods that
|
||||||
|
* need a running server will work.
|
||||||
|
* @param srv Interface for started server.
|
||||||
|
*/
|
||||||
|
void started(Server *srv);
|
||||||
|
|
||||||
|
/** Called when a server is stopped. The server is already stopped when this event is sent, so no methods that
|
||||||
|
* need a running server will work.
|
||||||
|
* @param srv Interface for started server.
|
||||||
|
*/
|
||||||
|
void stopped(Server *srv);
|
||||||
|
};
|
||||||
|
|
||||||
|
sequence<Server *> ServerList;
|
||||||
|
|
||||||
|
/** This is the meta interface. It is primarily used for retrieving the {@link Server} interfaces for each individual server.
|
||||||
|
**/
|
||||||
|
["amd"] interface Meta {
|
||||||
|
/** Fetch interface to specific server.
|
||||||
|
* @param id Server ID. See {@link Server.getId}.
|
||||||
|
* @return Interface for specified server, or a null proxy if id is invalid.
|
||||||
|
*/
|
||||||
|
idempotent Server *getServer(int id) throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Create a new server. Call {@link Server.getId} on the returned interface to find it's ID.
|
||||||
|
* @return Interface for new server.
|
||||||
|
*/
|
||||||
|
Server *newServer() throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch list of all currently running servers.
|
||||||
|
* @return List of interfaces for running servers.
|
||||||
|
*/
|
||||||
|
idempotent ServerList getBootedServers() throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch list of all defined servers.
|
||||||
|
* @return List of interfaces for all servers.
|
||||||
|
*/
|
||||||
|
idempotent ServerList getAllServers() throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch default configuraion. This returns the configuration items that were set in the configuration file, or
|
||||||
|
* the built-in default. The individual servers will use these values unless they have been overridden in the
|
||||||
|
* server specific configuration. The only special case is the port, which defaults to the value defined here +
|
||||||
|
* the servers ID - 1 (so that virtual server #1 uses the defined port, server #2 uses port+1 etc).
|
||||||
|
* @return Default configuration of the servers.
|
||||||
|
*/
|
||||||
|
idempotent ConfigMap getDefaultConf() throws InvalidSecretException;
|
||||||
|
|
||||||
|
/** Fetch version of Murmur.
|
||||||
|
* @param major Major version.
|
||||||
|
* @param minor Minor version.
|
||||||
|
* @param patch Patchlevel.
|
||||||
|
* @param text Textual representation of version. Note that this may not match the {@link major}, {@link minor} and {@link patch} levels, as it
|
||||||
|
* may be simply the compile date or the SVN revision. This is usually the text you want to present to users.
|
||||||
|
*/
|
||||||
|
idempotent void getVersion(out int major, out int minor, out int patch, out string text);
|
||||||
|
|
||||||
|
/** Add a callback. The callback will receive notifications when servers are started or stopped.
|
||||||
|
*
|
||||||
|
* @param cb Callback interface which will receive notifications.
|
||||||
|
*/
|
||||||
|
void addCallback(MetaCallback *cb) throws InvalidCallbackException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Remove a callback.
|
||||||
|
*
|
||||||
|
* @param cb Callback interface to be removed.
|
||||||
|
*/
|
||||||
|
void removeCallback(MetaCallback *cb) throws InvalidCallbackException, InvalidSecretException;
|
||||||
|
|
||||||
|
/** Get murmur uptime.
|
||||||
|
* @return Uptime of murmur in seconds
|
||||||
|
*/
|
||||||
|
idempotent int getUptime();
|
||||||
|
|
||||||
|
/** Get slice file.
|
||||||
|
* @return Contents of the slice file server compiled with.
|
||||||
|
*/
|
||||||
|
idempotent string getSlice();
|
||||||
|
|
||||||
|
/** Returns a checksum dict for the slice file.
|
||||||
|
* @return Checksum dict
|
||||||
|
*/
|
||||||
|
idempotent Ice::SliceChecksumDict getSliceChecksums();
|
||||||
|
};
|
||||||
|
};
|
74
mumble/sample.mumbleadmin.ini
Normal file
74
mumble/sample.mumbleadmin.ini
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
[MURMUR]
|
||||||
|
# This section controls some general settings.
|
||||||
|
|
||||||
|
# The host of the Murmur server. This will be used to determine where to connect to
|
||||||
|
# for interaction for whichever interface you choose.
|
||||||
|
# Examples:
|
||||||
|
# fqdn.domain.tld
|
||||||
|
# 127.0.0.1
|
||||||
|
# shorthost
|
||||||
|
# ::1
|
||||||
|
host = localhost
|
||||||
|
|
||||||
|
# The type of interface to use. Currently, only "ice" and "grpc" are supported.
|
||||||
|
# "ice" is the default.
|
||||||
|
connection = "ice"
|
||||||
|
|
||||||
|
|
||||||
|
[GRPC]
|
||||||
|
# The GRPC interface is intended to (potentially) replace the ICE and DBUS interfaces.
|
||||||
|
# However, it's currently considered "experimental" - both upstream in Mumble/Murmur,
|
||||||
|
# and in this project. It's faster and more secure than Ice, however, if you've
|
||||||
|
# enabled TLS transport in your murmur.ini. It requires you to build murmur explicitly
|
||||||
|
# with grpc support, however.
|
||||||
|
|
||||||
|
# The port GRPC is running on.
|
||||||
|
port = 50051
|
||||||
|
|
||||||
|
# One of udp or tcp. You probably want to use tcp.
|
||||||
|
proto = tcp
|
||||||
|
|
||||||
|
# You probably will need to change this.
|
||||||
|
# If you need a copy, you can get the most recent at:
|
||||||
|
# https://github.com/mumble-voip/mumble/blob/master/src/murmur/MurmurRPC.proto
|
||||||
|
# If you leave this empty ("proto = "), we will attempt to fetch the slice from the remote
|
||||||
|
# instance ("MURMUR:host" above).
|
||||||
|
spec = /usr/local/lib/optools/mumble/murmurRPC.proto
|
||||||
|
|
||||||
|
# The maximum size for GRPC Messages (in KB)
|
||||||
|
# You're probably fine with the default.
|
||||||
|
max_size = 1024
|
||||||
|
|
||||||
|
|
||||||
|
[ICE]
|
||||||
|
# Ice is on its way out, but is currently the stable interface and most widely
|
||||||
|
# supported across versions.
|
||||||
|
|
||||||
|
# The port ICE is running on
|
||||||
|
port = 6502
|
||||||
|
|
||||||
|
# One of udp or tcp. You probably want to use tcp.
|
||||||
|
proto = tcp
|
||||||
|
|
||||||
|
# You probably will need to change this.
|
||||||
|
# If you need a copy, you can get the most recent at:
|
||||||
|
# https://github.com/mumble-voip/mumble/blob/master/src/murmur/Murmur.ice
|
||||||
|
# If you leave this empty ("slice = "), we will attempt to fetch the slice from the remote
|
||||||
|
# instance ("host" above).
|
||||||
|
spec = /usr/local/lib/optools/mumble/murmur.ice
|
||||||
|
|
||||||
|
# The maximum size for ICE Messages (in KB)
|
||||||
|
# You're probably fine with the default.
|
||||||
|
max_size = 1024
|
||||||
|
|
||||||
|
|
||||||
|
[AUTH]
|
||||||
|
# If both read and write are populated, write will be used preferentially.
|
||||||
|
|
||||||
|
# The Ice secret for read-only operations.
|
||||||
|
# Can be a blank string if you specify a write connection (see below).
|
||||||
|
read =
|
||||||
|
|
||||||
|
# The Ice secret for read+write operations.
|
||||||
|
# Set to a blank string if you want to only make a read-only connection.
|
||||||
|
write =
|
453
mumble/usermgmt.py
Executable file
453
mumble/usermgmt.py
Executable file
@ -0,0 +1,453 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class Manager(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
self.args = args
|
||||||
|
self.conn = self.connect()
|
||||||
|
self.conn.row_factory = sqlite3.Row
|
||||||
|
if 'interactive' not in self.args.keys():
|
||||||
|
self.args['interactive'] = False
|
||||||
|
# Key mappings/types in user_info table; thanks to DireFog in Mumble's IRC channel for help with this.
|
||||||
|
# src/murmur/ServerDB.h, enum UserInfo
|
||||||
|
# 0 = User_Name
|
||||||
|
# 1 = User_Email
|
||||||
|
# 2 = User_Comment
|
||||||
|
# 3 = User_Hash
|
||||||
|
# 4 = User_Password
|
||||||
|
# 5 = User_LastActive
|
||||||
|
# 6 = User_KDFIterations
|
||||||
|
self.infomap = {0: 'name',
|
||||||
|
1: 'email',
|
||||||
|
2: 'comment',
|
||||||
|
3: 'certhash',
|
||||||
|
4: 'password',
|
||||||
|
5: 'last_active',
|
||||||
|
6: 'kdf_iterations'}
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
if not os.path.isfile(self.args['database']):
|
||||||
|
raise FileNotFoundError('{0} does not exist! Check your path or create the initial databse by running murmurd.')
|
||||||
|
conn = sqlite3.connect(self.args['database'])
|
||||||
|
return(conn)
|
||||||
|
|
||||||
|
def add(self):
|
||||||
|
# SQLDO("INSERT INTO `%1users`... in src/murmur/ServerDB.cpp
|
||||||
|
if not (self.args['certhash'] or self.args['password']):
|
||||||
|
raise RuntimeError('You must specify either a certificate hash or a method for getting the password.')
|
||||||
|
if self.args['certhash']: # it's a certificate fingerprint hash
|
||||||
|
_e = '{0} is not a valid certificate fingerprint hash.'.format(self.args['certhash'])
|
||||||
|
try:
|
||||||
|
# Try *really hard* to mahe sure it's a SHA1.
|
||||||
|
# SHA1s are 160 bits in length, in hex (the string representations are
|
||||||
|
# 40 chars). However, we use 162 because of the prefix python3 adds
|
||||||
|
# automatically: "0b".
|
||||||
|
h = int(self.args['certhash'], 16)
|
||||||
|
try:
|
||||||
|
assert len(bin(h)) == 162
|
||||||
|
except AssertionError:
|
||||||
|
raise ValueError(_e)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
raise ValueError(_e)
|
||||||
|
if self.args['password']: # it's a password
|
||||||
|
if self.args['password'] == 'stdin':
|
||||||
|
self.args['password'] = hashlib.sha1(sys.stdin.read().replace('\n', '').encode('utf-8')).hexdigest().lower()
|
||||||
|
else:
|
||||||
|
_repeat = True
|
||||||
|
while _repeat == True:
|
||||||
|
_pass_in = getpass.getpass('What password should {0} have (will not echo back)? ')
|
||||||
|
if not _pass_in or _pass_in == '':
|
||||||
|
print('Invalid password. Please re-enter: ')
|
||||||
|
else:
|
||||||
|
_repeat = False
|
||||||
|
self.args['password'] = hashlib.sha1(_pass_in.replace('\n', '').encode('utf-8')).hexdigest().lower()
|
||||||
|
# Insert into the "users" table
|
||||||
|
# I spit on the Mumble developers for not using https://sqlite.org/autoinc.html.
|
||||||
|
# Warning: this is kind of dangerous, as you can hit a race condition here.
|
||||||
|
_cur = self.conn.cursor()
|
||||||
|
_cur.execute("SELECT user_id FROM users WHERE server_id = '{0}'".format(self.args['server']))
|
||||||
|
_used_ids = [i[0] for i in _cur.fetchall()]
|
||||||
|
_used_ids2 = [x for x in range(_used_ids[0], _used_ids[-1] + 1)]
|
||||||
|
_avail_uids = list(set(_used_ids) ^ set(_used_ids2))
|
||||||
|
_qinsert = {}
|
||||||
|
_qinsert['lastchannel'] = '0'
|
||||||
|
_qinsert['last_active'] = None # Change this to '' if it complains
|
||||||
|
_qinsert['texture'] = None # Change this to '' if it complains
|
||||||
|
_qinsert['uid'] = _avail_uids[0]
|
||||||
|
for k in ('username', 'server', 'password'):
|
||||||
|
_qinsert[k] = self.args[k]
|
||||||
|
for k in _qinsert.keys():
|
||||||
|
if not _qinsert[k]:
|
||||||
|
_qinsert[k] = ''
|
||||||
|
_q = ("INSERT INTO users (server_id, user_id, name, pw, lastchannel, texture, last_active) " +
|
||||||
|
"VALUES ('{server}', '{uid}', '{username}', '{password}', '{lastchannel}', '{texture}'," +
|
||||||
|
"'{last_active}')").format(**_qinsert)
|
||||||
|
_cur.execute(_q)
|
||||||
|
self.conn.commit()
|
||||||
|
# Insert into the "user_info" table
|
||||||
|
for c in ('name', 'email', 'certhash', 'comment'):
|
||||||
|
if self.args[c]:
|
||||||
|
_qinsert = {}
|
||||||
|
_qinsert['server'] = self.args['server']
|
||||||
|
_qinsert['user_id'] = _avail_uids[0]
|
||||||
|
_qinsert['keyid'] = list(self.infomap.keys())[list(self.infomap.values()).index(c)]
|
||||||
|
_qinsert['value'] = self.args[c]
|
||||||
|
_q = ("INSERT INTO user_info (server_id, user_id, key, value) " +
|
||||||
|
"VALUES ('{server}', '{user_id}', '{keyid}', '{value}')".format(**_qinsert))
|
||||||
|
_cur.execute(_q)
|
||||||
|
self.conn.commit()
|
||||||
|
_cur.close()
|
||||||
|
# Insert into the "group_members" table if we need to
|
||||||
|
if self.args['groups']:
|
||||||
|
# The groups table, thankfully, has autoincrement.
|
||||||
|
for g in self.args['groups']:
|
||||||
|
_ginfo = {}
|
||||||
|
_minsert = {'server': self.args['server'],
|
||||||
|
'uid': _avail_uids[0],
|
||||||
|
'addit': 1}
|
||||||
|
_ginsert = {'server': self.args['server'],
|
||||||
|
'name': g,
|
||||||
|
'chan_id': 0,
|
||||||
|
'inherit': 1,
|
||||||
|
'inheritable': 1}
|
||||||
|
_create = True
|
||||||
|
_cur = self.conn.cursor()
|
||||||
|
_q = "SELECT * FROM groups WHERE server_id = '{0}'".format(self.args['server'])
|
||||||
|
_cur.execute(_q)
|
||||||
|
for r in _cur.fetchall():
|
||||||
|
if r['name'] == g:
|
||||||
|
_create = False
|
||||||
|
_ginfo = r
|
||||||
|
break
|
||||||
|
if not _ginfo:
|
||||||
|
create = True # Just in case...
|
||||||
|
if _create:
|
||||||
|
_q = ("INSERT INTO groups (server_id, name, channel_id, inherit, inheritable) " +
|
||||||
|
"VALUES ('{server}', '{name}', '{chan_id}', '{inherit}', '{inheritable}')").format(**_ginsert)
|
||||||
|
_cur.execute(_q)
|
||||||
|
self.conn.commit()
|
||||||
|
_lastins = _cur.lastrowid
|
||||||
|
_q = ("SELECT * FROM groups WHERE group_id = '{0}' AND server_id = '{1}'").format(_lastins,
|
||||||
|
self.args['server'])
|
||||||
|
_cur.execute(_q)
|
||||||
|
_ginfo = _cur.fetchone()
|
||||||
|
_minsert['gid'] = _ginfo['group_id']
|
||||||
|
_q = ("INSERT INTO group_members (group_id, server_id, user_id, addit) " +
|
||||||
|
"VALUES ('{gid}', '{server}', '{uid}', '{addit}')").format(**_minsert)
|
||||||
|
_cur.execute(_q)
|
||||||
|
self.conn.commit()
|
||||||
|
_cur.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def rm(self):
|
||||||
|
_cur = self.conn.cursor()
|
||||||
|
# First we'll need the user's UID.
|
||||||
|
_q = "SELECT user_id FROM users WHERE server_id = '{0}' AND name = '{1}'".format(self.args['server'],
|
||||||
|
self.args['username'])
|
||||||
|
_cur.execute(_q)
|
||||||
|
_uid = _cur.fetchone()[0]
|
||||||
|
# Then we get the groups the user's in; we'll need these in a bit.
|
||||||
|
_q = "SELECT group_id FROM group_members WHERE server_id = '{0}' AND user_id = '{0}'".format(self.args['server'],
|
||||||
|
_uid)
|
||||||
|
_cur.execute(_q)
|
||||||
|
_groups = [g[0] for g in _cur.fetchall()]
|
||||||
|
# Okay, now we can delete the user and their metadata...
|
||||||
|
_qtmpl = "DELETE FROM {0} WHERE server_id = '{1}' AND user_id = '{2}'"
|
||||||
|
for t in ('users', 'group_members'):
|
||||||
|
_q = _qtmpl.format(t, self.args['server'], _uid)
|
||||||
|
_cur.execute(_q)
|
||||||
|
self.conn.commit()
|
||||||
|
if not self.args['noprune']:
|
||||||
|
for t in ('user_info', 'acl'):
|
||||||
|
_q = _qtmpl.format(t, self.args['server'], _uid)
|
||||||
|
_cur.execute(_q)
|
||||||
|
self.conn.commit()
|
||||||
|
# Now some groups maintenance.
|
||||||
|
if self.args['prunegrps']:
|
||||||
|
for gid in _groups:
|
||||||
|
_q = ("SELECT COUNT(*) FROM group_members WHERE " +
|
||||||
|
"server_id = '{0}' AND group_id = '{1}'").format(self.args['server'],
|
||||||
|
gid)
|
||||||
|
_cur.execute(_q)
|
||||||
|
if _cur.fetchone()[0] == 0:
|
||||||
|
_q = ("DELETE FROM group_members WHERE " +
|
||||||
|
"server_id = '{0}' AND group_id = '{1}'").format(self.args['server'],
|
||||||
|
gid)
|
||||||
|
_cur.execute(_q)
|
||||||
|
self.conn.commit()
|
||||||
|
_cur.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def lsUsers(self):
|
||||||
|
users = {}
|
||||||
|
_fields = ('server_id', 'user_id', 'name', 'pw', 'lastchannel', 'texture', 'last_active')
|
||||||
|
if self.args['server']:
|
||||||
|
try:
|
||||||
|
self.args['server'] = int(self.args['server'])
|
||||||
|
_q = "SELECT * FROM users WHERE server_id = '{0}'".format(self.args['server'])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass # It's set as None, which we'll parse to mean as "all" per the --help output.
|
||||||
|
else:
|
||||||
|
_q = 'SELECT * FROM users'
|
||||||
|
_cur = self.conn.cursor()
|
||||||
|
_cur.execute(_q)
|
||||||
|
for r in _cur.fetchall():
|
||||||
|
_usr = r['user_id']
|
||||||
|
users[_usr] = {}
|
||||||
|
for f in _fields:
|
||||||
|
if f != 'user_id': # We set the dict key as this
|
||||||
|
users[_usr][f] = r[f]
|
||||||
|
_q = "SELECT * FROM user_info WHERE server_id = '{0}' AND user_id = '{1}'".format(r['server_id'],
|
||||||
|
r['user_id'])
|
||||||
|
_cur2 = self.conn.cursor()
|
||||||
|
_cur2.execute(_q)
|
||||||
|
for r2 in _cur2.fetchall():
|
||||||
|
if r2['key'] in self.infomap.keys():
|
||||||
|
users[_usr][self.infomap[r2['key']]] = r2['value']
|
||||||
|
_cur2.close()
|
||||||
|
for k in self.infomap.keys():
|
||||||
|
if self.infomap[k] not in users[_usr].keys():
|
||||||
|
users[_usr][self.infomap[k]] = None
|
||||||
|
if users[_usr]['comment']:
|
||||||
|
users[_usr]['comment'] = ('(truncated)' if len(users[_usr]['comment']) >= 32 else users[_usr]['comment'])
|
||||||
|
_cur.close()
|
||||||
|
#pprint.pprint(users)
|
||||||
|
# Now we print (or just return) the results. Whew.
|
||||||
|
if not self.args['interactive']:
|
||||||
|
return(users)
|
||||||
|
print_tmpl = ('{0:6}\t{1:3}\t{2:12} {3:24} {4:40} {5:40} {6:12} ' +
|
||||||
|
'{7:19} {8:32}')
|
||||||
|
print(print_tmpl.format('Server','UID','Username','Email',
|
||||||
|
'Password', 'Certhash', 'Last Channel',
|
||||||
|
'Last Active', 'Comment'), end = '\n\n')
|
||||||
|
for uid in users.keys():
|
||||||
|
d = users[uid]
|
||||||
|
print(print_tmpl.format(int(d['server_id']),
|
||||||
|
int(uid),
|
||||||
|
str(d['name']),
|
||||||
|
str(d['email']),
|
||||||
|
str(d['pw']),
|
||||||
|
str(d['certhash']),
|
||||||
|
(str(d['lastchannel']) if not d['lastchannel'] else int(d['lastchannel'])),
|
||||||
|
str(d['last_active']),
|
||||||
|
str(d['comment'])))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def lsGroups(self):
|
||||||
|
groups = {}
|
||||||
|
_cur = self.conn.cursor()
|
||||||
|
# First, we get the groups.
|
||||||
|
if self.args['server']:
|
||||||
|
_q = "SELECT * FROM groups WHERE server_id = '{0}'".format(self.args['server'])
|
||||||
|
else:
|
||||||
|
_q = "SELECT * FROM groups"
|
||||||
|
_cur.execute(_q)
|
||||||
|
for r in _cur.fetchall():
|
||||||
|
_gid = r['group_id']
|
||||||
|
groups[_gid] = {'server': r['server_id'],
|
||||||
|
'name': r['name'],
|
||||||
|
'chan_id': r['channel_id'],
|
||||||
|
'inherit': r['inherit'],
|
||||||
|
'inheritable': r['inheritable']}
|
||||||
|
groups[_gid]['members'] = {}
|
||||||
|
_cur2 = self.conn.cursor()
|
||||||
|
_q2 = "SELECT * FROM group_members WHERE group_id = '{0}' AND server_id = '{1}'".format(_gid,
|
||||||
|
groups[_gid]['server'])
|
||||||
|
_cur2.execute(_q2)
|
||||||
|
for r2 in _cur2.fetchall():
|
||||||
|
# True means they are a member of the group. False means they are excluded from the group.
|
||||||
|
# (Helps override default policies?)
|
||||||
|
groups[_gid]['members'][r2['user_id']] = (True if r2['addit'] else False)
|
||||||
|
_cur2.close()
|
||||||
|
_cur.close()
|
||||||
|
# Return if we're non-interactive...
|
||||||
|
if not self.args['interactive']:
|
||||||
|
return(groups)
|
||||||
|
# Print the groups
|
||||||
|
print('GROUPS:')
|
||||||
|
print_tmpl = ('{0:3}\t{1:16}\t{2:10}\t{3:35}\t{4:30}')
|
||||||
|
print(print_tmpl.format('GID', 'Name', 'Channel ID',
|
||||||
|
'Inherit Parent Channel Permissions?', 'Allow Sub-channels to Inherit?'), end = '\n\n')
|
||||||
|
for g in groups.keys():
|
||||||
|
d = groups[g]
|
||||||
|
print(print_tmpl.format(g,
|
||||||
|
d['name'],
|
||||||
|
d['chan_id'],
|
||||||
|
str(True if d['inherit'] == 1 else False),
|
||||||
|
str(True if d['inheritable'] == 1 else False)))
|
||||||
|
print('\n\nMEMBERSHIPS:')
|
||||||
|
# And print the members
|
||||||
|
print_tmpl = ('\t\t{0:3}\t{1:>19}') # UID, Include or Exclude?
|
||||||
|
for g in groups.keys():
|
||||||
|
d = groups[g]
|
||||||
|
print('{0} ({1}):'.format(d['name'], g))
|
||||||
|
if d['members']:
|
||||||
|
print(print_tmpl.format('UID', 'Include or Exclude?'), end = '\n\n')
|
||||||
|
for m in d['members'].keys():
|
||||||
|
print(print_tmpl.format(m, ('Include' if d['members'][m] == 1 else 'Exclude')))
|
||||||
|
else:
|
||||||
|
print('\t\tNo members found; group is empty.')
|
||||||
|
return()
|
||||||
|
|
||||||
|
def edit(self):
|
||||||
|
print('Editing is not currently supported.')
|
||||||
|
return()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.conn.close()
|
||||||
|
if self.args['operation'] in ('add', 'rm', 'edit'):
|
||||||
|
_cmd = ['systemctl', 'restart', 'murmur']
|
||||||
|
subprocess.run(_cmd)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
_db = '/var/lib/murmur/murmur.sqlite'
|
||||||
|
commonargs = argparse.ArgumentParser(add_help = False)
|
||||||
|
reqcommon = commonargs.add_argument_group('REQUIRED common arguments')
|
||||||
|
reqcommon.add_argument('-u',
|
||||||
|
'--user',
|
||||||
|
type = str,
|
||||||
|
dest = 'username',
|
||||||
|
required = True,
|
||||||
|
help = 'The username to perform the action for.')
|
||||||
|
reqcommon.add_argument('-s',
|
||||||
|
'--server',
|
||||||
|
type = int,
|
||||||
|
dest = 'server',
|
||||||
|
default = 1,
|
||||||
|
help = 'The server ID. Defaults to \033[1m{0}\033[0m'.format(1))
|
||||||
|
commonargs.add_argument('-d',
|
||||||
|
'--database',
|
||||||
|
type = str,
|
||||||
|
dest = 'database',
|
||||||
|
metavar = '/path/to/murmur.sqlite3',
|
||||||
|
default = _db,
|
||||||
|
help = 'The path to the sqlite3 database for Murmur. Default: \033[1m{0}\033[0m'.format(_db))
|
||||||
|
args = argparse.ArgumentParser(epilog = 'This program has context-sensitive help (e.g. try "... add --help")')
|
||||||
|
subparsers = args.add_subparsers(help = 'Operation to perform',
|
||||||
|
dest = 'operation')
|
||||||
|
addargs = subparsers.add_parser('add',
|
||||||
|
parents = [commonargs],
|
||||||
|
help = 'Add a user to the Murmur database')
|
||||||
|
delargs = subparsers.add_parser('rm',
|
||||||
|
parents = [commonargs],
|
||||||
|
help = 'Remove a user from the Murmur database')
|
||||||
|
listargs = subparsers.add_parser('ls',
|
||||||
|
help = 'List users in the Murmur database')
|
||||||
|
editargs = subparsers.add_parser('edit',
|
||||||
|
parents = [commonargs],
|
||||||
|
help = 'Edit a user in the Murmur database')
|
||||||
|
# Operation-specific optional arguments
|
||||||
|
addargs.add_argument('-n',
|
||||||
|
'--name',
|
||||||
|
type = str,
|
||||||
|
metavar = '"Firstname Lastname"',
|
||||||
|
dest = 'name',
|
||||||
|
default = None,
|
||||||
|
help = 'The new user\'s (real) name')
|
||||||
|
addargs.add_argument('-c',
|
||||||
|
'--comment',
|
||||||
|
type = str,
|
||||||
|
metavar = '"This comment becomes the user\'s profile."',
|
||||||
|
dest = 'comment',
|
||||||
|
default = None,
|
||||||
|
help = 'The comment for the new user')
|
||||||
|
addargs.add_argument('-e',
|
||||||
|
'--email',
|
||||||
|
type = str,
|
||||||
|
metavar = 'email@domain.tld',
|
||||||
|
dest = 'email',
|
||||||
|
default = None,
|
||||||
|
help = 'The email address for the new user')
|
||||||
|
addargs.add_argument('-C',
|
||||||
|
'--certhash',
|
||||||
|
type = str,
|
||||||
|
metavar = 'CERTIFICATE_FINGERPRINT_HASH',
|
||||||
|
default = None,
|
||||||
|
dest = 'certhash',
|
||||||
|
help = ('The certificate fingerprint hash. See genfprhash.py. ' +
|
||||||
|
'If you do not specify this, you must specify -p/--passwordhash'))
|
||||||
|
addargs.add_argument('-p',
|
||||||
|
'--passwordhash',
|
||||||
|
type = str,
|
||||||
|
dest = 'password',
|
||||||
|
choices = ['stdin', 'prompt'],
|
||||||
|
default = None,
|
||||||
|
help = ('If not specified, you must specify -C/--certhash. Otherwise, either ' +
|
||||||
|
'\'stdin\' (the password is being piped into this program) or \'prompt\' ' +
|
||||||
|
'(a password will be asked for in a non-echoing prompt). "prompt" is much more secure and recommended.'))
|
||||||
|
addargs.add_argument('-g',
|
||||||
|
'--groups',
|
||||||
|
type = str,
|
||||||
|
metavar = 'GROUP1(,GROUP2,GROUP3...)',
|
||||||
|
default = None,
|
||||||
|
help = ('A comma-separated list of groups the user should be added to. If a group ' +
|
||||||
|
'doesn\'t exist, it will be created'))
|
||||||
|
# Listing should only take the DB as the "common" arg
|
||||||
|
listargs.add_argument('-g',
|
||||||
|
'--groups',
|
||||||
|
action = 'store_true',
|
||||||
|
dest = 'groups',
|
||||||
|
help = 'If specified, list groups (and their members), not users')
|
||||||
|
listargs.add_argument('-s',
|
||||||
|
'--server',
|
||||||
|
type = str,
|
||||||
|
dest = 'server',
|
||||||
|
default = None,
|
||||||
|
help = 'The server ID. Defaults to all servers. Specify one by the numerical ID.')
|
||||||
|
listargs.add_argument('-d',
|
||||||
|
'--database',
|
||||||
|
type = str,
|
||||||
|
dest = 'database',
|
||||||
|
metavar = '/path/to/murmur.sqlite3',
|
||||||
|
default = _db,
|
||||||
|
help = 'The path to the sqlite3 database for Murmur. Default: \033[1m{0}\033[0m'.format(_db))
|
||||||
|
# Deleting args
|
||||||
|
delargs.add_argument('-n',
|
||||||
|
'--no-prune',
|
||||||
|
dest = 'noprune',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, do NOT remove the ACLs and user info for the user as well (profile, ' +
|
||||||
|
'certificate fingerprint, etc.)'))
|
||||||
|
delargs.add_argument('-P',
|
||||||
|
'--prune-groups',
|
||||||
|
dest = 'prunegrps',
|
||||||
|
action = 'store_true',
|
||||||
|
help = 'If specified, remove any groups the user was in that are now empty (i.e. the user was the only member)')
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
if not args['operation']:
|
||||||
|
#raise RuntimeError('You must specify an operation to perform. Try running with -h/--help.')
|
||||||
|
exit('You must specify an operation to perform. Try running with -h/--help.')
|
||||||
|
args['interactive'] = True
|
||||||
|
#pprint.pprint(args)
|
||||||
|
mgmt = Manager(args)
|
||||||
|
if args['operation'] == 'add':
|
||||||
|
if args['groups']:
|
||||||
|
mgmt.args['groups'] = [g.strip() for g in args['groups'].split(',')]
|
||||||
|
mgmt.add()
|
||||||
|
elif args['operation'] == 'rm':
|
||||||
|
mgmt.rm()
|
||||||
|
elif args['operation'] == 'ls':
|
||||||
|
if not args['groups']:
|
||||||
|
mgmt.lsUsers()
|
||||||
|
else:
|
||||||
|
mgmt.lsGroups()
|
||||||
|
elif args['operation'] == 'edit':
|
||||||
|
mgmt.edit()
|
||||||
|
else:
|
||||||
|
pass # No-op because something went SUPER wrong.
|
||||||
|
mgmt.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
382
mumble/usrmgmt2.py
Executable file
382
mumble/usrmgmt2.py
Executable file
@ -0,0 +1,382 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Thanks to https://github.com/alfg/murmur-rest/blob/master/app/__init__.py
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from collections import defaultdict
|
||||||
|
import configparser
|
||||||
|
import datetime
|
||||||
|
import email.utils
|
||||||
|
import getpass
|
||||||
|
import hashlib
|
||||||
|
import Ice # python-zeroc-ice in AUR
|
||||||
|
import IcePy # python-zeroc-ice in AUR
|
||||||
|
import getpass
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
class IceMgr(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
self.args = args
|
||||||
|
if 'interactive' not in self.args.keys():
|
||||||
|
self.args['interactive'] = False
|
||||||
|
if self.args['verbose']:
|
||||||
|
import pprint
|
||||||
|
self.getCfg()
|
||||||
|
if self.cfg['MURMUR']['connection'] == '':
|
||||||
|
self.cfg['MURMUR']['connection'] == 'ice'
|
||||||
|
self.connect(self.cfg['MURMUR']['connection'])
|
||||||
|
|
||||||
|
def getCfg(self):
|
||||||
|
_cfg = os.path.join(os.path.abspath(os.path.expanduser(self.args['cfgfile'])))
|
||||||
|
if not os.path.isfile(_cfg):
|
||||||
|
raise FileNotFoundError('{0} does not exist!'.format(_cfg))
|
||||||
|
return()
|
||||||
|
_parser = configparser.ConfigParser()
|
||||||
|
_parser._interpolation = configparser.ExtendedInterpolation()
|
||||||
|
_parser.read(_cfg)
|
||||||
|
self.cfg = defaultdict(dict)
|
||||||
|
for section in _parser.sections():
|
||||||
|
self.cfg[section] = {}
|
||||||
|
for option in _parser.options(section):
|
||||||
|
self.cfg[section][option] = _parser.get(section, option)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def connect(self, ctxtype):
|
||||||
|
ctxtype = ctxtype.strip().upper()
|
||||||
|
if ctxtype.lower() not in ('ice', 'grpc'):
|
||||||
|
raise ValueError('You have specified an invalid connection type.')
|
||||||
|
_cxcfg = self.cfg[ctxtype]
|
||||||
|
self.cfg[ctxtype]['spec'] = os.path.join(os.path.abspath(os.path.expanduser(self.cfg[ctxtype]['spec'])))
|
||||||
|
# ICE START
|
||||||
|
_props = {'ImplicitContext': 'Shared',
|
||||||
|
'Default.EncodingVersion': '1.0',
|
||||||
|
'MessageSizeMax': str(self.cfg['ICE']['max_size'])}
|
||||||
|
_prop_data = Ice.createProperties()
|
||||||
|
for k, v in _props.items():
|
||||||
|
_prop_data.setProperty('Ice.{0}'.format(k), v)
|
||||||
|
_conn = Ice.InitializationData()
|
||||||
|
_conn.properties = _prop_data
|
||||||
|
self.ice = Ice.initialize(_conn)
|
||||||
|
_host = 'Meta:{0} -h {1} -p {2} -t 1000'.format(self.cfg['ICE']['proto'],
|
||||||
|
self.cfg['ICE']['host'],
|
||||||
|
self.cfg['ICE']['port'])
|
||||||
|
_ctx = self.ice.stringToProxy(_host)
|
||||||
|
# I owe a lot of neat tricks here to:
|
||||||
|
# https://raw.githubusercontent.com/mumble-voip/mumble-scripts/master/Helpers/mice.py
|
||||||
|
# Namely, the load-slice-from-server stuff especially
|
||||||
|
_slicedir = Ice.getSliceDir()
|
||||||
|
if not _slicedir:
|
||||||
|
_slicedir = ["-I/usr/share/Ice/slice", "-I/usr/share/slice"]
|
||||||
|
else:
|
||||||
|
_slicedir = ['-I' + _slicedir]
|
||||||
|
if self.cfg['ICE']['slice'] == '':
|
||||||
|
if IcePy.intVersion() < 30500:
|
||||||
|
# Old 3.4 signature with 9 parameters
|
||||||
|
_op = IcePy.Operation('getSlice',
|
||||||
|
Ice.OperationMode.Idempotent,
|
||||||
|
Ice.OperationMode.Idempotent,
|
||||||
|
True,
|
||||||
|
(), (), (),
|
||||||
|
IcePy._t_string, ())
|
||||||
|
else:
|
||||||
|
# New 3.5 signature with 10 parameters.
|
||||||
|
_op = IcePy.Operation('getSlice',
|
||||||
|
Ice.OperationMode.Idempotent,
|
||||||
|
Ice.OperationMode.Idempotent,
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
(), (), (),
|
||||||
|
((), IcePy._t_string, False, 0),
|
||||||
|
())
|
||||||
|
_slice = _op.invoke(_ctx,
|
||||||
|
((), None))
|
||||||
|
(_filedesc, _filepath) = tempfile.mkstemp(suffix = '.ice')
|
||||||
|
_slicefile = os.fdopen(_filedesc, 'w')
|
||||||
|
_slicefile.write(_slice)
|
||||||
|
_slicefile.flush()
|
||||||
|
Ice.loadSlice('', _slicedir + [_filepath])
|
||||||
|
_slicefile.close()
|
||||||
|
os.remove(_filepath)
|
||||||
|
else: # A .ice file was explicitly defined in the cfg
|
||||||
|
_slicedir.append(self.cfg[ctxtype]['spec'])
|
||||||
|
Ice.loadSlice('', _slicedir)
|
||||||
|
import Murmur
|
||||||
|
self.conn = {}
|
||||||
|
if self.cfg['AUTH']['read'] != '':
|
||||||
|
_secret = self.ice.getImplicitContext().put("secret",
|
||||||
|
self.cfg['AUTH']['read'])
|
||||||
|
self.conn['read'] = Murmur.MetaPrx.checkedCast(_ctx)
|
||||||
|
else:
|
||||||
|
self.conn['read'] = False
|
||||||
|
if self.cfg['AUTH']['write'] != '':
|
||||||
|
_secret = self.ice.getImplicitContext().put("secret",
|
||||||
|
self.cfg['AUTH']['write'])
|
||||||
|
self.conn['write'] = Murmur.MetaPrx.checkedCast(_ctx)
|
||||||
|
else:
|
||||||
|
self.conn['write'] = False
|
||||||
|
return()
|
||||||
|
|
||||||
|
def dictify(self, obj):
|
||||||
|
# Thanks to:
|
||||||
|
# https://github.com/alfg/murmur-rest/blob/master/app/utils.py
|
||||||
|
# (Modified to be python 3 compatible)
|
||||||
|
_rv = {'_type': str(type(obj))}
|
||||||
|
if type(obj) in (bool, int, float, str, bytes):
|
||||||
|
return(obj)
|
||||||
|
if type(obj) in (list, tuple):
|
||||||
|
return([dictify(i) for i in obj])
|
||||||
|
if type(obj) == dict:
|
||||||
|
return(dict((str(k), dictify(v)) for k, v in obj.items()))
|
||||||
|
return(dictify(obj.__dict__))
|
||||||
|
|
||||||
|
def add(self):
|
||||||
|
_userinfo = {Murmur.UserInfo.UserName: self.args['UserName']}
|
||||||
|
if not self.conn['write']:
|
||||||
|
raise PermissionError('You do not have write access configured!')
|
||||||
|
if not (self.args['certhash'] or self.args['password']):
|
||||||
|
raise RuntimeError(('You must specify either a certificate hash ' +
|
||||||
|
'or a method for getting the password.'))
|
||||||
|
if self.args['certhash']: # it's a certificate fingerprint hash
|
||||||
|
_e = '{0} is not a valid certificate fingerprint hash.'.format(self.args['certhash'])
|
||||||
|
try:
|
||||||
|
# Try *really hard* to mahe sure it's a SHA1.
|
||||||
|
# SHA1s are 160 bits in length, in binary representation.
|
||||||
|
# (the string representations are 40 chars in hex).
|
||||||
|
# However, we use 161 because of the prefix python3 adds
|
||||||
|
# automatically: "0b". I know. "This should be 162!" Shut up, trust me.
|
||||||
|
# Change it to 162 and watch it break if you don't believe me.
|
||||||
|
h = int(self.args['certhash'], 16)
|
||||||
|
try:
|
||||||
|
assert len(bin(h)) == 161
|
||||||
|
#_userinfo[Murmur.UserInfo.UserPassword] = None
|
||||||
|
_userinfo[Murmur.UserInfo.UserHash] = self.args['UserHash']
|
||||||
|
except AssertionError:
|
||||||
|
raise ValueError(_e)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
raise ValueError(_e)
|
||||||
|
if self.args['UserPassword']: # it's a password
|
||||||
|
if self.args['UserPassword'] == 'stdin':
|
||||||
|
#self.args['password'] = hashlib.sha1(sys.stdin.read().replace('\n', '').encode('utf-8')).hexdigest().lower()
|
||||||
|
_userinfo[Murmur.UserInfo.UserPassword] = sys.stdin.read().replace('\n', '').encode('utf-8')
|
||||||
|
#_userinfo[Murmur.UserInfo.UserHash] = None
|
||||||
|
else:
|
||||||
|
_repeat = True
|
||||||
|
while _repeat == True:
|
||||||
|
_pass_in = getpass.getpass('What password should {0} have (will not echo back)? '.format(self.args['UserName']))
|
||||||
|
if not _pass_in or _pass_in == '':
|
||||||
|
print('Invalid password. Please re-enter: ')
|
||||||
|
else:
|
||||||
|
_repeat = False
|
||||||
|
#self.args['password'] = hashlib.sha1(_pass_in.replace('\n', '').encode('utf-8')).hexdigest().lower()
|
||||||
|
_userinfo[Murmur.UserInfo.UserPassword] = _pass_in.replace('\n', '').encode('utf-8')
|
||||||
|
#_userinfo[Murmur.UserInfo.UserHash] = None
|
||||||
|
# Validate the email address
|
||||||
|
if self.args['UserEmail']:
|
||||||
|
_email = email.utils.parseaddr(self.args['UserEmail'])
|
||||||
|
# This is a stupidly simplified regex. For reasons why, see:
|
||||||
|
# https://stackoverflow.com/questions/8022530/python-check-for-valid-email-address
|
||||||
|
# http://www.regular-expressions.info/email.html
|
||||||
|
# TL;DR: email is really fucking hard to regex against,
|
||||||
|
# even (especially) if you follow RFC5322, and I don't want to have
|
||||||
|
# to rely on https://pypi.python.org/pypi/validate_email
|
||||||
|
if not re.match('[^@]+@[^@]+\.[^@]+', _email[1]):
|
||||||
|
raise ValueError('{0} is not a valid email address!'.format(self.args['UserEmail']))
|
||||||
|
else:
|
||||||
|
_userinfo[Murmur.UserInfo.UserEmail] = _email[1]
|
||||||
|
#else:
|
||||||
|
# _userinfo[Murmur.UserInfo.UserEmail] = None
|
||||||
|
if self.args['UserComment']:
|
||||||
|
_userinfo[Murmur.UserInfo.UserComment] = self.args['UserComment']
|
||||||
|
# Set a dummy LastActive
|
||||||
|
_userinfo[Murmur.UserInfo.LastActive] = str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
|
# Now we Do the Thing(TM)
|
||||||
|
_server = self.conn['write'].getServer(self.args['server'])
|
||||||
|
_regid = _server.registerUser(_userinfo)
|
||||||
|
# And a little more Doing the Thing(TM), add groups.
|
||||||
|
# This is... a little convoluted.
|
||||||
|
# See https://sourceforge.net/p/mumble/discussion/492607/thread/579de8f9/
|
||||||
|
if args['groups']:
|
||||||
|
# First we get the ACL listings. The groups are *actually stored in
|
||||||
|
# the ACLs*, which is... insane to me, sort of, but whatever.
|
||||||
|
_acl = _server.getACL()
|
||||||
|
# Then build a dict of all groups to assign.
|
||||||
|
_groups = {}
|
||||||
|
for g in self.args['groups'].split(','):
|
||||||
|
_g = g.strip().split(':')
|
||||||
|
if _g[0] not in _groups.keys():
|
||||||
|
_groups[_g[0]] = [g[1]]
|
||||||
|
else:
|
||||||
|
_groups[_g[0]].append(_g[1])
|
||||||
|
# Now we need to see which groups currently exist and which down't.
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
print('Added user {0} (UID: {1})'.format(self.args['UserName'],
|
||||||
|
_regid))
|
||||||
|
if self.args['verbose']:
|
||||||
|
_u = _server.getRegistration(_regid)
|
||||||
|
pprint.pprint(self.dictify(_u))
|
||||||
|
|
||||||
|
return()
|
||||||
|
|
||||||
|
def rm(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def lsUsers(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def lsGroups(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def edit(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def status(self):
|
||||||
|
# https://github.com/alfg/murmur-rest/blob/master/app/api.py#L71
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.ice.destroy()
|
||||||
|
if self.cfg['TUNNEL']['enable'].lower() in ('', 'true'):
|
||||||
|
self.ssh.stop()
|
||||||
|
self.ssh.close()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
_cfgfile = os.path.abspath(os.path.join(os.path.expanduser('~'),
|
||||||
|
'.config',
|
||||||
|
'optools',
|
||||||
|
'mumbleadmin.ini'))
|
||||||
|
commonargs = argparse.ArgumentParser(add_help = False)
|
||||||
|
reqcommon = commonargs.add_argument_group('REQUIRED common arguments')
|
||||||
|
optcommon = argparse.ArgumentParser(add_help = False)
|
||||||
|
reqcommon.add_argument('-u', '--user',
|
||||||
|
type = str,
|
||||||
|
dest = 'UserName',
|
||||||
|
required = True,
|
||||||
|
help = 'The username to perform the action for.')
|
||||||
|
reqcommon.add_argument('-s', '--server',
|
||||||
|
type = int,
|
||||||
|
dest = 'server',
|
||||||
|
default = 1,
|
||||||
|
help = ('The server ID. ' +
|
||||||
|
'Defaults to \033[1m{0}\033[0m').format(1))
|
||||||
|
optcommon.add_argument('-f', '--config',
|
||||||
|
type = str,
|
||||||
|
dest = 'cfgfile',
|
||||||
|
metavar = '/path/to/mumbleadmin.ini',
|
||||||
|
default = _cfgfile,
|
||||||
|
help = ('The path to the configuration file ' +
|
||||||
|
'("mumleadmin.ini"). Default: \033[1m{0}\033[0m').format(_cfgfile))
|
||||||
|
optcommon.add_argument('-v', '--verbose',
|
||||||
|
dest = 'verbose',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, print more information than normal'))
|
||||||
|
args = argparse.ArgumentParser(epilog = 'This program has context-sensitive help (e.g. try "... add --help")')
|
||||||
|
subparsers = args.add_subparsers(help = 'Operation to perform',
|
||||||
|
dest = 'operation')
|
||||||
|
addargs = subparsers.add_parser('add',
|
||||||
|
parents = [commonargs, optcommon],
|
||||||
|
help = 'Add a user to the Murmur database')
|
||||||
|
delargs = subparsers.add_parser('rm',
|
||||||
|
parents = [commonargs, optcommon],
|
||||||
|
help = 'Remove a user from the Murmur database')
|
||||||
|
listargs = subparsers.add_parser('ls',
|
||||||
|
parents = [optcommon],
|
||||||
|
help = 'List users in the Murmur database')
|
||||||
|
editargs = subparsers.add_parser('edit',
|
||||||
|
parents = [commonargs, optcommon],
|
||||||
|
help = 'Edit a user in the Murmur database')
|
||||||
|
# Operation-specific optional arguments
|
||||||
|
# Why did I even add this? It's not used *anywhere*.
|
||||||
|
#addargs.add_argument('-n', '--name',
|
||||||
|
# type = str,
|
||||||
|
# metavar = '"Firstname Lastname"',
|
||||||
|
# dest = 'name',
|
||||||
|
# default = None,
|
||||||
|
# help = 'The new user\'s (real) name')
|
||||||
|
addargs.add_argument('-c', '--comment',
|
||||||
|
type = str,
|
||||||
|
metavar = '"This comment becomes the user\'s profile."',
|
||||||
|
dest = 'UserComment',
|
||||||
|
default = None,
|
||||||
|
help = 'The comment for the new user')
|
||||||
|
addargs.add_argument('-e', '--email',
|
||||||
|
type = str,
|
||||||
|
metavar = 'email@domain.tld',
|
||||||
|
dest = 'UserEmail',
|
||||||
|
default = None,
|
||||||
|
help = 'The email address for the new user')
|
||||||
|
addargs.add_argument('-C', '--certhash',
|
||||||
|
type = str,
|
||||||
|
metavar = 'CERTIFICATE_FINGERPRINT_HASH',
|
||||||
|
default = None,
|
||||||
|
dest = 'UserHash',
|
||||||
|
help = ('The certificate fingerprint hash. See gencerthash.py. ' +
|
||||||
|
'This is the preferred way. ' +
|
||||||
|
'If you do not specify this, you must specify -p/--passwordhash'))
|
||||||
|
addargs.add_argument('-p', '--password',
|
||||||
|
type = str,
|
||||||
|
dest = 'UserPassword',
|
||||||
|
choices = ['stdin', 'prompt'],
|
||||||
|
default = None,
|
||||||
|
help = ('If not specified, you must specify -C/--certhash. Otherwise, either ' +
|
||||||
|
'\'stdin\' (the password is being piped into this program) or \'prompt\' ' +
|
||||||
|
'(a password will be asked for in a non-echoing prompt). "prompt" is much more secure and recommended.'))
|
||||||
|
addargs.add_argument('-g', '--groups',
|
||||||
|
type = str,
|
||||||
|
metavar = 'CHANID:GROUP1(,CHANID:GROUP2,CHANID:GROUP3...)',
|
||||||
|
default = None,
|
||||||
|
help = ('A comma-separated list of groups the user should be added to. If a group ' +
|
||||||
|
'doesn\'t exist, it will be created. CHANID is a ' +
|
||||||
|
'numerical ID of the channel to assign the group to. ' +
|
||||||
|
'(You can get channel IDs by doing "... ls -gv".) ' +
|
||||||
|
'If no CHANID is provided, the root channel (0) will be used.'))
|
||||||
|
# Listing should only take the DB as the "common" arg
|
||||||
|
listargs.add_argument('-g', '--groups',
|
||||||
|
action = 'store_true',
|
||||||
|
dest = 'groups',
|
||||||
|
help = 'If specified, list groups (and their members), not users')
|
||||||
|
listargs.add_argument('-s', '--server',
|
||||||
|
type = str,
|
||||||
|
dest = 'server',
|
||||||
|
default = None,
|
||||||
|
help = 'The server ID. Defaults to all servers. Specify one by the numerical ID.')
|
||||||
|
# Deleting args
|
||||||
|
delargs.add_argument('-n', '--no-prune',
|
||||||
|
dest = 'noprune',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, do NOT remove the ACLs and user info for the user as well (profile, ' +
|
||||||
|
'certificate fingerprint, etc.)'))
|
||||||
|
delargs.add_argument('-P', '--prune-groups',
|
||||||
|
dest = 'prunegrps',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, remove any groups the user was in ' +
|
||||||
|
'that are now empty (i.e. the user was the only member)'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
if not args['operation']:
|
||||||
|
#raise RuntimeError('You must specify an operation to perform. Try running with -h/--help.')
|
||||||
|
exit('You must specify an operation to perform. Try running with -h/--help.')
|
||||||
|
args['interactive'] = True
|
||||||
|
mgmt = IceMgr(args)
|
||||||
|
if args['operation'] == 'add':
|
||||||
|
mgmt.add()
|
||||||
|
elif args['operation'] == 'rm':
|
||||||
|
mgmt.rm()
|
||||||
|
elif args['operation'] == 'ls':
|
||||||
|
if not args['groups']:
|
||||||
|
mgmt.lsUsers()
|
||||||
|
else:
|
||||||
|
mgmt.lsGroups()
|
||||||
|
elif args['operation'] == 'edit':
|
||||||
|
mgmt.edit()
|
||||||
|
else:
|
||||||
|
pass # No-op because something went SUPER wrong.
|
||||||
|
mgmt.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
84
mysql/tblinfo.py
Executable file
84
mysql/tblinfo.py
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import copy
|
||||||
|
import os
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
mysql_internal = ['information_schema', 'mysql']
|
||||||
|
|
||||||
|
# Not used, but could be in the future.
|
||||||
|
stat_hdrs = ['Name', 'Engine', 'Version', 'Row_format', 'Rows', 'Avg_row_length', 'Data_length',
|
||||||
|
'Max_data_length', 'Index_length', 'Data_free', 'Auto_increment', 'Create_time',
|
||||||
|
'Update_time', 'Check_time', 'Collation', 'Checksum', 'Create_options', 'Comment']
|
||||||
|
tblinfo_hdrs = ['Field', 'Type', 'Null', 'Key', 'Default', 'Extra']
|
||||||
|
|
||||||
|
def get_info(db, internal = False):
|
||||||
|
dbs = {}
|
||||||
|
if os.path.isfile(os.path.expanduser('~/.my.cnf')):
|
||||||
|
_cfg = configparser.ConfigParser(allow_no_value = True)
|
||||||
|
_cfg.read(os.path.expanduser('~/.my.cnf'))
|
||||||
|
_cfg = dict(_cfg['client'])
|
||||||
|
_cfg['ssl'] = {}
|
||||||
|
if 'host' not in _cfg:
|
||||||
|
_cfg['host'] = 'localhost'
|
||||||
|
conn = pymysql.connect(**_cfg, cursorclass = pymysql.cursors.DictCursor)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Need mysql creds at ~/.my.cnf')
|
||||||
|
cur = conn.cursor()
|
||||||
|
if not db:
|
||||||
|
cur.execute("SHOW DATABASES")
|
||||||
|
db = [row['Database'] for row in cur.fetchall()]
|
||||||
|
if not internal:
|
||||||
|
for d in mysql_internal:
|
||||||
|
try:
|
||||||
|
db.remove(d)
|
||||||
|
except ValueError: # Not in the list; our user probably doesn't have access
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
db = [db]
|
||||||
|
for d in db:
|
||||||
|
dbs[d] = {}
|
||||||
|
cur.execute("SHOW TABLES FROM `{0}`".format(d))
|
||||||
|
for tbl in [t['Tables_in_{0}'.format(d)] for t in cur.fetchall()]:
|
||||||
|
dbs[d][tbl] = {}
|
||||||
|
# Status
|
||||||
|
cur.execute("SHOW TABLE STATUS FROM `{0}` WHERE Name = %s".format(d), (tbl, ))
|
||||||
|
dbs[d][tbl]['_STATUS'] = copy.deepcopy(cur.fetchone())
|
||||||
|
# Columns
|
||||||
|
dbs[d][tbl]['_COLUMNS'] = {}
|
||||||
|
#cur.execute("DESCRIBE {0}.{1}".format(d, tbl))
|
||||||
|
cur.execute("SHOW COLUMNS IN `{0}` FROM `{1}`".format(tbl, d))
|
||||||
|
for row in cur.fetchall():
|
||||||
|
colNm = row['Field']
|
||||||
|
dbs[d][tbl]['_COLUMNS'][colNm] = {}
|
||||||
|
for k in [x for x in tblinfo_hdrs if x is not 'Field']:
|
||||||
|
dbs[d][tbl]['_COLUMNS'][colNm][k] = row[k]
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return(dbs)
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
args.add_argument('-i', '--internal',
|
||||||
|
dest = 'internal',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, include the MySQL internal databases '
|
||||||
|
'(mysql, information_schema, etc.); only used if -d is not specified'))
|
||||||
|
args.add_argument('-d', '--database',
|
||||||
|
dest = 'db',
|
||||||
|
default = None,
|
||||||
|
help = 'If specified, only list table info for this DB')
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
dbs = get_info(args['db'], internal = args['internal'])
|
||||||
|
#import json
|
||||||
|
#print(json.dumps(dbs, indent = 4, sort_keys = True, default = str))
|
||||||
|
import pprint
|
||||||
|
pprint.pprint(dbs)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
1
net/addr/TODO
Normal file
1
net/addr/TODO
Normal file
@ -0,0 +1 @@
|
|||||||
|
We can get more in-depth: https://danidee10.github.io/2016/09/24/flask-by-example-3.html
|
7
net/addr/app/__init__.py
Normal file
7
net/addr/app/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
app = Flask(__name__, instance_relative_config=True)
|
||||||
|
|
||||||
|
from app import views
|
||||||
|
|
||||||
|
app.config.from_object('config')
|
49
net/addr/app/dnsinfo.py
Normal file
49
net/addr/app/dnsinfo.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# https://gist.github.com/akshaybabloo/2a1df455e7643926739e934e910cbf2e
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
import dns # apacman -S python-dnspython
|
||||||
|
import ipwhois # apacman -S python-ipwhois
|
||||||
|
import whois # apacman -S python-ipwhois
|
||||||
|
|
||||||
|
class netTarget(object):
|
||||||
|
def __init__(self, target):
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
|
||||||
|
##!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
#import pprint
|
||||||
|
#import dns
|
||||||
|
#import whois
|
||||||
|
#import ipwhois
|
||||||
|
#
|
||||||
|
#d = 'sysadministrivia.com' # A/AAAA
|
||||||
|
#d = 'autoconfig.sysadministrivia.com' # CNAME
|
||||||
|
#
|
||||||
|
#records = {'whois': None,
|
||||||
|
# 'ptr': None,
|
||||||
|
# 'allocation': None}
|
||||||
|
#
|
||||||
|
#def getWhois(domain):
|
||||||
|
# _w = whois.whois(d)
|
||||||
|
# records['whois'] = dict(_w)
|
||||||
|
# return()
|
||||||
|
#
|
||||||
|
#def getIps(domain):
|
||||||
|
# addrs = []
|
||||||
|
# for t in ('A', 'AAAA'):
|
||||||
|
# answers = dns.resolver.query(domain, t)
|
||||||
|
# for a in answers:
|
||||||
|
# try:
|
||||||
|
# addrs.append(a.address)
|
||||||
|
# except:
|
||||||
|
# pass
|
||||||
|
# return(addrs)
|
||||||
|
#
|
||||||
|
#def getPtr(addrs):
|
||||||
|
# for a in addrs:
|
||||||
|
# pass
|
||||||
|
#
|
||||||
|
#print(getIps(d))
|
||||||
|
##pprint.pprint()
|
0
net/addr/app/models.py
Normal file
0
net/addr/app/models.py
Normal file
8
net/addr/app/templates/about.html
Normal file
8
net/addr/app/templates/about.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% extends "base.html" %}{% block title %}r00t^2 Client Info Revealer || About{% endblock %}{% block body %}<div class="jumbotron">
|
||||||
|
<h1>About</h1></div>
|
||||||
|
<p>This is a tool to reveal certain information about your connection that the server sees. Note that all of this information you see is <i>sent by your client</i>; there was no probing/scanning or the like done from the server this site is hosted on.</p>
|
||||||
|
<p>If you don't like this info being available to server administrators of the websites you visit you may want to consider <a href="https://getfoxyproxy.org/">hiding your client IP address</a><sup><a href="#0">0</a></sup> and/or <a href="https://panopticlick.eff.org/self-defense">hiding your browser's metadata</a>, which can be done via browser plugins such as <a href="https://www.eff.org/privacybadger">Privacy Badger</a>, {{ '<a href="https://addons.mozilla.org/en-US/firefox/addon/modify-headers/">Modify Headers</a>, '|safe if request.user_agent.browser == 'firefox' else '' }}<a href="https://www.requestly.in/">Requestly</a>, and others.</p>
|
||||||
|
<p>If you would like to view the <i>server</i> headers, then you can use a service such as <a href="https://securityheaders.io">SecurityHeaders.io</a> (or use the <b><code>curl -i</code></b> command in *Nix operating systems).</p>
|
||||||
|
<br />
|
||||||
|
<p><a name="0"></a><b>[0]</b> Disclosure: I am an engineer for this company.</p>
|
||||||
|
{% endblock %}
|
35
net/addr/app/templates/base.html
Normal file
35
net/addr/app/templates/base.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<!-- Thanks, https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xii-facelift and
|
||||||
|
https://scotch.io/tutorials/getting-started-with-flask-a-python-microframework -->
|
||||||
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!--<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">-->
|
||||||
|
<!-- Custom styles for this template -->
|
||||||
|
<link href="https://getbootstrap.com/examples/jumbotron-narrow/jumbotron-narrow.css" rel="stylesheet">
|
||||||
|
<!--<link href="https://getbootstrap.com/docs/4.0/examples/offcanvas/offcanvas.css" rel="stylesheet">-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header clearfix">
|
||||||
|
<nav>
|
||||||
|
<ul class="nav nav-pills pull-right">
|
||||||
|
<li role="presentation"><a href="/">Home</a></li>
|
||||||
|
<li role="presentation"><a href="/about">About</a></li>
|
||||||
|
<li role="presentation"><a href="/usage">Usage</a></li>
|
||||||
|
<!-- the following opens in a new tab/window/whatever. the line after opens in the same tab/window/etc. -->
|
||||||
|
<!-- <li role="presentation"><a href="https://square-r00t.net/" target="_blank">r00t^2</a></li> -->
|
||||||
|
<li role="presentation"><a href="https://square-r00t.net/">r00t^2</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
<footer class="footer">
|
||||||
|
<p><sub>The code for this page is released under the <a href="https://www.gnu.org/licenses/gpl-3.0.en.html#content">GPL 3.0 License</a>. It can be found <a href="https://git.square-r00t.net/OpTools/tree/net/addr/">here</a>.</sub></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<!-- /container -->
|
||||||
|
</body>
|
||||||
|
</html>
|
38
net/addr/app/templates/html.html
Normal file
38
net/addr/app/templates/html.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<h2>Client/Browser Information</h2>
|
||||||
|
<p>This is information that your browser sends with its connection.</p>
|
||||||
|
<p>
|
||||||
|
<ul>
|
||||||
|
<li><b>Client IP:</b> <a href="https://ipinfo.io/{{ visitor['ip'] }}">{{ visitor['ip'] }}</a></li>
|
||||||
|
<li><b>Browser:</b> {{ '<a href="{0}">{1}</a>'.format(browsers[visitor['client']['browser']][0],
|
||||||
|
browsers[visitor['client']['browser']][1])|safe
|
||||||
|
if visitor['client']['browser'] in browsers.keys()
|
||||||
|
else visitor['client']['browser'].title()
|
||||||
|
if visitor['client']['browser'] is not none
|
||||||
|
else '(N/A)' }}</li>
|
||||||
|
<li><b>Language/Locale:</b> {{ visitor['client']['language'] or '(N/A)' }}</li>
|
||||||
|
{%- set alt_os = alts[visitor['client']['os']] if visitor['client']['os'] in alts.keys() else '' %}
|
||||||
|
<li><b>Operating System:</b> {{ '<a href="{0}">{1}</a>{2}'.format(os[visitor['client']['os']][0],
|
||||||
|
os[visitor['client']['os']][1],
|
||||||
|
alt_os)|safe
|
||||||
|
if visitor['client']['os'] in os.keys()
|
||||||
|
else visitor['client']['os'].title()
|
||||||
|
if visitor['client']['os'] is not none
|
||||||
|
else '(N/A)' }}</li>
|
||||||
|
<li><b>User Agent:</b> {{ visitor['client']['str'] }}</li>
|
||||||
|
<li><b>Version:</b> {{ visitor['client']['version'] or '(N/A)' }}</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<h2>Request Headers</h2>
|
||||||
|
<p>These are headers sent along with the request your browser sends for the page's content.</p>
|
||||||
|
<p>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>{% for k in visitor['headers'].keys()|sort(case_sensitive = True) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ k }}</td>
|
||||||
|
<td>{{ visitor['headers'][k] if visitor['headers'][k] != '' else '(N/A)' }}</td>
|
||||||
|
</tr>{% endfor %}
|
||||||
|
</table>
|
||||||
|
</p>
|
6
net/addr/app/templates/index.html
Normal file
6
net/addr/app/templates/index.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% extends "base.html" %}{% block title %}r00t^2 Client Info Revealer{% endblock %}{% block body %}<div class="jumbotron">
|
||||||
|
<h1>Client Info Revealer</h1>
|
||||||
|
<p class="lead">A tool to reveal client-identifying data sent to webservers</p>
|
||||||
|
</div>
|
||||||
|
{% include 'html.html' if not params['json'] else 'json.html' %}
|
||||||
|
{% endblock %}
|
1
net/addr/app/templates/json.html
Normal file
1
net/addr/app/templates/json.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<pre>{{ json }}</pre>
|
51
net/addr/app/templates/usage.html
Normal file
51
net/addr/app/templates/usage.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{% extends "base.html" %}{% block title %}r00t^2 Client Info Revealer || Usage{% endblock %}{% block body %}<div class="jumbotron">
|
||||||
|
<h1>Usage</h1></div>
|
||||||
|
<h2>Parameters</h2>
|
||||||
|
<p>You can control how this page displays/renders. By default it will try to "guess" what you want; e.g. if you access it in Chrome, it will display this page but if you fetch via Curl, you'll get raw JSON. The following parameters control this behavior.</p>
|
||||||
|
<p><i><b>Note:</b> "Enabled" parameter values can be one of <b>y</b>, <b>yes</b>, <b>1</b>, or <b>true</b>. "Disabled" parameter values can be one of <b>n</b>, <b>no</b>, <b>0</b>, or <b>false</b>. The parameter names are case-sensitive but the values are not.</i></p>
|
||||||
|
<p><ul>
|
||||||
|
<li><b>json:</b> Force rendering in JSON format
|
||||||
|
<ul>
|
||||||
|
<li>It will display it nicely if you're in a browser, otherwise it will return raw/plaintext JSON.</li>
|
||||||
|
<li>Use <b>raw</b> if you want to force raw plaintext JSON output.</li>
|
||||||
|
</ul></li>
|
||||||
|
<li><b>html:</b> Force rendering in HTML
|
||||||
|
<ul>
|
||||||
|
<li>It will render HTML in clients that would normally render as JSON (e.g. curl, wget).</li>
|
||||||
|
</ul></li>
|
||||||
|
<li><b>raw:</b> Force output into a raw JSON string
|
||||||
|
<ul>
|
||||||
|
<li>Pure JSON instead of HTML or formatted JSON. This is suitable for API usages if your client is detected wrongly (or you just want to get the raw JSON).</li>
|
||||||
|
<li>Overrides all other tags.</li>
|
||||||
|
<li>Has no effect for clients that would normally render as JSON (curl, wget, etc.).</li>
|
||||||
|
</ul></li>
|
||||||
|
<li><b>tabs:</b> Indentation for JSON output
|
||||||
|
<ul>
|
||||||
|
<li>Accepts a positive integer.</li>
|
||||||
|
<li>Default is 4 for "desktop" browsers (if <b>json</b> is enabled), and no indentation otherwise.</li>
|
||||||
|
</ul></li>
|
||||||
|
</ul></p>
|
||||||
|
<h2>Examples</h2>{% set scheme = 'https' if request.is_secure else 'http'%}
|
||||||
|
<p><table>
|
||||||
|
<tr>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Behavior</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ scheme }}://{{ request.headers['host'] }}/">{{ scheme }}://{{ request.headers['host'] }}/</a></td>
|
||||||
|
<td>Displays HTML and "Human" formatting if in a graphical browser, otherwise returns a raw, unformatted JSON string.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ scheme }}://{{ request.headers['host'] }}/?raw=1">{{ scheme }}://{{ request.headers['host'] }}/?raw=1</a></td>
|
||||||
|
<td>Renders a raw, unformatted JSON string if in a graphical browser, otherwise no effect. All other parameters ignored (if in a graphical browser).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ scheme }}://{{ request.headers['host'] }}/?html=1">{{ scheme }}://{{ request.headers['host'] }}/?html=1</a></td>
|
||||||
|
<td>Forces HTML rendering on non-graphical clients.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ scheme }}://{{ request.headers['host'] }}/?json=1&tabs=4">{{ scheme }}://{{ request.headers['host'] }}/?json=1&tabs=4</a></td>
|
||||||
|
<td>Returns JSON indented by 4 spaces for each level (you can leave "json=1" off if it's in a non-graphical browser, unless you specified "html=1").</td>
|
||||||
|
</tr>
|
||||||
|
</table></p>
|
||||||
|
{% endblock %}
|
101
net/addr/app/views.py
Normal file
101
net/addr/app/views.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
from flask import render_template, make_response, request
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
@app.route('/', methods = ['GET']) #@app.route('/')
|
||||||
|
def index():
|
||||||
|
# First we define interactive browsers
|
||||||
|
_intbrowsers = {'camino': ['http://caminobrowser.org/', 'Camino'],
|
||||||
|
'chrome': ['https://www.google.com/chrome/', 'Google Chrome'],
|
||||||
|
'edge': ['https://www.microsoft.com/en-us/windows/microsoft-edge',
|
||||||
|
'Microsoft Edge'],
|
||||||
|
'firefox': ['https://www.mozilla.org/firefox/', 'Mozilla Firefox'],
|
||||||
|
'galeon': ['http://galeon.sourceforge.net/', 'Galeon'],
|
||||||
|
'kmeleon': ['http://kmeleonbrowser.org/', 'K-Meleon'],
|
||||||
|
'konqueror': ['https://konqueror.org/', 'Konqueror'],
|
||||||
|
'links': ['http://links.twibright.com/', 'Links'],
|
||||||
|
'msie': ['https://en.wikipedia.org/wiki/Internet_Explorer',
|
||||||
|
'Microsoft Internet Explorer'],
|
||||||
|
'lynx': ['http://lynx.browser.org/', 'Lynx'],
|
||||||
|
'safari': ['https://www.apple.com/safari/', 'Apple Safari']}
|
||||||
|
_os = {'aix': ['https://www.ibm.com/power/operating-systems/aix', 'AIX'],
|
||||||
|
'amiga': ['http://www.amiga.org/', 'Amiga'],
|
||||||
|
'android': ['https://www.android.com/', 'Android'],
|
||||||
|
'bsd': ['http://www.bsd.org/', 'BSD'],
|
||||||
|
'chromec': ['https://www.chromium.org/chromium-os', 'ChromeOS'],
|
||||||
|
'hpux': ['https://www.hpe.com/us/en/servers/hp-ux.html', 'HP-UX'],
|
||||||
|
'iphone': ['https://www.apple.com/iphone/', 'iPhone'],
|
||||||
|
'ipad': ['https://www.apple.com/ipad/', 'iPad'],
|
||||||
|
'irix': ['https://www.sgi.com/', 'IRIX'],
|
||||||
|
'linux': ['https://www.kernel.org/', 'GNU/Linux'],
|
||||||
|
'macos': ['https://www.apple.com/macos/', 'macOS'],
|
||||||
|
'sco': ['http://www.sco.com/products/unix/', 'SCO'],
|
||||||
|
'solaris': ['https://www.oracle.com/solaris/', 'Solaris'],
|
||||||
|
'wii': ['http://wii.com/', 'Wii'],
|
||||||
|
'windows': ['https://www.microsoft.com/windows/', 'Windows']}
|
||||||
|
_alts = {'amiga': ' (have you tried <a href="http://aros.sourceforge.net/">AROS</a> yet?)',
|
||||||
|
'android': ' (have you tried <a href="https://lineageos.org/">LineageOS</a> yet?)',
|
||||||
|
'macos': ' (have you tried <a href="https://elementary.io/">ElementaryOS</a> yet?)',
|
||||||
|
'sgi': ' (have you tried <a href="http://www.maxxinteractive.com">MaXX</a> yet?)',
|
||||||
|
'windows': ' (have you tried <a href="https://https://reactos.org/">ReactOS</a> yet?)'}
|
||||||
|
# And then we set some parameter options for less typing later on.
|
||||||
|
_yes = ('y', 'yes', 'true', '1', True)
|
||||||
|
_no = ('y', 'no', 'false', '0', False, 'none')
|
||||||
|
# http://werkzeug.pocoo.org/docs/0.12/utils/#module-werkzeug.useragents
|
||||||
|
visitor = {'client': {'str': request.user_agent.string,
|
||||||
|
'browser': request.user_agent.browser,
|
||||||
|
'os': request.user_agent.platform,
|
||||||
|
'language': request.user_agent.language,
|
||||||
|
'to_header': request.user_agent.to_header(),
|
||||||
|
'version': request.user_agent.version},
|
||||||
|
'ip': re.sub('^::ffff:', '', request.remote_addr),
|
||||||
|
'headers': dict(request.headers)}
|
||||||
|
# We have to convert these to strings so we can do tuple comparisons on lower()s.
|
||||||
|
params = {'json': str(request.args.get('json')).lower(),
|
||||||
|
'html': str(request.args.get('html')).lower(),
|
||||||
|
'raw': str(request.args.get('raw')).lower()}
|
||||||
|
if visitor['client']['browser'] in _intbrowsers.keys():
|
||||||
|
if params['html'] == 'none':
|
||||||
|
params['html'] = True
|
||||||
|
if params['json'] == 'none':
|
||||||
|
params['json'] = False
|
||||||
|
elif params['json'] in _yes:
|
||||||
|
params['json'] = True
|
||||||
|
for k in params.keys():
|
||||||
|
if params[k] in _no:
|
||||||
|
params[k] = False
|
||||||
|
else:
|
||||||
|
params[k] = True
|
||||||
|
# Set the tabs for JSON
|
||||||
|
try:
|
||||||
|
params['tabs'] = int(request.args.get('tabs'))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
if visitor['client']['browser'] in _intbrowsers.keys() or params['html']:
|
||||||
|
params['tabs'] = 4
|
||||||
|
else:
|
||||||
|
params['tabs'] = None
|
||||||
|
j = json.dumps(visitor, indent = params['tabs'])
|
||||||
|
if (visitor['client']['browser'] in _intbrowsers.keys() and params['html'] and not params['raw']) or \
|
||||||
|
(visitor['client']['browser'] not in _intbrowsers.keys() and params['html']):
|
||||||
|
return(render_template('index.html',
|
||||||
|
visitor = visitor,
|
||||||
|
browsers = _intbrowsers,
|
||||||
|
os = _os,
|
||||||
|
alts = _alts,
|
||||||
|
json = j,
|
||||||
|
params = params))
|
||||||
|
else:
|
||||||
|
if visitor['client']['browser'] in _intbrowsers.keys() and not params['raw']:
|
||||||
|
return(render_template('json.html',
|
||||||
|
json = j,
|
||||||
|
params = params))
|
||||||
|
return(j)
|
||||||
|
|
||||||
|
@app.route('/about', methods = ['GET'])
|
||||||
|
def about():
|
||||||
|
return(render_template('about.html'))
|
||||||
|
|
||||||
|
@app.route('/usage', methods = ['GET'])
|
||||||
|
def usage():
|
||||||
|
return(render_template('usage.html'))
|
5
net/addr/config.py
Normal file
5
net/addr/config.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# config.py
|
||||||
|
|
||||||
|
# Flask debugging - DISABLE FOR PRODUCTION ENVIRONMENTS
|
||||||
|
DEBUG = True
|
||||||
|
#DEBUG = False
|
4
net/addr/run.py
Normal file
4
net/addr/run.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from app import app
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
18
net/addr/uwsgi.ini
Normal file
18
net/addr/uwsgi.ini
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[uwsgi]
|
||||||
|
plugin = python
|
||||||
|
py-autoreload = 1
|
||||||
|
#uid = http
|
||||||
|
#gid = http
|
||||||
|
socket = /run/uwsgi/netinfo.sock
|
||||||
|
chown-socket = http:http
|
||||||
|
processes = 4
|
||||||
|
master = 1
|
||||||
|
base = /usr/local/lib/optools/net/addr
|
||||||
|
chdir = %(base)
|
||||||
|
#mount = /=%(base)/run.py
|
||||||
|
wsgi-file = %(base)/run.py
|
||||||
|
chmod-socket = 660
|
||||||
|
callable = app
|
||||||
|
cgi-helper =.py=python
|
||||||
|
logto = /var/log/uwsgi/%n.log
|
||||||
|
vacuum
|
24
net/bofh_gen.py
Executable file
24
net/bofh_gen.py
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import telnetlib
|
||||||
|
import time
|
||||||
|
|
||||||
|
counter = 8
|
||||||
|
|
||||||
|
def get_excuse():
|
||||||
|
# http://www.blinkenlights.nl/services.html
|
||||||
|
# port 23 (default) is Star Wars.
|
||||||
|
# port 666 is BOfH excuses
|
||||||
|
with telnetlib.Telnet('towel.blinkenlights.nl', port = 666) as t:
|
||||||
|
excuse = [x.decode('utf-8').strip() \
|
||||||
|
for x in t.read_all().split(b'===\r\n')]
|
||||||
|
return(excuse[2])
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for i in range(counter):
|
||||||
|
e = get_excuse()
|
||||||
|
print(e)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
2
net/connchk.py
Normal file
2
net/connchk.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
1
net/devices/actiontec/ActionTec/Cmd.py
Executable file
1
net/devices/actiontec/ActionTec/Cmd.py
Executable file
@ -0,0 +1 @@
|
|||||||
|
#!/usr/bin/env python3.6
|
66
net/devices/actiontec/ActionTec/Connector.py
Executable file
66
net/devices/actiontec/ActionTec/Connector.py
Executable file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3.6
|
||||||
|
|
||||||
|
# stdlib
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
|
||||||
|
def CheckConnection(host, port):
|
||||||
|
# We favor socket over telnetlib's check because it has a little better
|
||||||
|
# handling of exceptions.
|
||||||
|
try:
|
||||||
|
port = int(port) # just in case we were passed a str()
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('"{0}" is not a port number'.format(port))
|
||||||
|
# In case they're catching the exception...
|
||||||
|
return(False)
|
||||||
|
s = socket.socket()
|
||||||
|
try:
|
||||||
|
s.connect((host, port))
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(('We were unable to successfully connect to ' +
|
||||||
|
'"{0}:{1}": {2}').format(host, port, e))
|
||||||
|
return(False)
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
return(True)
|
||||||
|
|
||||||
|
def Login(host, port, ssl, user, password):
|
||||||
|
user_prompt = [re.compile('^\s*user(name)?\s*:?\s*'.encode('utf-8'),
|
||||||
|
re.IGNORECASE)]
|
||||||
|
passwd_prompt = [re.compile('^\s*passw(or)d?\s*:?\s*'.encode('utf-8'),
|
||||||
|
re.IGNORECASE)]
|
||||||
|
# Are there any other valid chars? Will need to experiment.
|
||||||
|
# How is this even set? The default is "Wireless Broadband Router".
|
||||||
|
# I think it can't be changed, at least via the Web GUI.
|
||||||
|
cmd_prompt = [re.compile('[-_a-z0-9\s]*>'.encode('utf-8'),
|
||||||
|
re.IGNORECASE)]
|
||||||
|
ctx = None
|
||||||
|
ctxargs = {'host': host, 'port': port}
|
||||||
|
try:
|
||||||
|
if ssl:
|
||||||
|
try:
|
||||||
|
from ssltelnet import SslTelnet as telnet
|
||||||
|
ctxargs['force_ssl'] = True
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(('You have enabled SSL but do not have ' +
|
||||||
|
'the ssltelnet module installed. See ' +
|
||||||
|
'the README file, footnote [1].'))
|
||||||
|
else:
|
||||||
|
from telnetlib import Telnet as telnet
|
||||||
|
ctx = telnet(**ctxargs)
|
||||||
|
ctx.expect(user_prompt, timeout = 8)
|
||||||
|
ctx.write((user + '\n').encode('utf-8'))
|
||||||
|
ctx.expect(passwd_prompt, timeout = 8)
|
||||||
|
ctx.write((password + '\n').encode('utf-8'))
|
||||||
|
ctx.expect(cmd_prompt, timeout = 15)
|
||||||
|
except EOFError:
|
||||||
|
if ctx:
|
||||||
|
ctx.close()
|
||||||
|
ctx = None
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(('We encountered an error when trying to connect:' +
|
||||||
|
' {0}').format(e))
|
||||||
|
if ctx:
|
||||||
|
ctx.close()
|
||||||
|
ctx = None
|
||||||
|
return(ctx)
|
33
net/devices/actiontec/ActionTec/__init__.py
Executable file
33
net/devices/actiontec/ActionTec/__init__.py
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3.6
|
||||||
|
|
||||||
|
import .Cmd as Cmd
|
||||||
|
import .Connector as Connector
|
||||||
|
|
||||||
|
class Router(object):
|
||||||
|
def __init__(self, host, port, user, password, ssl = False):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
self.ssl = ssl
|
||||||
|
self.ctx = None
|
||||||
|
# Convenient shorthand. See "help.all.txt".
|
||||||
|
self.cmds = {'reboot': 'system reboot',
|
||||||
|
'wipe': 'conf factory restore',
|
||||||
|
# this will... require an interactive session
|
||||||
|
'shell': 'system shell'}
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
# We don't need to define an except, really.
|
||||||
|
# The function handles that for us.
|
||||||
|
Connector.CheckConnection(self.host, self.port)
|
||||||
|
self.ctx = Connector.Login(self.host, self.port, self.ssl, self.user,
|
||||||
|
self.password)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.ctx:
|
||||||
|
self.ctx.close()
|
177
net/devices/actiontec/ActionTec/help.all.txt
Normal file
177
net/devices/actiontec/ActionTec/help.all.txt
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
Command Category wanmonitor - wanmonitor commands for rtd, do not use it directly
|
||||||
|
get notify wanmonitor to send wan type to rtd
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category conf - Read and write Wireless Broadband Router configuration data
|
||||||
|
factory Factory related commands
|
||||||
|
print Print Wireless Broadband Router configuration
|
||||||
|
set Set Wireless Broadband Router configuration path to value
|
||||||
|
set_obscure Set Wireless Broadband Router configuration path to an
|
||||||
|
obscured value
|
||||||
|
del Delete subtree from Wireless Broadband Router configuration
|
||||||
|
ram_set Set Wireless Broadband Router dynamic configuration
|
||||||
|
ram_print Print Wireless Broadband Router dynamic configuration
|
||||||
|
reconf Reconfigure the system according to the current Wireless
|
||||||
|
Broadband Router configuration
|
||||||
|
firmware_restore Restore to saved firmware and reboot.
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category upnp - UPnP commands
|
||||||
|
igd IGD commands
|
||||||
|
status Display UPnP status
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category qos - Control and display QoS data
|
||||||
|
utilization Connection utilization information
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category wmm - wmm configuration and control
|
||||||
|
get get the specified entry
|
||||||
|
set set the specified entry
|
||||||
|
del del the specified entry
|
||||||
|
get_dev get the entries of the specified device
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category cwmp - CWMP related commands
|
||||||
|
status Print CWMP status
|
||||||
|
session_start Start CWMP session to ACS
|
||||||
|
session_stop Stop CWMP session
|
||||||
|
indexes Print CWMP devices indexes
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category bridge - API for managing ethernet bridge
|
||||||
|
connection connect separate network interfaces to form one seamless LAN
|
||||||
|
config Configure bridge
|
||||||
|
info Print bridge information
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category firewall - Control and display Firewall and NAT data
|
||||||
|
restart Stop and start Firewall & NAT
|
||||||
|
start Start Firewall & NAT
|
||||||
|
stop Stop Firewall & NAT
|
||||||
|
filter Turn Firewall packet inspection on/off
|
||||||
|
mac_cache_dump Dump MAC cache data
|
||||||
|
dump Display Firewall data
|
||||||
|
variable Display variables of the firewall rules
|
||||||
|
trace Trace packet traversal via the Firewall ruleset
|
||||||
|
fastpath Turns firewall fastpath feature on/off (default is on)
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category connection - API for managing connections
|
||||||
|
pppoe Configure pppoe interface
|
||||||
|
vlan Configure vlan interface
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category inet_connection - API for managing internet connections
|
||||||
|
pppoe Configure pppoe internet connection
|
||||||
|
ether Configure ethernet internet connection
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category misc - API for Wireless Broadband Router miscellaneous tasks
|
||||||
|
print_ram print ram consumption for each process
|
||||||
|
vlan_add Add VLAN interface
|
||||||
|
top Profiling over event loop and estream
|
||||||
|
wbm_debug_set Stop and start WBM debug mode
|
||||||
|
wbm_border_set Stop and start WBM border mode
|
||||||
|
knet_hooks_dump Dump to console which knet_hooks run on each device
|
||||||
|
malloc_info Print memory information of malloc module
|
||||||
|
malloc_trim Free unused allocated memory in malloc module
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category firmware_update - Firmware update commands
|
||||||
|
start Remotely upgrade Wireless Broadband Router
|
||||||
|
cancel Kill running remote upgrade
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category log - Controls Wireless Broadband Router logging behavior
|
||||||
|
filter Controls the CLI session logging behavior
|
||||||
|
print Print the contents of a given syslog buffer to the console
|
||||||
|
clear Clear the contents of a given syslog buffer
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category dev - Device related commands
|
||||||
|
mv88e60xx Marvell MV88e60xx Ethernet Switch commands
|
||||||
|
moca MOCA commands
|
||||||
|
mii_reg_get Get Ethernet MII register value
|
||||||
|
mii_reg_set Set Ethernet MII register value
|
||||||
|
mii_phy_reg_get Get Ethernet MII register value
|
||||||
|
mii_phy_reg_set Set Ethernet MII register value
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category kernel - Kernel related commands
|
||||||
|
sys_ioctl issue openrg ioctl
|
||||||
|
meminfo Print memory information
|
||||||
|
top Print Wireless Broadband Router's processes memory usage
|
||||||
|
cpu_load_on Periodically shows cpu usage.
|
||||||
|
cpu_load_off Stop showing cpu usage (triggered by cpu_load_on).
|
||||||
|
cpu_load_avg Shows average cpu usage of last 1, 5 and 15 minutes.
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category system - Commands to control Wireless Broadband Router execution
|
||||||
|
http_intercept_status Display HTTP intercept status
|
||||||
|
diag_test run diagtest 0=all or select 1-9 for TBHR
|
||||||
|
..TLANIPSTB
|
||||||
|
diag_correction run corrections -- may reboot or reset BHR
|
||||||
|
die Exit from Wireless Broadband Router and return ret
|
||||||
|
ps Print Wireless Broadband Router's tasks
|
||||||
|
entity_close Close an entity
|
||||||
|
etask_list_dump Dump back trace of all etasks
|
||||||
|
restore_factory_settings Restore factory configuration
|
||||||
|
reboot Reboot the system
|
||||||
|
ver Display version information
|
||||||
|
print_config Print compilation configuration. Search for option
|
||||||
|
if specified
|
||||||
|
exec Execute program
|
||||||
|
cat Print file contents to console
|
||||||
|
shell Spawn busybox shell in foreground
|
||||||
|
date Print the current UTC and local time
|
||||||
|
print_page Print page id and name
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category flash - Flash and loader related commands
|
||||||
|
commit Save Wireless Broadband Router configuration to flash
|
||||||
|
erase Erase a given section in the flash
|
||||||
|
load Load and burn image
|
||||||
|
boot Boot the system
|
||||||
|
bset Configure bootloader
|
||||||
|
layout Print the flash layout and content
|
||||||
|
dump Dump the flash content
|
||||||
|
lock Lock mtd region
|
||||||
|
unlock Unlock mtd region
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category net - Network related commands
|
||||||
|
dns_route Dyncamic Routing according to DNS replies
|
||||||
|
igmp IGMP Proxy related commands
|
||||||
|
host Resolve host by name
|
||||||
|
protected_setup Network related commands
|
||||||
|
wsc wps related commands
|
||||||
|
ifconfig Configure network interface
|
||||||
|
ping Test network connectivity
|
||||||
|
rg_ifconfig List Wireless Broadband Router Network Devices
|
||||||
|
route Print route table
|
||||||
|
main_wan Print the name of the current main wan device
|
||||||
|
intercept_state Print interception state
|
||||||
|
exit Exit sub menu
|
||||||
|
help Show help for commands within this menu
|
||||||
|
|
||||||
|
Command Category cmd - Commands related to the Command module
|
||||||
|
exit Exit from the current CLI session
|
||||||
|
help Show help for commands within this menu
|
122
net/devices/actiontec/README
Normal file
122
net/devices/actiontec/README
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
This has been confirmed to work for, at the very least, my own Verizon Fi-OS
|
||||||
|
Actiontec MI424WR-GEN3I on firmware 40.21.24. It might work on other models as
|
||||||
|
well, but this hasn't been tested.
|
||||||
|
|
||||||
|
No non-stdlib modules are required.
|
||||||
|
|
||||||
|
Place your routers credentials in ~/.config/optools/actiontec_mgmt.json
|
||||||
|
in the following format:
|
||||||
|
(pay close attention to the quoting)
|
||||||
|
(minified json is OK/whitespace-insensitive):
|
||||||
|
_______________________________________________________________________________
|
||||||
|
{
|
||||||
|
"ip_addr": "192.168.1.1",
|
||||||
|
"user": "admin",
|
||||||
|
"password": "admin",
|
||||||
|
"ssl": false,
|
||||||
|
"port": 23
|
||||||
|
}
|
||||||
|
_______________________________________________________________________________
|
||||||
|
|
||||||
|
IF:
|
||||||
|
|
||||||
|
- That file isn't found:
|
||||||
|
-- A default (blank) one will be created (with secure permissions). All values
|
||||||
|
will be null (see below).
|
||||||
|
|
||||||
|
- "ip_addr" is null:
|
||||||
|
-- You will be prompted for the IP address interactively. (If you don't know
|
||||||
|
the IP address of it, it's probably the default -- "192.168.1.1".)
|
||||||
|
|
||||||
|
- "user" is null:
|
||||||
|
-- You will be prompted for the username to log in interactively. (If you don't
|
||||||
|
know the username, it's probably the default -- "admin".)
|
||||||
|
|
||||||
|
- "password" is null:
|
||||||
|
-- You will be prompted for the password. When being prompted, it will NOT echo
|
||||||
|
back (like a sudo prompt).
|
||||||
|
|
||||||
|
- "ssl" is null:
|
||||||
|
-- The default (false) will be used.
|
||||||
|
|
||||||
|
- "port" is null:
|
||||||
|
-- The default port (23) will be used.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
TIPS:
|
||||||
|
|
||||||
|
- You need to ensure that you have the management interface enabled. Log into
|
||||||
|
your Actiontec's web interface, and:
|
||||||
|
1.) "Advanced" button (at the top)
|
||||||
|
2.) "Yes" button
|
||||||
|
3.) a.) Choose "Local administration" if you'll be managing the device within
|
||||||
|
the network it provides.[0]
|
||||||
|
b.) Choose "Remote administration" if you'll be managing the device
|
||||||
|
outside the network it provides (i.e. over the Internet).[0]
|
||||||
|
3.5) The "Telnet" options are what you want, ignore the "Web" settings.
|
||||||
|
4.) Select the protocols/ports you'll be using. SEE FOOTNOTE 0 ([0])!
|
||||||
|
5.) Click the "Apply" button.
|
||||||
|
|
||||||
|
- "ip_addr" can also be a host/DNS name -- just make sure it resolves on your
|
||||||
|
local machine to your Actiontec IP address! The default, at least on mine,
|
||||||
|
was "wireless_broadband_router" (can be changed via Advanced > Yes > System
|
||||||
|
Settings > Wireless Broadband Router's Hostname):
|
||||||
|
|
||||||
|
[bts@cylon ~]$ nslookup wireless_broadband_router 192.168.1.1
|
||||||
|
Server: 192.168.1.1
|
||||||
|
Address: 192.168.1.1#53
|
||||||
|
|
||||||
|
Name: wireless_broadband_router
|
||||||
|
Address: 192.168.1.1
|
||||||
|
Name: wireless_broadband_router
|
||||||
|
Address: <YOUR_PUBLIC_IP_ADDRESS>
|
||||||
|
|
||||||
|
|
||||||
|
- Unfortunately it's a necessity to store the password in plaintext currently.
|
||||||
|
Future versions may give the option of encrypting it via GPG and using an
|
||||||
|
existing GPG agent session to unlock (if there's demand for such a feature).
|
||||||
|
Make sure your machine's files are safe (I recommend full-disk encryption).
|
||||||
|
|
||||||
|
|
||||||
|
[0] NOTE: ENABLING MANAGEMENT CAN BE HIGHLY INSECURE, *ESPECIALLY* IF ENABLING
|
||||||
|
"REMOTE ADMINISTRATION"! *ONLY* DO THIS IF YOU UNDERSTAND THE RISKS
|
||||||
|
AND HAVE ACCOUNTED FOR THEM. TELNET PASSES CREDENTIALS IN PLAINTEXT
|
||||||
|
BY DEFAULT, AND IF SOMEONE NASTY GETS THEIR HANDS ON YOUR DEVICE'S
|
||||||
|
CREDENTIALS THEY CAN DO *VERY* NASTY THINGS. I REFUSE ANY AND ALL
|
||||||
|
LIABILITY YOU OPEN YOURSELF UP TO BY ENABLING THIS. AT *LEAST* USE
|
||||||
|
THE "USING SECURE TELNET OVER SSL PORT"[1] OPTION.
|
||||||
|
YOU HAVE BEEN WARNED.
|
||||||
|
|
||||||
|
[1] NOTE: Even if using SSL, it's HIGHLY insecure and not to be trusted. The
|
||||||
|
key has been leaked (as of 2018-04-12):
|
||||||
|
https://code.google.com/archive/p/littleblackbox/
|
||||||
|
and it uses VERY weak ciphers, at that:
|
||||||
|
_____________________________________________________________________
|
||||||
|
| ssl-cert: Subject: commonName=ORname_Jungo: OpenRG Products Group/|
|
||||||
|
| countryName=US |
|
||||||
|
| Not valid before: 2004-06-03T11:11:43 |
|
||||||
|
|_Not valid after: 2024-05-29T11:11:43 |
|
||||||
|
|_ssl-date: 2018-04-12T09:42:22+00:00; -1s from scanner time. |
|
||||||
|
|_ssl-known-key: Found in Little Black Box 0.1 - |
|
||||||
|
| http://code.google.com/p/littleblackbox/ |
|
||||||
|
| (SHA-1: 4388 33c0 94f6 afc8 64c6 0e4a 6f57 e9f4 d128 1411)|
|
||||||
|
| sslv2: |
|
||||||
|
| SSLv2 supported |
|
||||||
|
| ciphers: |
|
||||||
|
| SSL2_RC4_128_WITH_MD5 |
|
||||||
|
| SSL2_RC4_64_WITH_MD5 |
|
||||||
|
| SSL2_RC2_128_CBC_EXPORT40_WITH_MD5 |
|
||||||
|
| SSL2_RC4_128_EXPORT40_WITH_MD5 |
|
||||||
|
| SSL2_DES_192_EDE3_CBC_WITH_MD5 |
|
||||||
|
| SSL2_RC2_128_CBC_WITH_MD5 |
|
||||||
|
|_ SSL2_DES_64_CBC_WITH_MD5 |
|
||||||
|
|___________________________________________________________________|
|
||||||
|
|
||||||
|
It's generally probably not even worth it, to be honest. You'll get
|
||||||
|
more security mileage out of firewalling off to select hosts/nets.
|
||||||
|
But, if you insist on having it and using it, you will ALSO need to
|
||||||
|
install the following module:
|
||||||
|
|
||||||
|
ssltelnet
|
||||||
|
https://pypi.python.org/pypi/ssltelnet
|
1
net/devices/actiontec/actiontec_mgmt.py
Executable file
1
net/devices/actiontec/actiontec_mgmt.py
Executable file
@ -0,0 +1 @@
|
|||||||
|
#!/usr/bin/env python3.6
|
212
net/dhcp/dhcpcdump.py
Executable file
212
net/dhcp/dhcpcdump.py
Executable file
@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# INCOMPLETE
|
||||||
|
|
||||||
|
# See RFC 2131, Figure 1 and Table 1 (section 2)
|
||||||
|
# Much thanks to https://github.com/igordcard/dhcplease for digging into dhcpcd
|
||||||
|
# source for the actual file structure (and providing inspiration).
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import collections
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
## DEFINE SOME PRETTY STUFF ##
|
||||||
|
class color(object):
|
||||||
|
PURPLE = '\033[95m'
|
||||||
|
CYAN = '\033[96m'
|
||||||
|
DARKCYAN = '\033[36m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
|
END = '\033[0m'
|
||||||
|
|
||||||
|
class packetParser(object):
|
||||||
|
def __init__(self, data):
|
||||||
|
## Set the segment labels and struct formats
|
||||||
|
self.fmt = collections.OrderedDict()
|
||||||
|
# In the below, 'cnt' is how large (in octets) the field is.
|
||||||
|
# 'fmt' is a struct format string (https://docs.python.org/3/library/struct.html#format-characters)
|
||||||
|
# "op" through "hops" (incl.) may actually be '8B' instead of '8c'.
|
||||||
|
self.fmt['op'] = {'cnt': 8, 'fmt': '8c'} # this will always be \x02
|
||||||
|
self.fmt['htype'] = {'cnt': 8, 'fmt': '8c'} # this will always be \x01
|
||||||
|
self.fmt['hlen'] = {'cnt': 8, 'fmt': '8c'}
|
||||||
|
self.fmt['hops'] = {'cnt': 8, 'fmt': '8c'}
|
||||||
|
self.fmt['xid'] = {'cnt': 32, 'fmt': '8I'}
|
||||||
|
self.fmt['secs'] = {'cnt': 16, 'fmt': '8H'}
|
||||||
|
self.fmt['flags'] = {'cnt': 16, 'fmt': '8H'}
|
||||||
|
# "ciaddr" through "giaddr" (incl.) may actually be '4c' instead of '4B'.
|
||||||
|
self.fmt['ciaddr'] = {'cnt': 4, 'fmt': '4B'}
|
||||||
|
self.fmt['yiaddr'] = {'cnt': 4, 'fmt': '4B'}
|
||||||
|
self.fmt['siaddr'] = {'cnt': 4, 'fmt': '4B'}
|
||||||
|
self.fmt['giaddr'] = {'cnt': 4, 'fmt': '4B'}
|
||||||
|
# "chaddr" through "file" (incl.) may actually be <#>c instead of <#>B.
|
||||||
|
self.fmt['chaddr'] = {'cnt': 16, 'fmt': '16B'} # first 6 bytes used for MAC addr of client
|
||||||
|
self.fmt['sname'] = {'cnt': 64, 'fmt': '64B'} # server host name (via BOOTP)
|
||||||
|
self.fmt['file'] = {'cnt': 128, 'fmt': '128B'} # the boot filename (for BOOTP)
|
||||||
|
# OPTIONS - RFC 2132
|
||||||
|
# Starting at octet 320 (so, f.seek(319, 0)) to the end of the message are
|
||||||
|
# DHCP options. It's a variable-length field so it makes things tricky
|
||||||
|
# for us. But it's at *least* 312 octets long per the RFC?
|
||||||
|
# It probably starts with a magic.
|
||||||
|
#self.dhcp_opts = {'idx': 324, 'cnt': 4, 'fmt': '4c'}
|
||||||
|
#self.dhcp_opts = {'idx': 324, 'cnt': 4, 'fmt': None}
|
||||||
|
self.opts = {'magic': b'\x63\x82\x53\x63',
|
||||||
|
'struct': {'idx': 324, 'cnt': 4, 'fmt': '4B'},
|
||||||
|
'size': 0,
|
||||||
|
'bytes': b'\00'}
|
||||||
|
## Convert the data into a bytes object because struct.unpack() wants a stream
|
||||||
|
self.buf = BytesIO(data)
|
||||||
|
|
||||||
|
def getStd(self):
|
||||||
|
self.reconstructed_segments = collections.OrderedDict()
|
||||||
|
_idx = 0 # add to this with the 'cnt' value for each iteration.
|
||||||
|
for k in self.fmt.keys():
|
||||||
|
print('Segment: ' + k) # TODO: remove, this stuff goes in the printer
|
||||||
|
pkt = struct.Struct(self.fmt[k]['fmt'])
|
||||||
|
self.buf.seek(_idx, 0)
|
||||||
|
try:
|
||||||
|
self.reconstructed_segments[k] = pkt.unpack(self.buf.read(self.fmt[k]['cnt']))
|
||||||
|
except struct.error as e:
|
||||||
|
# Some DHCP implementations are... broken.
|
||||||
|
# I've noticed it mostly in Verizon Fi-OS gateways/WAPs/routers.
|
||||||
|
print('Warning({0}): {1}'.format(k, e))
|
||||||
|
self.buf.seek(_idx, 0)
|
||||||
|
_truesize = len(self.buf.read(self.fmt[k]['cnt']))
|
||||||
|
print('Length of bytes read: {0}'.format(_truesize))
|
||||||
|
# But sometimes it's... kind of fixable?
|
||||||
|
if k == 'file' and _truesize < self.fmt[k]['cnt']:
|
||||||
|
self.buf.seek(_idx, 0)
|
||||||
|
self.fmt[k] = {'cnt': _truesize, 'fmt': '{0}B'.format(_truesize)}
|
||||||
|
pkt = struct.Struct(self.fmt[k]['fmt'])
|
||||||
|
print('Struct format size automatically adjusted.')
|
||||||
|
try:
|
||||||
|
self.reconstructed_segments[k] = pkt.unpack(self.buf.read(self.fmt[k]['cnt']))
|
||||||
|
except struct.error as e2:
|
||||||
|
# yolo.
|
||||||
|
print('We still couldn\'t populate {0}; filling with a nullbyte.'.format(k))
|
||||||
|
print('Error (try #2): {0}'.format(e2))
|
||||||
|
print('We read {0} bytes.'.format(_truesize))
|
||||||
|
print('fmt: {0}'.format(self.fmt[k]['fmt']))
|
||||||
|
self.reconstructed_segments[k] = b'\00'
|
||||||
|
_idx += self.fmt[k]['cnt']
|
||||||
|
self.buf.seek(_idx, 0)
|
||||||
|
# Finally, check for opts. If they exist, populate.
|
||||||
|
_optbytes = len(self.buf.read())
|
||||||
|
if _optbytes >= 1:
|
||||||
|
self.opts['size'] = _optbytes
|
||||||
|
self.buf.seek(_idx, 0)
|
||||||
|
self.opts['bytes'] = self.buf.read() # read to the end
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getOpts(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.buf.close()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
_deflease = '/var/lib/dhcpcd/'
|
||||||
|
args.add_argument('-l', '--lease',
|
||||||
|
metavar = '/path/to/lease/dir/or_file.lease',
|
||||||
|
default = _deflease,
|
||||||
|
dest = 'leasepath',
|
||||||
|
help = ('The path to the directory of lease files or specific lease file. ' +
|
||||||
|
'If a directory is provided, all lease files found within will be ' +
|
||||||
|
'parsed. Default: {0}{1}{2}').format(color.BOLD,
|
||||||
|
_deflease,
|
||||||
|
color.END))
|
||||||
|
args.add_argument('-n', '--no-color',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'color',
|
||||||
|
help = ('If specified, suppress color formatting in output.'))
|
||||||
|
args.add_argument('-d', '--dump',
|
||||||
|
metavar = '/path/to/dumpdir',
|
||||||
|
default = False,
|
||||||
|
dest = 'dump',
|
||||||
|
help = ('If provided, dump the parsed leases to this directory (in ' +
|
||||||
|
'addition to printing). It will dump with the same filename ' +
|
||||||
|
'and overwrite any existing file with the same filename, so ' +
|
||||||
|
'do NOT use the same directory as your dhcpcd lease files! ' +
|
||||||
|
'({0}-l/--lease{1}). The directory will be created if it does ' +
|
||||||
|
'not exist').format(color.BOLD,
|
||||||
|
color.END))
|
||||||
|
args.add_argument('-p', '--pretty',
|
||||||
|
action = 'store_true',
|
||||||
|
dest = 'prettyprint',
|
||||||
|
help = ('If specified, include color formatting {0}in the dump ' +
|
||||||
|
'file(s){1}').format(color.BOLD, color.END))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def getLeaseData(fpath):
|
||||||
|
if not os.path.isfile(fpath):
|
||||||
|
raise FileNotFoundError('{0} does not exist'.format(fpath))
|
||||||
|
with open(fpath, 'rb') as f:
|
||||||
|
_data = f.read()
|
||||||
|
return(_data)
|
||||||
|
|
||||||
|
def iterLease(args):
|
||||||
|
# If the lease path is a file, just operate on that.
|
||||||
|
# If it's a directory, iterate (recursively) through it.
|
||||||
|
leases = {}
|
||||||
|
if not os.path.lexists(args['leasepath']):
|
||||||
|
raise FileNotFoundError('{0} does not exist'.format(args['leasepath']))
|
||||||
|
if os.path.isfile(args['leasepath']):
|
||||||
|
_pp = packetParser(getLeaseData(args['leasepath']))
|
||||||
|
# TODO: convert the hex vals to their actual vals... maybe?
|
||||||
|
_keyname = re.sub('^(dhcpcd-)?(.*)\.lease$',
|
||||||
|
'\g<2>',
|
||||||
|
os.path.basename(args['leasepath']))
|
||||||
|
leases[_keyname] = leaseParse(_pp, args)
|
||||||
|
else:
|
||||||
|
# walk() instead of listdir() because whotf knows when some distro like
|
||||||
|
# *coughcoughUbuntucoughcough* will do some breaking change like creating
|
||||||
|
# subdirs based on iface name or something.
|
||||||
|
for _, _, files in os.walk(args['leasepath']):
|
||||||
|
if not files:
|
||||||
|
continue
|
||||||
|
files = [i for i in files if i.endswith('.lease')] # only get .lease files
|
||||||
|
for i in files:
|
||||||
|
_args = args.copy()
|
||||||
|
_fpath = os.path.join(args['leasepath'], i)
|
||||||
|
_keyname = re.sub('^(dhcpcd-)?(.*)\.lease$', '\g<2>', os.path.basename(_fpath))
|
||||||
|
_dupeid = 0
|
||||||
|
# JUST in case there are multiple levels of dirs in the future
|
||||||
|
# that have files of the sama name
|
||||||
|
while _keyname in leases.keys():
|
||||||
|
# TODO: convert the hex vals to their actual vals... maybe?
|
||||||
|
_keyname = re.sub('^$',
|
||||||
|
'\g<1>.{0}'.format(_dupeid),
|
||||||
|
_keyname)
|
||||||
|
_dupeid += 1
|
||||||
|
_pp = packetParser(getLeaseData(_fpath))
|
||||||
|
leases[_keyname] = leaseParse(_pp, _args, fname = _fpath)
|
||||||
|
return(leases)
|
||||||
|
|
||||||
|
def leaseParse(pp, args, fname = False):
|
||||||
|
# Essentially just a wrapper function.
|
||||||
|
# Debugging output...
|
||||||
|
if fname:
|
||||||
|
print(fname)
|
||||||
|
pp.getStd()
|
||||||
|
pp.getOpts()
|
||||||
|
if args['dump']:
|
||||||
|
pass # TODO: write to files, creating dump dir if needed, etc.
|
||||||
|
pp.close()
|
||||||
|
# do pretty-printing (color-coded segments, etc.) here
|
||||||
|
return(pp.reconstructed_segments)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
args['leasepath'] = os.path.abspath(os.path.expanduser(args['leasepath']))
|
||||||
|
if not os.path.lexists(args['leasepath']):
|
||||||
|
exit('{0} does not exist!'.format(args['leasepath']))
|
||||||
|
leases = iterLease(args)
|
||||||
|
# just print for now until we write the parser/prettyprinter
|
||||||
|
print(list(leases.keys()))
|
57
net/dns/linode/README
Normal file
57
net/dns/linode/README
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
This script requires a configuration file (by default, ~/.config/ddns.xml). Please refer to example.ddns.xml for an example.
|
||||||
|
|
||||||
|
The path to the configuration file can be changed with the -c/--config argument.
|
||||||
|
|
||||||
|
!!! NOTE !!!
|
||||||
|
This script as a precautionary measure does NOT create new domain names! It may create or remove A/AAAA records depending
|
||||||
|
on whether your client has a IPv4 and/or IPv6 WAN route respectively, however.
|
||||||
|
|
||||||
|
Because network DNS settings are unpredictable and we need to ensure we don't get split-brain or bogus DNS responses,
|
||||||
|
this script uses Verisign's public DNS resolvers hardcoded in. These resolvers are recommended for privacy, speed, and
|
||||||
|
RFC compliance. The exact resolvers used are:
|
||||||
|
|
||||||
|
* 64.6.64.6
|
||||||
|
* 64.6.65.6
|
||||||
|
|
||||||
|
If you do not consent to this, do not use this script.
|
||||||
|
!!!!!!!!!!!!
|
||||||
|
|
||||||
|
!!! NOTE !!!
|
||||||
|
This script, by *necessity*, connects to (tries to connect to) the following URLs:
|
||||||
|
|
||||||
|
* https://ipv4.clientinfo.square-r00t.net/?raw=1
|
||||||
|
* https://ipv6.clientinfo.square-r00t.net/?raw=1
|
||||||
|
|
||||||
|
This is a necessity because otherwise we do not have a method of fetching the WAN IP if the client is e.g. behind NAT
|
||||||
|
(or is using ULA addresses with a routed gateway/RFC 6296 in IPv6 networks, etc.).
|
||||||
|
|
||||||
|
This is a service that the author himself has written (https://git.square-r00t.net/OpTools/tree/net/addr) and deployed.
|
||||||
|
No personal information is sold, etc. and it only returns the headers and connection information the client sends in a
|
||||||
|
standard HTTP(S) request.
|
||||||
|
|
||||||
|
If you do not consent to this, either change the URL in Updater._getMyIP() (it is compatible with https://ipinfo.io/,
|
||||||
|
but this service does not return split IPv4 and IPv6 records so further modifications would be required) or do not use
|
||||||
|
this script.
|
||||||
|
!!!!!!!!!!!!
|
||||||
|
|
||||||
|
SETUP:
|
||||||
|
|
||||||
|
1.)a.) Create the domain(s) you wish to use in the Linode Domains manager (https://cloud.linode.com/domains).
|
||||||
|
b.) Create the API token (https://cloud.linode.com/profile/tokens).
|
||||||
|
* It MUST have "Read/Write" access to the "Domains" scope. All other scopes can be "None".
|
||||||
|
* It is *HIGHLY recommended* that you generate a *unique* token for each and every client machine rather than
|
||||||
|
sharing a token across them.
|
||||||
|
1.) Create a configuration file. Refer to the accompanying "example.ddns.xml" file.
|
||||||
|
2.) Make sure the script is executable and you have all required python modules installed:
|
||||||
|
https://pypi.org/project/dnspython/
|
||||||
|
https://pypi.org/project/requests/
|
||||||
|
https://pypi.org/project/lxml/
|
||||||
|
https://pypi.org/project/systemd/ (optional; for logging to the journal)
|
||||||
|
3.) You're ready to go! It is recommended that you either:
|
||||||
|
a.) Set up a cronjob (https://crontab.guru/), or
|
||||||
|
b.) Create a systemd timer (https://wiki.archlinux.org/index.php/Systemd/Timers) (if you're on a system with systemd).
|
||||||
|
|
||||||
|
LOGGING:
|
||||||
|
Logging is done to ~/.cache/ddns.log. Messages will also be logged to the systemd journal (if available and the systemd module is installed).
|
||||||
|
|
||||||
|
Suggestions for improvement are welcome (r00t [at] square-r00t.net).
|
299
net/dns/linode/ddns.py
Executable file
299
net/dns/linode/ddns.py
Executable file
@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
##
|
||||||
|
import dns.exception
|
||||||
|
import dns.resolver
|
||||||
|
import requests
|
||||||
|
##
|
||||||
|
from lxml import etree
|
||||||
|
try:
|
||||||
|
# https://www.freedesktop.org/software/systemd/python-systemd/journal.html#journalhandler-class
|
||||||
|
from systemd import journal
|
||||||
|
_has_journald = True
|
||||||
|
except ImportError:
|
||||||
|
_has_journald = False
|
||||||
|
|
||||||
|
|
||||||
|
logfile = '~/.cache/ddns.log'
|
||||||
|
|
||||||
|
# Prep the log file.
|
||||||
|
logfile = os.path.abspath(os.path.expanduser(logfile))
|
||||||
|
os.makedirs(os.path.dirname(logfile), exist_ok = True, mode = 0o0700)
|
||||||
|
if not os.path.isfile(logfile):
|
||||||
|
with open(logfile, 'w') as fh:
|
||||||
|
fh.write('')
|
||||||
|
os.chmod(logfile, 0o0600)
|
||||||
|
|
||||||
|
# And set up logging.
|
||||||
|
_cfg_args = {'handlers': [],
|
||||||
|
'level': logging.DEBUG}
|
||||||
|
if _has_journald:
|
||||||
|
# There were some weird changes somewhere along the line.
|
||||||
|
try:
|
||||||
|
# But it's *probably* this one.
|
||||||
|
h = journal.JournalHandler()
|
||||||
|
except AttributeError:
|
||||||
|
h = journal.JournaldLogHandler()
|
||||||
|
# Systemd includes times, so we don't need to.
|
||||||
|
h.setFormatter(logging.Formatter(style = '{',
|
||||||
|
fmt = ('{name}:{levelname}:{name}:{filename}:'
|
||||||
|
'{funcName}:{lineno}: {message}')))
|
||||||
|
_cfg_args['handlers'].append(h)
|
||||||
|
h = logging.handlers.RotatingFileHandler(logfile,
|
||||||
|
encoding = 'utf8',
|
||||||
|
# Disable rotating for now.
|
||||||
|
# maxBytes = 50000000000,
|
||||||
|
# backupCount = 30
|
||||||
|
)
|
||||||
|
h.setFormatter(logging.Formatter(style = '{',
|
||||||
|
fmt = ('{asctime}:'
|
||||||
|
'{levelname}:{name}:{filename}:'
|
||||||
|
'{funcName}:{lineno}: {message}')))
|
||||||
|
_cfg_args['handlers'].append(h)
|
||||||
|
logging.basicConfig(**_cfg_args)
|
||||||
|
logger = logging.getLogger('DDNS')
|
||||||
|
logger.info('Logging initialized.')
|
||||||
|
|
||||||
|
is_tty = sys.stdin.isatty()
|
||||||
|
if not is_tty:
|
||||||
|
logger.debug('Not running in an interactive invocation; disabling printing warnings')
|
||||||
|
else:
|
||||||
|
logger.debug('Running in an interactive invocation; enabling printing warnings')
|
||||||
|
|
||||||
|
|
||||||
|
class Updater(object):
|
||||||
|
tree = None
|
||||||
|
records = {}
|
||||||
|
api_base = None
|
||||||
|
session = None
|
||||||
|
token = None
|
||||||
|
my_ips = {4: None, 6: None}
|
||||||
|
resolver = dns.resolver.Resolver(configure = False)
|
||||||
|
resolver.nameservers = ['64.6.64.6', '64.6.65.6']
|
||||||
|
|
||||||
|
def __init__(self, cfg_path = '~/.config/ddns.xml', *args, **kwargs):
|
||||||
|
self.xml = os.path.abspath(os.path.expanduser(cfg_path))
|
||||||
|
logger.debug('Updater initialized with config {0}'.format(self.xml))
|
||||||
|
self._getConf()
|
||||||
|
self._getMyIP()
|
||||||
|
self._getSession()
|
||||||
|
|
||||||
|
def _getConf(self):
|
||||||
|
try:
|
||||||
|
with open(self.xml, 'rb') as fh:
|
||||||
|
self.xml = etree.fromstring(fh.read())
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.error('Configuration file does not exist; please create it')
|
||||||
|
raise e
|
||||||
|
self.tree = self.xml.getroottree()
|
||||||
|
self.token = self.xml.attrib['token']
|
||||||
|
self.api_base = re.sub(r'/$', '', self.xml.attrib['base'])
|
||||||
|
dom_xml = self.xml.findall('domain')
|
||||||
|
num_doms = len(dom_xml)
|
||||||
|
logger.debug('Found {0} domains in config'.format(num_doms))
|
||||||
|
for idx, d in enumerate(dom_xml):
|
||||||
|
domain = d.attrib['name']
|
||||||
|
logger.debug('Iterating domain {0} ({1}/{2})'.format(domain, (idx + 1), num_doms))
|
||||||
|
if domain not in self.records.keys():
|
||||||
|
self.records[domain] = []
|
||||||
|
sub_xml = d.findall('sub')
|
||||||
|
num_subs = len(sub_xml)
|
||||||
|
logger.debug('Found {0} records for domain {1}'.format(num_subs, domain))
|
||||||
|
for idx2, s in enumerate(sub_xml):
|
||||||
|
logger.debug('Adding record {0}.{1} to index ({2}/{3})'.format(s.text, domain, (idx2 + 1), num_subs))
|
||||||
|
self.records[domain].append(s.text)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _getDNS(self, record):
|
||||||
|
records = {}
|
||||||
|
for t in ('A', 'AAAA'):
|
||||||
|
logger.debug('Resolving {0} ({1})'.format(record, t))
|
||||||
|
try:
|
||||||
|
q = self.resolver.resolve(record, t)
|
||||||
|
for a in q:
|
||||||
|
if t not in records.keys():
|
||||||
|
records[t] = []
|
||||||
|
ip = a.to_text()
|
||||||
|
logger.debug('Found IP {0} for record {1} ({2})'.format(ip, record, t))
|
||||||
|
records[t].append(ip)
|
||||||
|
except dns.exception.Timeout as e:
|
||||||
|
logger.error('Got a timeout when resolving {0} ({1}): {2}'.format(record, t, e))
|
||||||
|
continue
|
||||||
|
except dns.resolver.NXDOMAIN as e:
|
||||||
|
# This is a debug instead of an error because that record type may not exist.
|
||||||
|
logger.debug('Record {0} ({1}) does not exist: {2}'.format(record, t, e))
|
||||||
|
continue
|
||||||
|
except dns.resolver.YXDOMAIN as e:
|
||||||
|
logger.error('Record {0} ({1}) is too long: {2}'.format(record, t, e))
|
||||||
|
continue
|
||||||
|
except dns.resolver.NoAnswer as e:
|
||||||
|
# This is a debug instead of an error because that record type may not exist.
|
||||||
|
logger.debug('Record {0} ({1}) exists but has no content: {2}'.format(record, t, e))
|
||||||
|
continue
|
||||||
|
except dns.resolver.NoNameservers as e:
|
||||||
|
logger.error(('Could not failover to a non-broken resolver when resolving {0} ({1}): '
|
||||||
|
'{2}').format(record, t, e))
|
||||||
|
continue
|
||||||
|
return(records)
|
||||||
|
|
||||||
|
def _getMyIP(self):
|
||||||
|
for v in self.my_ips.keys():
|
||||||
|
try:
|
||||||
|
logger.debug('Getting the client\'s WAN address for IPv{0}'.format(v))
|
||||||
|
r = requests.get('https://ipv{0}.clientinfo.square-r00t.net/?raw=1'.format(v))
|
||||||
|
if not r.ok:
|
||||||
|
logger.error('Got a non-OK response from WAN IPv{0} fetch.'.format(v))
|
||||||
|
raise RuntimeError('Could not get the IPv{0} address'.format(v))
|
||||||
|
ip = r.json()['ip']
|
||||||
|
logger.debug('Got WAN IP address {0} for IPv{1}'.format(ip, v))
|
||||||
|
self.my_ips[v] = ip
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
logger.debug('Could not get WAN address for IPv{0}; likely not supported on this network'.format(v))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _getSession(self):
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({'Authorization': 'Bearer {0}'.format(self.token)})
|
||||||
|
return()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
for d in self.records.keys():
|
||||||
|
d_f = json.dumps({'domain': d})
|
||||||
|
doms_url = '{0}/domains'.format(self.api_base)
|
||||||
|
logger.debug('Getting list of domains from {0} (filtered to {1})'.format(doms_url, d))
|
||||||
|
d_r = self.session.get(doms_url,
|
||||||
|
headers = {'X-Filter': d_f})
|
||||||
|
if not d_r.ok:
|
||||||
|
e = 'Could not get list of domains when attempting to check {0}; skipping'.format(d)
|
||||||
|
if is_tty:
|
||||||
|
warnings.warn(e)
|
||||||
|
logger.warning(e)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
d_id = d_r.json()['data'][0]['id']
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
e = 'Could not find domain {0} in the returned domains list; skipping'.format(d)
|
||||||
|
if is_tty:
|
||||||
|
warnings.warn(e)
|
||||||
|
logger.warning(e)
|
||||||
|
continue
|
||||||
|
for s in self.records[d]:
|
||||||
|
fqdn = '{0}.{1}'.format(s, d)
|
||||||
|
logger.debug('Processing {0}'.format(fqdn))
|
||||||
|
records = self._getDNS(fqdn)
|
||||||
|
for v, t in ((4, 'A'), (6, 'AAAA')):
|
||||||
|
ip = self.my_ips.get(v)
|
||||||
|
rrset = records.get(t)
|
||||||
|
if not ip:
|
||||||
|
e = 'IPv{0} disabled; skipping'.format(v)
|
||||||
|
if is_tty:
|
||||||
|
warnings.warn(e)
|
||||||
|
logger.warning(e)
|
||||||
|
continue
|
||||||
|
if rrset and ip in rrset:
|
||||||
|
e = 'Skipping adding {0} for {1}; already exists in DNS'.format(ip, fqdn)
|
||||||
|
logger.info(e)
|
||||||
|
if is_tty:
|
||||||
|
print(e)
|
||||||
|
continue
|
||||||
|
s_f = json.dumps({'name': s,
|
||||||
|
'type': t})
|
||||||
|
records_url = '{0}/domains/{1}/records'.format(self.api_base, d_id)
|
||||||
|
logger.debug(('Getting list of records from {0} '
|
||||||
|
'(filtered to name {1} and type {2})').format(records_url, s, t))
|
||||||
|
s_r = self.session.get(records_url,
|
||||||
|
headers = {'X-Filter': s_f})
|
||||||
|
if not s_r.ok:
|
||||||
|
e = 'Could not get list of records when attempting to check {0} ({1}); skipping'.format(fqdn, t)
|
||||||
|
if is_tty:
|
||||||
|
warnings.warn(e)
|
||||||
|
logger.warning(e)
|
||||||
|
continue
|
||||||
|
r_ids = set()
|
||||||
|
# If r_exists is:
|
||||||
|
# None, then the record exists but the current WAN IP is missing (all records replaced).
|
||||||
|
# False, then the record does not exist (record will be added).
|
||||||
|
# True, then the record exists and is current (nothing will be done).
|
||||||
|
r_exists = None
|
||||||
|
try:
|
||||||
|
api_records = s_r.json().pop('data')
|
||||||
|
for idx, r in enumerate(api_records):
|
||||||
|
r_ids.add(r['id'])
|
||||||
|
r_ip = r['target']
|
||||||
|
if r_ip == ip:
|
||||||
|
r_exists = True
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
e = ('Could not find record {0} ({1}) in the returned records list; '
|
||||||
|
'creating new record').format(fqdn, t)
|
||||||
|
if is_tty:
|
||||||
|
print(e)
|
||||||
|
logger.info(e)
|
||||||
|
r_exists = False
|
||||||
|
if r_exists:
|
||||||
|
# Do nothing.
|
||||||
|
e = 'Skipping adding {0} for {1}; already exists in API and is correct'.format(ip, fqdn)
|
||||||
|
logger.info(e)
|
||||||
|
if is_tty:
|
||||||
|
print(e)
|
||||||
|
continue
|
||||||
|
elif r_exists is None:
|
||||||
|
# Remove all records and then add (at the end).
|
||||||
|
# We COULD do an update:
|
||||||
|
# https://developers.linode.com/api/v4/domains-domain-id-records-record-id/#put
|
||||||
|
# BUT then we break future updating since we don't know which record is the "right" one to
|
||||||
|
# update.
|
||||||
|
logger.debug('Record {0} ({1}) exists but does not contain {2}; replacing'.format(fqdn, t, ip))
|
||||||
|
for r_id in r_ids:
|
||||||
|
del_url = '{0}/{1}'.format(records_url, r_id)
|
||||||
|
logger.debug(('Deleting record ID {0} for {1} ({2})').format(r_id, fqdn, t))
|
||||||
|
del_r = self.session.delete(del_url)
|
||||||
|
if not del_r.ok:
|
||||||
|
e = 'Could not delete record ID {0} for {1} ({2}); skipping'.format(r_id, fqdn, t)
|
||||||
|
if is_tty:
|
||||||
|
warnings.warn(e)
|
||||||
|
logger.warning(e)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Create the record.
|
||||||
|
logger.debug('Record {0} ({1}) does not exist; creating'.format(fqdn, ip))
|
||||||
|
record = {'name': s,
|
||||||
|
'type': t,
|
||||||
|
'target': ip,
|
||||||
|
'ttl_sec': 300}
|
||||||
|
create_r = self.session.post(records_url,
|
||||||
|
json = record)
|
||||||
|
if not create_r.ok:
|
||||||
|
e = 'Could not create record {0} ({1}); skipping'.format(fqdn, t)
|
||||||
|
if is_tty:
|
||||||
|
warnings.warn(e)
|
||||||
|
logger.warning(e)
|
||||||
|
continue
|
||||||
|
return()
|
||||||
|
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = ('Automatically update Linode DNS via their API'))
|
||||||
|
args.add_argument('-c', '--config',
|
||||||
|
dest = 'cfg_path',
|
||||||
|
default = '~/.config/ddns.xml',
|
||||||
|
help = ('The path to the configuration file. Default: ~/.config/ddns.xml'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parseArgs().parse_args()
|
||||||
|
u = Updater(**vars(args))
|
||||||
|
u.update()
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
19
net/dns/linode/example.ddns.xml
Normal file
19
net/dns/linode/example.ddns.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!-- You very much most likely will want to leave "base" ALONE. Make sure you change "token" to your Linode API token,
|
||||||
|
though. -->
|
||||||
|
<api base="https://api.linode.com/v4/"
|
||||||
|
token="YOUR_TOKEN_HERE">
|
||||||
|
<!-- Domains MUST be created first in the Linode Domains manager! -->
|
||||||
|
<domain name="domain1.com">
|
||||||
|
<!-- This would be for the A/AAAA record "foo.domain1.com". -->
|
||||||
|
<sub>foo</sub>
|
||||||
|
<!-- And obviously, this for "bar.domain1.com". -->
|
||||||
|
<sub>bar</sub>
|
||||||
|
</domain>
|
||||||
|
<domain name="domain2.net">
|
||||||
|
<!-- baz.domain2.net -->
|
||||||
|
<sub>baz</sub>
|
||||||
|
<!-- quux.domain2.net -->
|
||||||
|
<sub>quux</sub>
|
||||||
|
</domain>
|
||||||
|
</api>
|
120
net/dns/rfc4183.py
Executable file
120
net/dns/rfc4183.py
Executable file
@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# https://tools.ietf.org/html/rfc2317
|
||||||
|
# https://tools.ietf.org/html/rfc4183
|
||||||
|
desc = 'Gets the RFC 2317/4183 PTR of given IP addresses or A/AAAA records.'
|
||||||
|
|
||||||
|
# stdlib
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import ipaddress
|
||||||
|
import os
|
||||||
|
# pypi/pip
|
||||||
|
#try:
|
||||||
|
# import ipwhois
|
||||||
|
#except ImportError:
|
||||||
|
# exit('You need to install the ipwhois module.')
|
||||||
|
try:
|
||||||
|
import dns.resolver
|
||||||
|
import dns.reversename
|
||||||
|
except ImportError:
|
||||||
|
exit('You need to install the dnspython module.')
|
||||||
|
try:
|
||||||
|
import fqdn
|
||||||
|
except ImportError:
|
||||||
|
exit('You need to install the fqdn module.')
|
||||||
|
|
||||||
|
def resolveRecord(addr):
|
||||||
|
r = dns.resolver.Resolver()
|
||||||
|
ipaddrs = {'A': [],
|
||||||
|
'AAAA': []}
|
||||||
|
for rtype in ipaddrs.keys():
|
||||||
|
for record in r.query(addr, 'A'):
|
||||||
|
ipaddrs[rtype].append(record)
|
||||||
|
ipaddrs['ipv4'] = sorted(list(set(copy.deepcopy(ipaddrs['A']))))
|
||||||
|
ipaddrs['ipv6'] = sorted(list(set(copy.deepcopy(ipaddrs['AAAA']))))
|
||||||
|
del(ipaddrs['A'], ipaddrs['AAAA'])
|
||||||
|
if ipaddrs['ipv4'] == ipaddrs['ipv6']:
|
||||||
|
del(ipaddrs['ipv6'])
|
||||||
|
return(ipaddrs)
|
||||||
|
|
||||||
|
def genPTR(ipaddr, iptype):
|
||||||
|
_suffix = ''
|
||||||
|
# TODO: get the current PTR.
|
||||||
|
# TODO: do this more manually. We should use ipaddress and ipwhois to get
|
||||||
|
# the proper return for e.g. network gateways.
|
||||||
|
return(dns.reversename.from_address(ipaddr))
|
||||||
|
|
||||||
|
def chkInput(src):
|
||||||
|
# Determine the input, if we can.
|
||||||
|
src_out = (None, None)
|
||||||
|
try:
|
||||||
|
ipaddress.IPv4Address(src)
|
||||||
|
return(('ipv4', src))
|
||||||
|
except ipaddress.AddressValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ipaddress.IPv6Address(src)
|
||||||
|
return(('ipv6', src))
|
||||||
|
except ipaddress.AddressValueError:
|
||||||
|
pass
|
||||||
|
_p = os.path.abspath(os.path.expanduser(src))
|
||||||
|
if os.path.isfile(_p):
|
||||||
|
return(('file', _p))
|
||||||
|
# Last shot - is it a DNS record?
|
||||||
|
# Not quite perfect, as it's strictly RFC and there are plenty of
|
||||||
|
# subdomains out there that break RFC.
|
||||||
|
f = fqdn.FQDN(src)
|
||||||
|
if f.is_valid:
|
||||||
|
return(('dns', src))
|
||||||
|
return(src_out)
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
def chkArg(src):
|
||||||
|
src_out = chkInput(src)
|
||||||
|
if src_out == (None, None):
|
||||||
|
raise argparse.ArgumentTypeError(('"{0}" does not seem to be a ' +
|
||||||
|
'path to a file, an A/AAAA ' +
|
||||||
|
'record, or IPv4/IPv6 ' +
|
||||||
|
'address.').format(src))
|
||||||
|
return(src_out)
|
||||||
|
args = argparse.ArgumentParser(description = desc)
|
||||||
|
args.add_argument('data_in',
|
||||||
|
type = chkArg,
|
||||||
|
metavar = 'ADDRESS_OR_FILE',
|
||||||
|
help = ('The path to a file containing domains and IP ' +
|
||||||
|
'addresses OR a single IPv4/IPv6 address or ' +
|
||||||
|
'A/AAAA record. If an A/AAAA record, your ' +
|
||||||
|
'machine must be able to resolve it (and it ' +
|
||||||
|
'must exist)'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# TODO: clean this up, migrate the duplicated code into a func
|
||||||
|
args = vars(parseArgs().parse_args())['data_in']
|
||||||
|
if args[0] == 'dns':
|
||||||
|
r = resolveRecord(args[1])
|
||||||
|
for k in r.keys():
|
||||||
|
for ip in r[k]:
|
||||||
|
print('IP: {0}'.format(ip))
|
||||||
|
print('PTR: {0}'.format(genPTR(str(ip), k)))
|
||||||
|
elif args[0] in ('ipv4', 'ipv6'):
|
||||||
|
print('PTR: {0}'.format(genPTR(args[1], args[0])))
|
||||||
|
elif args[0] == 'file':
|
||||||
|
with open(args[1], 'r') as f:
|
||||||
|
recordlst = [i.strip() for i in f.readlines()]
|
||||||
|
for i in recordlst:
|
||||||
|
ltype, data = chkInput(i)
|
||||||
|
print('== {0} =='.format(i))
|
||||||
|
if ltype == 'dns':
|
||||||
|
r = resolveRecord(data)
|
||||||
|
for k in r.keys():
|
||||||
|
for ip in r[k]:
|
||||||
|
print('IP: {0}'.format(ip))
|
||||||
|
print('PTR: {0}'.format(genPTR(str(ip), k)))
|
||||||
|
elif ltype in ('ipv4', 'ipv6'):
|
||||||
|
print('PTR: {0}'.format(genPTR(data, ltype)))
|
||||||
|
print()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
93
net/get_title.py
Executable file
93
net/get_title.py
Executable file
@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
try:
|
||||||
|
import requests as handler
|
||||||
|
has_req = True
|
||||||
|
except ImportError:
|
||||||
|
from urllib.request import urlopen as handler
|
||||||
|
has_req = False
|
||||||
|
try:
|
||||||
|
import lxml
|
||||||
|
parser = 'lxml'
|
||||||
|
except ImportError:
|
||||||
|
parser = 'html.parser'
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
|
def_elem = 'title'
|
||||||
|
|
||||||
|
|
||||||
|
class InfoScraper(object):
|
||||||
|
def __init__(self, url, elem = def_elem, *args, **kwargs):
|
||||||
|
self.url = url
|
||||||
|
self.elem = elem
|
||||||
|
self.raw = None
|
||||||
|
self.str = None
|
||||||
|
self.soup = None
|
||||||
|
self._get_page()
|
||||||
|
|
||||||
|
def _get_page(self):
|
||||||
|
if has_req:
|
||||||
|
self.raw = handler.get(self.url).content
|
||||||
|
else:
|
||||||
|
with handler(self.url) as fh:
|
||||||
|
self.raw = fh.read()
|
||||||
|
try:
|
||||||
|
self.str = self.raw.decode('utf-8')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.soup = BeautifulSoup(self.str, features = parser)
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
def find(self):
|
||||||
|
rtrn = [e for e in self.soup.find_all(self.elem)]
|
||||||
|
return(rtrn)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser(description = 'Get quick information from a URL at a glance')
|
||||||
|
args.add_argument('-e', '--elem',
|
||||||
|
dest = 'elem',
|
||||||
|
default = def_elem,
|
||||||
|
help = ('The element(s) you want to scrape from the page. This is likely just going to be "{0}" (the default)').format(def_elem))
|
||||||
|
args.add_argument('-s', '--strip',
|
||||||
|
dest = 'strip',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, strip whitespace at the beginning/end of each element text'))
|
||||||
|
args.add_argument('-d', '--delineate',
|
||||||
|
dest = 'delin',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, delineate each element instance'))
|
||||||
|
args.add_argument('-c', '--count',
|
||||||
|
dest = 'count',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('If specified, provide a count of how many times -e/--elem was found'))
|
||||||
|
args.add_argument('url',
|
||||||
|
metavar = 'URL',
|
||||||
|
help = ('The URL to parse. It may need to be quoted or escaped depending on the URL and what shell you\'re using'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parseArgs().parse_args()
|
||||||
|
i = InfoScraper(**vars(args))
|
||||||
|
rslts = i.find()
|
||||||
|
if args.count:
|
||||||
|
print('Element {0} was found {1} time(s) at {2}. Results follow:'.format(args.elem, len(rslts), args.url))
|
||||||
|
for i in rslts:
|
||||||
|
t = i.text
|
||||||
|
if args.strip:
|
||||||
|
t = t.strip()
|
||||||
|
if args.delin:
|
||||||
|
print('== {0}: =='.format(args.elem))
|
||||||
|
print(t)
|
||||||
|
if args.delin:
|
||||||
|
print('==\n')
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
1
net/irc/.gitignore
vendored
Normal file
1
net/irc/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
control2format.pl
|
435
net/irc/irssilogparse.py
Executable file
435
net/irc/irssilogparse.py
Executable file
@ -0,0 +1,435 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
## REFERENCE ##
|
||||||
|
# https://github.com/myano/jenni/wiki/IRC-String-Formatting
|
||||||
|
# https://www.mirc.com/colors.html
|
||||||
|
# https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||||
|
# http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||||
|
# https://github.com/shabble/irssi-docs/wiki/Formats#Colourising-Text-in-IRC-Messages
|
||||||
|
# https://askubuntu.com/a/528938
|
||||||
|
# <irssi.git>/themes/default.theme and ..docs/formats.txt holds SOME clues to
|
||||||
|
# these.
|
||||||
|
# e.g.
|
||||||
|
# # text to insert at the beginning of each non-message line
|
||||||
|
# line_start = "%B-%n!%B-%n ";
|
||||||
|
#
|
||||||
|
# # timestamp styling, nothing by default
|
||||||
|
# timestamp = "$*";
|
||||||
|
#
|
||||||
|
# # any kind of text that needs hilighting, default is to bold
|
||||||
|
# hilight = "%_$*%_";
|
||||||
|
#####################
|
||||||
|
# ^D = \x04
|
||||||
|
# ^D8/ = bold dark grey (14)
|
||||||
|
# ^D9/ = cyan (10)
|
||||||
|
# ^Dg = color/fmting? reset
|
||||||
|
# ^D;/ = bold light cyan(11)
|
||||||
|
# ^Dc = bold
|
||||||
|
# ^D>/ = (incl. bell for ">"?)
|
||||||
|
## The key seems to be /opt/dev/optools/net/irc/irssilogparse.py (& assoc. .c file)
|
||||||
|
## see also <irssi.git>/src/core/log.c/h
|
||||||
|
## !!! HUGE THANKS to Nei@Freenode#irssi! He pointed me to http://anti.teamidiot.de/static/nei/*/Code/Irssi/control2format.pl
|
||||||
|
# which nicely maps those internal command/control chars to the irssi
|
||||||
|
# templated stuff in their official docs (e.g. %b).
|
||||||
|
|
||||||
|
# Sorry for the copious comments, but the majority of the Irssi log stuff isn't
|
||||||
|
# really documented... anywhere. Just the color codes and \x03.
|
||||||
|
# And the log2ansi.pl script is... well, perl, so minus points for that, but
|
||||||
|
# the names are obtuse and perl's ugly af.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import curses
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
import magic
|
||||||
|
has_magic = True
|
||||||
|
except ImportError:
|
||||||
|
print('Warning: you do not have the magic module installed (you can '
|
||||||
|
'install it via "pip3 install --user file-magic"). Automatic log '
|
||||||
|
'decompression will not work.')
|
||||||
|
has_magic = False
|
||||||
|
|
||||||
|
# This is a map to determine which module to use to decompress,
|
||||||
|
# if we should.
|
||||||
|
cmprsn_map = {'text/plain': None, # Plain ol' text
|
||||||
|
# Sometimes the formatting with color gives this
|
||||||
|
'application/octet-stream': None,
|
||||||
|
'application/x-bzip2': 'bz2', # Bzip2
|
||||||
|
'application/x-gzip': 'gzip', # Gzip
|
||||||
|
'application/x-xz': 'lzma'} # XZ
|
||||||
|
|
||||||
|
# irssi/mIRC to ANSI
|
||||||
|
# Split into 3 maps (truecolor will be populated later, currently uses 8-bit):
|
||||||
|
# - 8 (3/4 bit color values, 8 colors)
|
||||||
|
# - 256 (8-bit, 256 colors)
|
||||||
|
# - 'truecolor' (24-bit, ISO-8613-3, 16777216 colors)
|
||||||
|
# Keys are the mIRC color value. Reference the links above for reference on
|
||||||
|
# what the values map to.
|
||||||
|
# Values are:
|
||||||
|
# - 8: tuple for ANSI fg and bg values
|
||||||
|
# - 256: single value (same number is used for fg and bg)
|
||||||
|
# - 'truecolor': tuple of (R#, G#, B#) (same number is used for fg and bg)
|
||||||
|
# In addition, all three have the following:
|
||||||
|
# - ansi_wrap: the string formatter.
|
||||||
|
# fg: foreground color
|
||||||
|
# bg: background color (if present)
|
||||||
|
# They are concatted together in that order.
|
||||||
|
## https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit
|
||||||
|
colormap = {8: {'0': ('97', '107'),
|
||||||
|
'1': ('30', '40'),
|
||||||
|
'2': ('34', '44'),
|
||||||
|
'3': ('32', '42'),
|
||||||
|
'4': ('91', '101'),
|
||||||
|
'5': ('31', '41'),
|
||||||
|
'6': ('35', '45'),
|
||||||
|
'7': ('33', '43'),
|
||||||
|
'8': ('93', '103'),
|
||||||
|
'9': ('92', '102'),
|
||||||
|
'10': ('36', '46'),
|
||||||
|
'11': ('96', '106'),
|
||||||
|
'12': ('94', '104'),
|
||||||
|
'13': ('95', '105'),
|
||||||
|
'14': ('90', '100'),
|
||||||
|
'15': ('37', '47'),
|
||||||
|
'ansi_wrap': {'fg': '\x1b[{0[0]}',
|
||||||
|
'bg': ';{0[1]}m'}},
|
||||||
|
## https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
|
||||||
|
256: {'0': '15',
|
||||||
|
'1': '0',
|
||||||
|
'2': '19',
|
||||||
|
'3': '34',
|
||||||
|
'4': '196',
|
||||||
|
'5': '52',
|
||||||
|
'6': '90',
|
||||||
|
'7': '208',
|
||||||
|
'8': '226',
|
||||||
|
'9': '82',
|
||||||
|
'10': '37',
|
||||||
|
'11': '51',
|
||||||
|
'12': '21',
|
||||||
|
'13': '199',
|
||||||
|
'14': '241',
|
||||||
|
'15': '252',
|
||||||
|
'ansi_wrap': {'fg': '\x1b[38;5;{0}m',
|
||||||
|
'bg': '\x1b[48;5;{0}m'}},
|
||||||
|
## https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit
|
||||||
|
# (can just use mIRC's R,G,B)
|
||||||
|
'truecolor': {'0': ('255', '255', '255'),
|
||||||
|
'1': ('0', '0', '0'),
|
||||||
|
'2': ('0', '0', '127'),
|
||||||
|
'3': ('0', '147', '0'),
|
||||||
|
'4': ('255', '0', '0'),
|
||||||
|
'5': ('127', '0', '0'),
|
||||||
|
'6': ('156', '0', '156'),
|
||||||
|
'7': ('252', '127', '0'),
|
||||||
|
'8': ('255', '255', '0'),
|
||||||
|
'9': ('0', '252', '0'),
|
||||||
|
'10': ('0', '147', '147'),
|
||||||
|
'11': ('0', '255', '255'),
|
||||||
|
'12': ('0', '0', '252'),
|
||||||
|
'13': ('255', '0', '255'),
|
||||||
|
'14': ('127', '127', '127'),
|
||||||
|
'15': ('210', '210', '210'),
|
||||||
|
'ansi_wrap': {'fg': '\x1b[38;2;'
|
||||||
|
'{0[0]};{0[1]};{0[2]}m',
|
||||||
|
'bg': '\x1b[48;2;'
|
||||||
|
'{0[0]};{0[1]};{0[2]}m'}}}
|
||||||
|
# These are special "control characters" Irssi uses.
|
||||||
|
reset_char = '\x1b[0m'
|
||||||
|
bold_char = '\x1b[1m'
|
||||||
|
invert_char = '\x1b[7m'
|
||||||
|
# Used for Irssi-specific escapes/controls.
|
||||||
|
irssi_ctrl = {'a': '\x1b[5m', # Blink
|
||||||
|
'b': '\x1b[4m', # Underline
|
||||||
|
'c': bold_char, # Bold
|
||||||
|
'd': invert_char, # Reverse (unused; color_inverter() is called)
|
||||||
|
#'e': '\t', # Indent
|
||||||
|
'e': None, # Indent
|
||||||
|
'f': None, # "f" is an indent func, so no-op
|
||||||
|
'g': reset_char, # Reset
|
||||||
|
'h': None, # Monospace (no-op)
|
||||||
|
'>': bold_char, # Undocumented? Seems to be bold/highlight.
|
||||||
|
';': bold_char} # Undocumented? Seems to be bold/highlight.
|
||||||
|
# This is used for inversion on the colors.
|
||||||
|
# # the foreground color (dynamic)
|
||||||
|
# fg = '\x1b[39m'
|
||||||
|
# # the background color (dynamic)
|
||||||
|
# bg = '\x1b[49m'
|
||||||
|
# the value to reset the foreground text (not changed)
|
||||||
|
def_fg = '\x1b[39m'
|
||||||
|
# the value to reset the background text (not changed)
|
||||||
|
def_bg = '\x1b[49m'
|
||||||
|
# if the state is currently inverted (dynamic)
|
||||||
|
is_inverted = False
|
||||||
|
|
||||||
|
def get_palette():
|
||||||
|
# Return 8, 256, or 'truecolor'
|
||||||
|
colorterm = os.getenv('COLORTERM', None)
|
||||||
|
if colorterm in ('truecolor', '24bit'):
|
||||||
|
# TODO: 24-bit support (16777216 colors) instead of 8-bit.
|
||||||
|
# See note above.
|
||||||
|
#return('truecolor')
|
||||||
|
#return(256)
|
||||||
|
return(8)
|
||||||
|
else:
|
||||||
|
curses.initscr()
|
||||||
|
curses.start_color()
|
||||||
|
c = curses.COLORS
|
||||||
|
curses.endwin()
|
||||||
|
return(c)
|
||||||
|
|
||||||
|
def color_inverter(data = None):
|
||||||
|
# global fg
|
||||||
|
# global bg
|
||||||
|
global is_inverted
|
||||||
|
#fg, bg = bg, fg
|
||||||
|
if is_inverted:
|
||||||
|
char = '\x1b[27m'
|
||||||
|
else:
|
||||||
|
char = '\x1b[7m'
|
||||||
|
is_inverted = (not is_inverted)
|
||||||
|
return(char)
|
||||||
|
|
||||||
|
|
||||||
|
def color_converter(data_in, palette_map):
|
||||||
|
# Only used if logParser().args['color'] = True
|
||||||
|
# Convert mIRC/Irssi color coding to ANSI color codes.
|
||||||
|
# A sub-function that generates the replacement characters.
|
||||||
|
global fg
|
||||||
|
global bg
|
||||||
|
global is_inverted
|
||||||
|
_colors = colormap[palette_map]
|
||||||
|
def _repl(ch_in):
|
||||||
|
ch_out = ''
|
||||||
|
_ch = {'stripped': re.sub('^[\x00-\x7f]', '', ch_in.group()),
|
||||||
|
#'ctrl': re.sub('^\x04([a-h]|[0-9]|;|>)/?.*$', '\g<1>',
|
||||||
|
'ctrl': re.sub('^\x04([^/])/?.*$', '\g<1>',
|
||||||
|
ch_in.group())}
|
||||||
|
# We assign this separately as we use an existing dict entry.
|
||||||
|
# This is the "color code" (if it exists).
|
||||||
|
_ch['c'] = [re.sub('^0?([0-9]{1,2}).*',
|
||||||
|
'\g<1>',
|
||||||
|
i.strip()) for i in _ch['stripped'].split(',', 1)]
|
||||||
|
# Color-handling
|
||||||
|
#if _ch['ctrl'].startswith('\x03'):
|
||||||
|
if re.search('[\x00-\x03]', _ch['ctrl']):
|
||||||
|
if len(_ch['c']) == 1:
|
||||||
|
fg_only = True
|
||||||
|
elif len(_ch['c']) == 2:
|
||||||
|
fg_only = False
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Parsing error! "{0}"'.format(
|
||||||
|
ch_in.group()))
|
||||||
|
fg = _colors['ansi_wrap']['fg'].format(_colors[_ch['c'][0]])
|
||||||
|
ch_out = fg
|
||||||
|
if not fg_only:
|
||||||
|
bg = _colors['ansi_wrap']['bg'].format(_colors[_ch['c'][1]])
|
||||||
|
ch_out += bg
|
||||||
|
else:
|
||||||
|
if palette_map == 8:
|
||||||
|
ch_out += 'm'
|
||||||
|
# Control-character handling
|
||||||
|
else:
|
||||||
|
if _ch['ctrl'] in irssi_ctrl:
|
||||||
|
if irssi_ctrl[_ch['ctrl']]:
|
||||||
|
ch_out = irssi_ctrl[_ch['ctrl']]
|
||||||
|
if _ch['ctrl'] == 'g':
|
||||||
|
color_inverter()
|
||||||
|
elif re.search('^[0-9]', _ch['ctrl']):
|
||||||
|
ch_out = _colors['ansi_wrap']['fg'].format(
|
||||||
|
_colors[_ch['c'][0]])
|
||||||
|
if palette_map == 8:
|
||||||
|
ch_out += 'm'
|
||||||
|
else:
|
||||||
|
# _ch['ctrl'] is not found and we don't have a color number
|
||||||
|
# to look up, so leave ch_out as ''
|
||||||
|
pass
|
||||||
|
return(ch_out)
|
||||||
|
#color_ptrn = re.compile('\x03[0-9]{1,2}(,[0-9]{1,2})?')
|
||||||
|
catch = re.compile('(\x03[0-9]{2}(,[0-9]{1, 2})?|'
|
||||||
|
'\x04([a-h]|;/|>/|[0-9]/?))')
|
||||||
|
# These are some non-programmatic regexes.
|
||||||
|
# Clean up the nick.
|
||||||
|
nick_re = re.compile('(\x048/\s*)')
|
||||||
|
# mIRC uses a different tag for reset.
|
||||||
|
mirc_rst = re.compile('\x0f')
|
||||||
|
# mIRC and Irssi tags, respectively, for inversion.
|
||||||
|
re_invert = re.compile('(\x16|\x04d)')
|
||||||
|
data = data_in.splitlines()
|
||||||
|
for idx, line in enumerate(data[:]):
|
||||||
|
# Get some preliminary replacements out of the way.
|
||||||
|
line = nick_re.sub(' ', line, 1)
|
||||||
|
line = re_invert.sub(color_inverter, line)
|
||||||
|
line = mirc_rst.sub(reset_char, line)
|
||||||
|
# This is like 90% of the magic, honestly.
|
||||||
|
line = catch.sub(_repl, line)
|
||||||
|
if not line.endswith(reset_char):
|
||||||
|
line += reset_char
|
||||||
|
# Since we clear all formatting at the end of each line
|
||||||
|
is_inverted = False
|
||||||
|
data[idx] = line
|
||||||
|
return('\n'.join(data))
|
||||||
|
|
||||||
|
def plain_stripper(data_in):
|
||||||
|
# Strip to plaintext only.
|
||||||
|
data = data_in.splitlines()
|
||||||
|
ptrns = [re.compile('\x04(g|c|[389;]/?|e|>)/?'),
|
||||||
|
re.compile('((\x03)\d\d?,\d\d?|(\x03)\d\d?|[\x01-\x1F])')]
|
||||||
|
for idx, line in enumerate(data[:]):
|
||||||
|
# This cleans the nick field
|
||||||
|
l = re.sub('\x04[89]/', ' ', line, 1)
|
||||||
|
# And these clean the actual chat messages
|
||||||
|
for p in ptrns:
|
||||||
|
l = p.sub('', l)
|
||||||
|
data[idx] = l
|
||||||
|
return('\n'.join(data))
|
||||||
|
|
||||||
|
class irssiLogParser(object):
|
||||||
|
def __init__(self, args, data = None):
|
||||||
|
# We'll need these accessible across the entire class.
|
||||||
|
self.args = args
|
||||||
|
# If specified, self.data takes precedence over self.args['logfile']
|
||||||
|
# (if it was specified).
|
||||||
|
self.data = data
|
||||||
|
self.raw = data
|
||||||
|
self.has_html = False
|
||||||
|
self.decompress = None
|
||||||
|
if 'color' in self.args and self.args['color']:
|
||||||
|
if not self.args['html']:
|
||||||
|
# Ensure that we support color output.
|
||||||
|
curses.initscr()
|
||||||
|
self.args['color'] = curses.can_change_color()
|
||||||
|
curses.endwin()
|
||||||
|
if not self.args['color'] and not self.args['raw']:
|
||||||
|
raise RuntimeError('You have specified ANSI colorized '
|
||||||
|
'output but your terminal does not '
|
||||||
|
'support it. Use -fc/--force-color '
|
||||||
|
'to force.')
|
||||||
|
elif not self.args['color'] and self.args['raw']:
|
||||||
|
self.args['color'] = True # Force the output anyways.
|
||||||
|
if self.args['color']:
|
||||||
|
if not self.args['raw']:
|
||||||
|
self.colors = get_palette()
|
||||||
|
else:
|
||||||
|
self.colors = 8 # Best play it safe for maximum compatibility.
|
||||||
|
# The full, interpreted path.
|
||||||
|
if ('logfile' in self.args.keys() and
|
||||||
|
self.args['logfile'] is not None):
|
||||||
|
self.args['logfile'] = os.path.abspath(
|
||||||
|
os.path.expanduser(
|
||||||
|
self.args['logfile']))
|
||||||
|
if not self.data:
|
||||||
|
self.getlog()
|
||||||
|
else:
|
||||||
|
# Conform everything to bytes.
|
||||||
|
if not isinstance(self.data, bytes):
|
||||||
|
self.data = self.data.encode('utf-8')
|
||||||
|
self.decompressor()
|
||||||
|
self.parser()
|
||||||
|
|
||||||
|
def getlog(self):
|
||||||
|
# A filepath was specified
|
||||||
|
if self.args['logfile']:
|
||||||
|
if not os.path.isfile(self.args['logfile']):
|
||||||
|
raise FileNotFoundError('{0} does not exist'.format(
|
||||||
|
self.args['logfile']))
|
||||||
|
with open(self.args['logfile'], 'rb') as f:
|
||||||
|
self.data = f.read()
|
||||||
|
# Try to get it from stdin
|
||||||
|
else:
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
self.data = sys.stdin.buffer.read()
|
||||||
|
else:
|
||||||
|
raise ValueError('Either a path to a logfile must be '
|
||||||
|
'specified or you must pipe a log in from '
|
||||||
|
'stdin.')
|
||||||
|
self.raw = self.data
|
||||||
|
return()
|
||||||
|
|
||||||
|
def decompressor(self):
|
||||||
|
# TODO: use mime module as fallback?
|
||||||
|
# https://docs.python.org/3/library/mimetypes.html
|
||||||
|
# VERY less-than-ideal since it won't work without self.args['logfile']
|
||||||
|
# (and has iffy detection at best, since it relies on file extensions).
|
||||||
|
# Determine what decompressor to use, if we need to.
|
||||||
|
if has_magic:
|
||||||
|
_mime = magic.detect_from_content(self.data).mime_type
|
||||||
|
self.decompress = cmprsn_map[_mime]
|
||||||
|
if self.decompress:
|
||||||
|
import importlib
|
||||||
|
decmp = importlib.import_module(self.decompress)
|
||||||
|
self.raw = decmp.decompress(self.data)
|
||||||
|
else:
|
||||||
|
# Assume that it's text and that it isn't compressed.
|
||||||
|
# We'll get a UnicodeDecodeError exception if it isn't.
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.raw = self.data.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
pass
|
||||||
|
self.data = self.raw
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parser(self):
|
||||||
|
if 'color' not in self.args or not self.args['color']:
|
||||||
|
self.data = plain_stripper(self.data)
|
||||||
|
else:
|
||||||
|
self.data = color_converter(self.data, self.colors)
|
||||||
|
# Just in case...
|
||||||
|
self.data += '\x1b[0m'
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
args.add_argument('-c', '--color',
|
||||||
|
dest = 'color',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('Print the log with converted colors (ANSI)'))
|
||||||
|
args.add_argument('-r', '--raw',
|
||||||
|
dest = 'raw',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('Use this switch if your terminal is detected '
|
||||||
|
'as not supporting color output but wish to '
|
||||||
|
'force it anyways. A string representation of '
|
||||||
|
'the ANSI output will be produced instead ('
|
||||||
|
'suitable for pasting elsewhere). Only used if '
|
||||||
|
'-c/--color is enabled (ignored with '
|
||||||
|
'-H/--html)'))
|
||||||
|
args.add_argument('-H', '--html',
|
||||||
|
dest = 'html',
|
||||||
|
action = 'store_true',
|
||||||
|
help = ('Render HTML output (requires ansi2html)'))
|
||||||
|
args.add_argument(dest = 'logfile',
|
||||||
|
default = None,
|
||||||
|
nargs = '?',
|
||||||
|
metavar = 'path/to/logfile',
|
||||||
|
help = ('The path to the log file. It can be uncompressed ' +
|
||||||
|
'or compressed with XZ/LZMA, Gzip, or Bzip2. '
|
||||||
|
'If not specified, read from stdin'))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
l = irssiLogParser(args)
|
||||||
|
import shutil
|
||||||
|
cols = shutil.get_terminal_size().columns
|
||||||
|
#print('ARGS:')
|
||||||
|
#pprint.pprint(l.args, width = cols)
|
||||||
|
# print('RAW')
|
||||||
|
# pprint.pprint(l.raw, width = cols)
|
||||||
|
# with open('/tmp/freenode.formatted', 'r') as f:
|
||||||
|
# print(f.read())
|
||||||
|
#print('DATA')
|
||||||
|
#pprint.pprint(l.data, width = cols)
|
||||||
|
#print('DATA (REPR)')
|
||||||
|
#pprint.pprint(repr(l.data).split('\\n'))
|
||||||
|
print('DATA')
|
||||||
|
print(l.data)
|
||||||
|
with open('/tmp/log.raw', 'w') as f:
|
||||||
|
for line in repr(l.data).split('\\n'):
|
||||||
|
f.write(line + '\n')
|
||||||
|
# l.parseLog()
|
||||||
|
# print(l.data.decode('utf-8'))
|
2
net/mirroring/.gitignore
vendored
Normal file
2
net/mirroring/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
urls.csv
|
||||||
|
/cache
|
159
net/mirroring/check.py
Executable file
159
net/mirroring/check.py
Executable file
@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import datetime
|
||||||
|
import difflib
|
||||||
|
import hashlib
|
||||||
|
import lzma
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
# TODO: to avoid race conditions, we should probably simply ignore/remove
|
||||||
|
# timestamps and just touch the cache file whenever checking.
|
||||||
|
|
||||||
|
class website(object):
|
||||||
|
def __init__(self, args, csvline):
|
||||||
|
# Field names
|
||||||
|
self.fnames = ('UUID', 'url', 'checksum', 'timestamp')
|
||||||
|
self.args = args
|
||||||
|
self.parseCSV([csvline])
|
||||||
|
self.cache = args['cache_dir']
|
||||||
|
self.cacheControl()
|
||||||
|
self.remoteFetch()
|
||||||
|
return
|
||||||
|
|
||||||
|
def parseCSV(self, data):
|
||||||
|
_rows = csv.DictReader(data,
|
||||||
|
fieldnames = self.fnames,
|
||||||
|
delimiter = ',',
|
||||||
|
quotechar = '"')
|
||||||
|
for r in _rows:
|
||||||
|
self.meta = r
|
||||||
|
break # We only want one, so if we SOMEHOW got more than one line...
|
||||||
|
return()
|
||||||
|
|
||||||
|
def cacheControl(self):
|
||||||
|
os.makedirs(self.cache, exist_ok = True)
|
||||||
|
self.site = {}
|
||||||
|
_cachefile = os.path.join(self.cache, self.meta['UUID'])
|
||||||
|
if os.path.isfile(_cachefile):
|
||||||
|
with lzma.open(_cachefile, mode = 'rb') as f:
|
||||||
|
self.site['local'] = pickle.load(f)
|
||||||
|
else:
|
||||||
|
with urlopen(self.meta['url']) as _site,\
|
||||||
|
lzma.open(_cachefile,
|
||||||
|
mode = 'wb',
|
||||||
|
check = lzma.CHECK_SHA256,
|
||||||
|
preset = 9|lzma.PRESET_EXTREME) as f:
|
||||||
|
_data = _site.read().decode('utf-8')
|
||||||
|
pickle.dump(_data, f)
|
||||||
|
self.site['local'] = _data
|
||||||
|
self.meta['timestamp'] = str(int(datetime.datetime.now().timestamp()))
|
||||||
|
_hash = hashlib.sha256(self.site['local'].encode('utf-8'))
|
||||||
|
self.meta['checksum'] = str(_hash.hexdigest())
|
||||||
|
return()
|
||||||
|
|
||||||
|
def remoteFetch(self):
|
||||||
|
with urlopen(self.meta['url']) as _site:
|
||||||
|
self.site['remote'] = _site.read()
|
||||||
|
self.headers = dict(_site.info())
|
||||||
|
# Handle gzip encoding
|
||||||
|
if 'Content-Encoding' in self.headers.keys():
|
||||||
|
if self.headers['Content-Encoding'] == 'gzip':
|
||||||
|
from gzip import decompress
|
||||||
|
self.site['remote'] = decompress(self.site['remote']).decode('utf-8')
|
||||||
|
else:
|
||||||
|
self.site['remote'] = self.site['remote'].decode('utf-8')
|
||||||
|
_hash = hashlib.sha256(self.site['remote'].encode('utf-8'))
|
||||||
|
self.site['remotesum'] = str(_hash.hexdigest())
|
||||||
|
self.meta['timestamp'] = str(int(datetime.datetime.now().timestamp()))
|
||||||
|
return()
|
||||||
|
|
||||||
|
def compare(self):
|
||||||
|
# Don't even compare if the checksums match.
|
||||||
|
if self.site['remotesum'] == self.meta['checksum']:
|
||||||
|
self.diff = None
|
||||||
|
#print('{0}: Doing nothing'.format(self.meta['UUID']))
|
||||||
|
return()
|
||||||
|
print('{{{0}}}: "{1}":'.format(self.meta['UUID'], self.meta['url']))
|
||||||
|
diff = difflib.unified_diff(self.site['local'].splitlines(1),
|
||||||
|
self.site['remote'].splitlines(1))
|
||||||
|
self.diff = ''.join(diff)
|
||||||
|
print(self.diff)
|
||||||
|
with urlopen(self.meta['url']) as _site,\
|
||||||
|
lzma.open(os.path.join(self.cache, self.meta['UUID']),
|
||||||
|
mode = 'wb',
|
||||||
|
check = lzma.CHECK_SHA256,
|
||||||
|
preset = 9|lzma.PRESET_EXTREME) as f:
|
||||||
|
_data = _site.read().decode('utf-8')
|
||||||
|
pickle.dump(_data, f)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def writeCSV(self):
|
||||||
|
#if self.diff: # We actually WANT to write, because we're updating the last fetch timestamp.
|
||||||
|
_lines = []
|
||||||
|
with open(self.args['urls_csv'], 'r') as f:
|
||||||
|
_f = f.read()
|
||||||
|
_rows = csv.DictReader(_f.splitlines(),
|
||||||
|
fieldnames = self.fnames,
|
||||||
|
delimiter = ',',
|
||||||
|
quotechar = '"')
|
||||||
|
for r in _rows:
|
||||||
|
_uuid = r['UUID']
|
||||||
|
if _uuid == self.meta['UUID']:
|
||||||
|
r['checksum'] = self.site['remotesum']
|
||||||
|
r['timestamp'] = self.meta['timestamp']
|
||||||
|
_lines.append(r)
|
||||||
|
with open(self.args['urls_csv'], 'w', newline = '') as f:
|
||||||
|
_w = csv.DictWriter(f,
|
||||||
|
fieldnames = self.fnames,
|
||||||
|
delimiter = ',',
|
||||||
|
quotechar = '"',
|
||||||
|
quoting = csv.QUOTE_ALL)
|
||||||
|
_w.writerows(_lines)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
# Define defaults
|
||||||
|
_self_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
_cache_dir = os.path.join(_self_dir, 'cache')
|
||||||
|
_urls_csv = os.path.join(_self_dir, 'urls.csv')
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
args.add_argument('-c',
|
||||||
|
'--cache-dir',
|
||||||
|
metavar = '/path/to/cache/dir/',
|
||||||
|
default = _cache_dir,
|
||||||
|
dest = 'cache_dir',
|
||||||
|
type = str,
|
||||||
|
help = ('The path to where cached versions of websites are stored. ' +
|
||||||
|
'They are stored in the python binary "pickle" format. ' +
|
||||||
|
'Default: \n\n\t\033[1m{0}\033[0m').format(_cache_dir))
|
||||||
|
args.add_argument('-u',
|
||||||
|
'--urls',
|
||||||
|
metavar = '/path/to/urls.csv',
|
||||||
|
default = _urls_csv,
|
||||||
|
dest = 'urls_csv',
|
||||||
|
type = str,
|
||||||
|
help = ('The path to where a CSV file of the URLs to check should be. ' +
|
||||||
|
'Note that it should be writeable by whatever user the script is running as.' +
|
||||||
|
'See urls.csv.spec for the specification. ' +
|
||||||
|
'Default: \n\n\t\033[1m{0}\033[0m').format(_urls_csv))
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
for d in ('cache_dir', 'urls_csv'):
|
||||||
|
args[d] = os.path.realpath(os.path.expanduser(args[d]))
|
||||||
|
with open(args['urls_csv'], 'r', newline = '') as f:
|
||||||
|
_csv = f.read()
|
||||||
|
for line in _csv.splitlines():
|
||||||
|
w = website(args, line)
|
||||||
|
w.compare()
|
||||||
|
w.writeCSV()
|
||||||
|
if w.diff:
|
||||||
|
print(w.diff)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
15
net/mirroring/urls.csv.spec
Normal file
15
net/mirroring/urls.csv.spec
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"UUID","URL","SHA512_of_CONTENT","LAST_FETCHED_IN_UNIX_EPOCH"
|
||||||
|
|
||||||
|
UUID can be any non-whitespace, non-slashed string suitable for filenames you want, but I recommend a UUID4.
|
||||||
|
You can generate one at either https://www.uuidgenerator.net/ or via python:
|
||||||
|
|
||||||
|
>>> import uuid
|
||||||
|
>>> str(uuid.uuid4())
|
||||||
|
'16728c9e-5fde-4f63-8a36-4a3db612be8d'
|
||||||
|
|
||||||
|
It should be unique for every page.
|
||||||
|
|
||||||
|
|
||||||
|
You can generate an UNIX Epoch timestamp via:
|
||||||
|
|
||||||
|
date '+%s'
|
131
net/ssh/audit.py
Executable file
131
net/ssh/audit.py
Executable file
@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import paramiko
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
class SSHAuthInfo(object):
|
||||||
|
def __init__(self, target, port = 22, banner = True, ciphers = True, digests = True, kex = True, key_types = True,
|
||||||
|
methods = True, hostkeys = True, version = True):
|
||||||
|
self.target = target
|
||||||
|
self.port = int(port)
|
||||||
|
self.info = {'target': self.target,
|
||||||
|
'port': self.port,
|
||||||
|
'banner': banner,
|
||||||
|
'ciphers': ciphers,
|
||||||
|
'digests': digests,
|
||||||
|
'kex': kex,
|
||||||
|
'key_types': key_types,
|
||||||
|
'methods': methods,
|
||||||
|
'hostkeys': hostkeys,
|
||||||
|
'version': version}
|
||||||
|
self._ssh = None
|
||||||
|
if any((ciphers, banner, methods, digests, kex, key_types)): # These need an SSH connection.
|
||||||
|
self._ssh_dummy()
|
||||||
|
if banner:
|
||||||
|
self.getBanner()
|
||||||
|
if hostkeys:
|
||||||
|
self.getHostkeys()
|
||||||
|
if version:
|
||||||
|
self.getVersion()
|
||||||
|
self._close()
|
||||||
|
|
||||||
|
def _ssh_dummy(self):
|
||||||
|
self._ssh = paramiko.Transport((self.target, self.port))
|
||||||
|
self._ssh.connect()
|
||||||
|
try:
|
||||||
|
self._ssh.auth_none('')
|
||||||
|
except paramiko.ssh_exception.BadAuthenticationType as err:
|
||||||
|
secopts = self._ssh.get_security_options()
|
||||||
|
if self.info['methods']:
|
||||||
|
# https://stackoverflow.com/a/1257769
|
||||||
|
self.info['methods'] = err.allowed_types
|
||||||
|
if self.info['ciphers']:
|
||||||
|
self.info['ciphers'] = list(secopts.ciphers)
|
||||||
|
if self.info['digests']:
|
||||||
|
self.info['digests'] = list(secopts.digests)
|
||||||
|
if self.info['kex']:
|
||||||
|
self.info['kex'] = list(secopts.kex)
|
||||||
|
if self.info['key_types']:
|
||||||
|
self.info['key_types'] = list(secopts.key_types)
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getBanner(self):
|
||||||
|
self.info['banner'] = None
|
||||||
|
# https://github.com/paramiko/paramiko/issues/273#issuecomment-225058645 doesn't seem to work.
|
||||||
|
# But https://github.com/paramiko/paramiko/pull/58#issuecomment-63857078 did!
|
||||||
|
self.info['banner'] = self._ssh.get_banner()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getHostkeys(self):
|
||||||
|
# TODO: how the hell do I get *all* hostkeys served?
|
||||||
|
self.info['hostkeys'] = {}
|
||||||
|
k = self._ssh.get_remote_server_key()
|
||||||
|
self.info['hostkeys'][k.get_name()] = k.get_base64()
|
||||||
|
return()
|
||||||
|
|
||||||
|
def getVersion(self):
|
||||||
|
self.info['version'] = None
|
||||||
|
s = socket.socket()
|
||||||
|
s.connect((self.target, self.port))
|
||||||
|
try:
|
||||||
|
# 8192 bytes is kind of overkill considering most are probably going to be around 20 bytes or so.
|
||||||
|
self.info['version'] = s.recv(8192)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
return()
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
if self._ssh:
|
||||||
|
self._ssh.close()
|
||||||
|
|
||||||
|
def parseArgs():
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
args.add_argument('-b', '--no-banner',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'banner',
|
||||||
|
help = 'Do not gather the SSH banner')
|
||||||
|
args.add_argument('-c', '--no-ciphers',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'ciphers',
|
||||||
|
help = 'Do not gather supported ciphers')
|
||||||
|
args.add_argument('-d', '--no-digests',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'digests',
|
||||||
|
help = 'Do not gather supported digests')
|
||||||
|
args.add_argument('-m', '--no-methods',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'methods',
|
||||||
|
help = 'Do not gather supported auth methods')
|
||||||
|
args.add_argument('-k', '--no-hostkeys',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'hostkeys',
|
||||||
|
help = 'Do not gather hostkeys')
|
||||||
|
args.add_argument('-x', '--no-kex',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'kex',
|
||||||
|
help = 'Do not gather supported key exchanges')
|
||||||
|
args.add_argument('-t', '--no-key-types',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'key_types',
|
||||||
|
help = 'Do not gather supported key types')
|
||||||
|
args.add_argument('-v', '--no-version',
|
||||||
|
action = 'store_false',
|
||||||
|
dest = 'version',
|
||||||
|
help = 'Do not gather SSH version')
|
||||||
|
args.add_argument('-p', '--port',
|
||||||
|
default = 22,
|
||||||
|
help = 'The port on target that the SSH daemon is running on. Default is 22')
|
||||||
|
args.add_argument('target',
|
||||||
|
help = 'The server to run the check against')
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = vars(parseArgs().parse_args())
|
||||||
|
i = SSHAuthInfo(**args)
|
||||||
|
import pprint
|
||||||
|
pprint.pprint(i.info)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
7
net/ssh/hostkeymanager/app/__init__.py
Normal file
7
net/ssh/hostkeymanager/app/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
app = Flask(__name__, instance_relative_config=True)
|
||||||
|
|
||||||
|
from app import views
|
||||||
|
|
||||||
|
app.config.from_object('config')
|
41
net/ssh/hostkeymanager/app/manage.py
Executable file
41
net/ssh/hostkeymanager/app/manage.py
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
# This is ugly as fuck. TODO: can we do this more cleanly?
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)))
|
||||||
|
import config
|
||||||
|
|
||||||
|
class DBmgr(object):
|
||||||
|
def __init__(self, args = None):
|
||||||
|
self.DB = config.DB
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def keyChk(self):
|
||||||
|
# Is it a pubkey file?
|
||||||
|
if os.path.isfile(os.path.abspath(os.path.expanduser(self.args['key']))):
|
||||||
|
with open(os.path.abspath(os.path.expanduser(self.args['key'])), 'r') as f:
|
||||||
|
self.args['key'] = f.read()
|
||||||
|
self.args['key'] = self.args['key'].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def add(self, key, host, role):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def argParse():
|
||||||
|
args = argparse.ArgumentParser()
|
||||||
|
args.add_argument('-k',
|
||||||
|
'--key',
|
||||||
|
dest = 'key',
|
||||||
|
default = None,
|
||||||
|
type = 'str',
|
||||||
|
|
||||||
|
return(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args -
|
||||||
|
d = DBmgr(args)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
0
net/ssh/hostkeymanager/app/models.py
Normal file
0
net/ssh/hostkeymanager/app/models.py
Normal file
4
net/ssh/hostkeymanager/app/templates/about.html
Normal file
4
net/ssh/hostkeymanager/app/templates/about.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% extends "base.html" %}{% block title %}r00t^2 SSH Key Repository || About{% endblock %}{% block body %}<div class="jumbotron">
|
||||||
|
<h1>About</h1></div>
|
||||||
|
<p>This is a tool to deliver SSH public keys (or, optionally, host keys) to SSH's authentication system in a safe and secure manner.</p>
|
||||||
|
{% endblock %}
|
35
net/ssh/hostkeymanager/app/templates/base.html
Normal file
35
net/ssh/hostkeymanager/app/templates/base.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<!-- Thanks, https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xii-facelift and
|
||||||
|
https://scotch.io/tutorials/getting-started-with-flask-a-python-microframework -->
|
||||||
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!--<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">-->
|
||||||
|
<!-- Custom styles for this template -->
|
||||||
|
<link href="https://getbootstrap.com/examples/jumbotron-narrow/jumbotron-narrow.css" rel="stylesheet">
|
||||||
|
<!--<link href="https://getbootstrap.com/docs/4.0/examples/offcanvas/offcanvas.css" rel="stylesheet">-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header clearfix">
|
||||||
|
<nav>
|
||||||
|
<ul class="nav nav-pills pull-right">
|
||||||
|
<li role="presentation"><a href="/">Home</a></li>
|
||||||
|
<li role="presentation"><a href="/about">About</a></li>
|
||||||
|
<li role="presentation"><a href="/usage">Usage</a></li>
|
||||||
|
<!-- the following opens in a new tab/window/whatever. the line after opens in the same tab/window/etc. -->
|
||||||
|
<!-- <li role="presentation"><a href="https://square-r00t.net/" target="_blank">r00t^2</a></li> -->
|
||||||
|
<li role="presentation"><a href="https://square-r00t.net/">r00t^2</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
<footer class="footer">
|
||||||
|
<p><sub>The code for this page is released under the <a href="https://www.gnu.org/licenses/gpl-3.0.en.html#content">GPL 3.0 License</a>. It can be found <a href="https://git.square-r00t.net/OpTools/tree/net">here</a>.</sub></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<!-- /container -->
|
||||||
|
</body>
|
||||||
|
</html>
|
38
net/ssh/hostkeymanager/app/templates/html.html
Normal file
38
net/ssh/hostkeymanager/app/templates/html.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<h2>Client/Browser Information</h2>
|
||||||
|
<p>This is information that your browser sends with its connection.</p>
|
||||||
|
<p>
|
||||||
|
<ul>
|
||||||
|
<li><b>Client IP:</b> <a href="https://ipinfo.io/{{ visitor['ip'] }}">{{ visitor['ip'] }}</a></li>
|
||||||
|
<li><b>Browser:</b> {{ '<a href="{0}">{1}</a>'.format(browsers[visitor['client']['browser']][0],
|
||||||
|
browsers[visitor['client']['browser']][1])|safe
|
||||||
|
if visitor['client']['browser'] in browsers.keys()
|
||||||
|
else visitor['client']['browser'].title()
|
||||||
|
if visitor['client']['browser'] is not none
|
||||||
|
else '(N/A)' }}</li>
|
||||||
|
<li><b>Language/Locale:</b> {{ visitor['client']['language'] or '(N/A)' }}</li>
|
||||||
|
{%- set alt_os = alts[visitor['client']['os']] if visitor['client']['os'] in alts.keys() else '' %}
|
||||||
|
<li><b>Operating System:</b> {{ '<a href="{0}">{1}</a>{2}'.format(os[visitor['client']['os']][0],
|
||||||
|
os[visitor['client']['os']][1],
|
||||||
|
alt_os)|safe
|
||||||
|
if visitor['client']['os'] in os.keys()
|
||||||
|
else visitor['client']['os'].title()
|
||||||
|
if visitor['client']['os'] is not none
|
||||||
|
else '(N/A)' }}</li>
|
||||||
|
<li><b>User Agent:</b> {{ visitor['client']['str'] }}</li>
|
||||||
|
<li><b>Version:</b> {{ visitor['client']['version'] or '(N/A)' }}</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<h2>Request Headers</h2>
|
||||||
|
<p>These are headers sent along with the request your browser sends for the page's content.</p>
|
||||||
|
<p>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>{% for k in visitor['headers'].keys()|sort(case_sensitive = True) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ k }}</td>
|
||||||
|
<td>{{ visitor['headers'][k] if visitor['headers'][k] != '' else '(N/A)' }}</td>
|
||||||
|
</tr>{% endfor %}
|
||||||
|
</table>
|
||||||
|
</p>
|
6
net/ssh/hostkeymanager/app/templates/index.html
Normal file
6
net/ssh/hostkeymanager/app/templates/index.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% extends "base.html" %}{% block title %}r00t^2 Client Info Revealer{% endblock %}{% block body %}<div class="jumbotron">
|
||||||
|
<h1>Client Info Revealer</h1>
|
||||||
|
<p class="lead">A tool to reveal client-identifying data sent to webservers</p>
|
||||||
|
</div>
|
||||||
|
{% include 'html.html' if not params['json'] else 'json.html' %}
|
||||||
|
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user