From 9c528c490815f7581aef97863230aee724c39502 Mon Sep 17 00:00:00 2001
From: brent s
Date: Sat, 18 Nov 2017 22:33:31 -0500
Subject: [PATCH] checking in all work done so far because what if my SSD dies?
---
TODO | 11 +-
git/remotehooks.py | 119 ++++++++++
git/remotehooks2.py | 69 ++++++
git/sample.githooks..json | 27 +++
mumble/.gitignore | 1 +
mumble/TODO | 8 +-
mumble/grpctest.py | 7 +
mumble/sample.mumbleadmin.ini | 87 ++++----
mumble/usrmgmt2.py | 76 +------
net/addr/app/dnsinfo.py | 49 ++++
net/dhcp/dhcpcdump.py | 210 ++++++++++++++++++
net/ssh/hostkeymanager/app/__init__.py | 7 +
net/ssh/hostkeymanager/app/manage.py | 41 ++++
net/ssh/hostkeymanager/app/models.py | 0
.../hostkeymanager/app/templates/about.html | 4 +
.../hostkeymanager/app/templates/base.html | 35 +++
.../hostkeymanager/app/templates/html.html | 38 ++++
.../hostkeymanager/app/templates/index.html | 6 +
.../hostkeymanager/app/templates/json.html | 1 +
.../hostkeymanager/app/templates/usage.html | 51 +++++
net/ssh/hostkeymanager/app/views.py | 57 +++++
net/ssh/hostkeymanager/config.py | 8 +
net/ssh/hostkeymanager/run.py | 4 +
net/ssh/hostkeymanager/uwsgi.ini | 18 ++
24 files changed, 820 insertions(+), 114 deletions(-)
create mode 100755 git/remotehooks.py
create mode 100755 git/remotehooks2.py
create mode 100644 git/sample.githooks..json
create mode 100755 mumble/grpctest.py
create mode 100644 net/addr/app/dnsinfo.py
create mode 100755 net/dhcp/dhcpcdump.py
create mode 100644 net/ssh/hostkeymanager/app/__init__.py
create mode 100755 net/ssh/hostkeymanager/app/manage.py
create mode 100644 net/ssh/hostkeymanager/app/models.py
create mode 100644 net/ssh/hostkeymanager/app/templates/about.html
create mode 100644 net/ssh/hostkeymanager/app/templates/base.html
create mode 100644 net/ssh/hostkeymanager/app/templates/html.html
create mode 100644 net/ssh/hostkeymanager/app/templates/index.html
create mode 100644 net/ssh/hostkeymanager/app/templates/json.html
create mode 100644 net/ssh/hostkeymanager/app/templates/usage.html
create mode 100644 net/ssh/hostkeymanager/app/views.py
create mode 100644 net/ssh/hostkeymanager/config.py
create mode 100644 net/ssh/hostkeymanager/run.py
create mode 100644 net/ssh/hostkeymanager/uwsgi.ini
diff --git a/TODO b/TODO
index 61015be..318e864 100644
--- a/TODO
+++ b/TODO
@@ -13,6 +13,13 @@
sshkeys:
--need to verify keys via GPG signature. we also need to have a more robust way of updating pubkeys - categorization
+-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])
\ No newline at end of file
+-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
\ No newline at end of file
diff --git a/git/remotehooks.py b/git/remotehooks.py
new file mode 100755
index 0000000..1bb1f28
--- /dev/null
+++ b/git/remotehooks.py
@@ -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()
diff --git a/git/remotehooks2.py b/git/remotehooks2.py
new file mode 100755
index 0000000..e9cb14e
--- /dev/null
+++ b/git/remotehooks2.py
@@ -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()
diff --git a/git/sample.githooks..json b/git/sample.githooks..json
new file mode 100644
index 0000000..5b45da5
--- /dev/null
+++ b/git/sample.githooks..json
@@ -0,0 +1,27 @@
+# remotehooks.py should go in your /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
+
+{
+ "": {
+ "remotecmds": {
+ "": {
+ "": {
+ "cmds": [
+ "",
+ ""
+ ]
+ }
+ }
+ }
+ },
+ "": {
+ "cmds": [
+ [
+ "",
+ ""
+ ]
+ ]
+ }
+}
diff --git a/mumble/.gitignore b/mumble/.gitignore
index 2f88269..b929f87 100644
--- a/mumble/.gitignore
+++ b/mumble/.gitignore
@@ -1 +1,2 @@
/docs
+/testcertimport.py
diff --git a/mumble/TODO b/mumble/TODO
index e710a27..ae93f21 100644
--- a/mumble/TODO
+++ b/mumble/TODO
@@ -1,4 +1,8 @@
-add lsChans()
-lsACL? lsBans? edit these?
--find out some way to use the DBus/ICE/RPC interface instead? then we can get rid of the restart
--- NOTE: Arch murmur package currently disables ice at compile-time. https://bugs.archlinux.org/task/55958
\ No newline at end of file
+-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
diff --git a/mumble/grpctest.py b/mumble/grpctest.py
new file mode 100755
index 0000000..2a58784
--- /dev/null
+++ b/mumble/grpctest.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python3
+
+import grpc
+from grpc.tools import protoc
+import tempfile
+
+conn = grpc.
diff --git a/mumble/sample.mumbleadmin.ini b/mumble/sample.mumbleadmin.ini
index 7824bc8..d4387ca 100644
--- a/mumble/sample.mumbleadmin.ini
+++ b/mumble/sample.mumbleadmin.ini
@@ -1,5 +1,8 @@
-[ICE]
+[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
@@ -7,6 +10,40 @@
# ::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
@@ -18,7 +55,7 @@ proto = tcp
# 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).
-slice = /usr/local/lib/optools/mumble/murmur.ice
+spec = /usr/local/lib/optools/mumble/murmur.ice
# The maximum size for ICE Messages (in KB)
# You're probably fine with the default.
@@ -26,52 +63,12 @@ max_size = 1024
[AUTH]
+# If both read and write are populated, write will be used preferentially.
# The Ice secret for read-only operations.
-# Set to a blank string if you want to only make a write-only connection.
+# Can be a blank string if you specify a write connection (see below).
read =
-# The Ice secret for write-only operations.
+# The Ice secret for read+write operations.
# Set to a blank string if you want to only make a read-only connection.
write =
-
-[TUNNEL]
-# NOTE: TO USE SSH TUNNELING, YOU MUST HAVE THE "sshtunnel" PYTHON MODULE INSTALLED.
-# If enabled, we will bind the remote port to the host and port given in the [ICE] section.
-# So you probably want to use localhost/127.0.0.1/::1 up there.
-
-# If this is enabled, we will try to initiate an SSH tunnel to the remote server,
-# and use the Ice interface through that. Probably only works with TCP Ice instances.
-# "enable" should be true or false. If blank, assume true. It's a VERY GOOD IDEA
-# to use this feature, as it greatly heightens the security.
-enable = true
-
-# The remote host to bind a port with. In most cases, this is going to be the host
-# that your Murmur instance is running on.
-host = your.murmur.server.tld
-
-# The remote user to auth as. If blank, use the current (local) username.
-user =
-
-# The port for SSH. In most cases, 22 is what you want. You can leave it blank,
-# we'll use the default in that case.
-port = 22
-
-# The authentication method. Currently supported methods are "key" and "passphrase".
-# Key is recommended (and the default). See:
-# https://sysadministrivia.com/news/hardening-ssh-security#auth_client
-# (and/or a multitude of other resources) on how to set up pubkey auth for SSH.
-auth = key
-
-# If "auth" is "password", enter the password here. If password auth is used
-# and no password is provided, you will be prompted to enter it.
-passphrase =
-
-# If "auth" is "key", enter the path to the *private* (not public) key here.
-# If none is provided, we'll use the default of ~/.ssh/id_rsa.
-# Note that if your key is password-protected, you should enable "key_passphrase".
-key = ~/.ssh/id_rsa
-
-# Should we (securely) prompt for a key_passphrase? This is REQUIRED if your key
-# is password-protected and you're using key authentication. Can be "true" or "false".
-key_passphrase = false
diff --git a/mumble/usrmgmt2.py b/mumble/usrmgmt2.py
index 629b999..d420300 100755
--- a/mumble/usrmgmt2.py
+++ b/mumble/usrmgmt2.py
@@ -25,7 +25,9 @@ class IceMgr(object):
if self.args['verbose']:
import pprint
self.getCfg()
- self.connect()
+ 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'])))
@@ -42,69 +44,13 @@ class IceMgr(object):
self.cfg[section][option] = _parser.get(section, option)
return()
- def sshTunnel(self):
- try:
- from sshtunnel import SSHTunnelForwarder,create_logger
- except ImportError:
- raise ImportError('You must install the sshtunnel Python module to use SSH tunneling!')
- import time
- _sshcfg = self.cfg['TUNNEL']
- # Do some munging to make this easier to deal with.
- if _sshcfg['user'] == '':
- _sshcfg['user'] = getpass.getuser()
- if _sshcfg['port'] == '':
- _sshcfg['port'] = 22
- else:
- _sshcfg['port'] = int(_sshcfg['port'])
- if _sshcfg['auth'].lower() == 'passphrase':
- if _sshcfg['passphrase'] == '':
- _sshcfg['passphrase'] = getpass.getpass(('What passphrase should ' +
- 'we use for {0}@{1}:{2}? (Will not ' +
- 'echo back.)\nPassphrase: ').format(
- _sshcfg['user'],
- _sshcfg['host'],
- _sshcfg['port'])).encode('utf-8')
- else:
- _sshcfg['passphrase'] = _sshcfg['passphrase'].encode('utf-8')
- _sshcfg['key'] = None
- else:
- if _sshcfg['key'] == '':
- _sshcfg['key'] = '~/.ssh/id_rsa'
- _key = os.path.abspath(os.path.expanduser(_sshcfg['key']))
- # We need to get the passphrase for the key, if it's set.
- if _sshcfg['key_passphrase'].lower() == 'true':
- _keypass = getpass.getpass(('What is the passphrase for {0}? ' +
- '(Will not be echoed back.)\nPassphrase: ').format(_key)).encode('utf-8')
- else:
- _keypass = None
- # To pring debug info, just add "logger=create_logger(loglevel=1)" to the params.
- self.ssh = SSHTunnelForwarder(_sshcfg['host'],
- ssh_pkey = _key,
- ssh_private_key_password = _keypass,
- ssh_username = _sshcfg['user'],
- ssh_port = _sshcfg['port'],
- local_bind_address = ('127.0.0.1', ),
- remote_bind_address = (self.cfg['ICE']['host'],
- int(self.cfg['ICE']['port'])),
- set_keepalive = 3.0)
- self.ssh.start()
- if self.args['verbose']:
- print('Configured tunneling for {0}:{1}({2}:{3}) => {4}:{5}'.format(
- _sshcfg['host'],
- _sshcfg['port'],
- self.cfg['ICE']['host'],
- self.cfg['ICE']['port'],
- self.ssh.local_bind_address[0],
- self.ssh.local_bind_address[1]))
- #self.cfg['ICE']['port'] = int(self.ssh.local_bind_ports[0])
- self.cfg['ICE']['port'] = int(self.ssh.local_bind_port)
- self.cfg['ICE']['host'] = self.ssh.local_bind_address[0]
- time.sleep(3)
- return()
-
- def connect(self):
- if self.cfg['TUNNEL']['enable'].lower() == 'true':
- self.sshTunnel()
+ 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'])}
@@ -155,7 +101,7 @@ class IceMgr(object):
_slicefile.close()
os.remove(_filepath)
else: # A .ice file was explicitly defined in the cfg
- _slicedir.append(os.path.join(os.path.abspath(os.path.expanduser(self.cfg['ICE']['slice']))))
+ _slicedir.append(self.cfg[ctxtype]['spec'])
Ice.loadSlice('', _slicedir)
import Murmur
self.conn = {}
diff --git a/net/addr/app/dnsinfo.py b/net/addr/app/dnsinfo.py
new file mode 100644
index 0000000..882fc4c
--- /dev/null
+++ b/net/addr/app/dnsinfo.py
@@ -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()
diff --git a/net/dhcp/dhcpcdump.py b/net/dhcp/dhcpcdump.py
new file mode 100755
index 0000000..e59083b
--- /dev/null
+++ b/net/dhcp/dhcpcdump.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+
+# 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()))
diff --git a/net/ssh/hostkeymanager/app/__init__.py b/net/ssh/hostkeymanager/app/__init__.py
new file mode 100644
index 0000000..4a49fb4
--- /dev/null
+++ b/net/ssh/hostkeymanager/app/__init__.py
@@ -0,0 +1,7 @@
+from flask import Flask
+
+app = Flask(__name__, instance_relative_config=True)
+
+from app import views
+
+app.config.from_object('config')
diff --git a/net/ssh/hostkeymanager/app/manage.py b/net/ssh/hostkeymanager/app/manage.py
new file mode 100755
index 0000000..6429ca8
--- /dev/null
+++ b/net/ssh/hostkeymanager/app/manage.py
@@ -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()
diff --git a/net/ssh/hostkeymanager/app/models.py b/net/ssh/hostkeymanager/app/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/net/ssh/hostkeymanager/app/templates/about.html b/net/ssh/hostkeymanager/app/templates/about.html
new file mode 100644
index 0000000..1fcb1cf
--- /dev/null
+++ b/net/ssh/hostkeymanager/app/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "base.html" %}{% block title %}r00t^2 SSH Key Repository || About{% endblock %}{% block body %}
+
About
+
This is a tool to deliver SSH public keys (or, optionally, host keys) to SSH's authentication system in a safe and secure manner.
Browser: {{ '{1}'.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)' }}
+
Language/Locale: {{ visitor['client']['language'] or '(N/A)' }}
+{%- set alt_os = alts[visitor['client']['os']] if visitor['client']['os'] in alts.keys() else '' %}
+
Operating System: {{ '{1}{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)' }}
+
User Agent: {{ visitor['client']['str'] }}
+
Version: {{ visitor['client']['version'] or '(N/A)' }}
+
+
+
Request Headers
+
These are headers sent along with the request your browser sends for the page's content.
+
+
+
+
Field
+
Value
+
{% for k in visitor['headers'].keys()|sort(case_sensitive = True) %}
+
+
{{ k }}
+
{{ visitor['headers'][k] if visitor['headers'][k] != '' else '(N/A)' }}
+
{% endfor %}
+
+
diff --git a/net/ssh/hostkeymanager/app/templates/index.html b/net/ssh/hostkeymanager/app/templates/index.html
new file mode 100644
index 0000000..b74b5ca
--- /dev/null
+++ b/net/ssh/hostkeymanager/app/templates/index.html
@@ -0,0 +1,6 @@
+{% extends "base.html" %}{% block title %}r00t^2 Client Info Revealer{% endblock %}{% block body %}
+
Client Info Revealer
+
A tool to reveal client-identifying data sent to webservers
+
+{% include 'html.html' if not params['json'] else 'json.html' %}
+{% endblock %}
diff --git a/net/ssh/hostkeymanager/app/templates/json.html b/net/ssh/hostkeymanager/app/templates/json.html
new file mode 100644
index 0000000..f216cfa
--- /dev/null
+++ b/net/ssh/hostkeymanager/app/templates/json.html
@@ -0,0 +1 @@
+
{{ json }}
diff --git a/net/ssh/hostkeymanager/app/templates/usage.html b/net/ssh/hostkeymanager/app/templates/usage.html
new file mode 100644
index 0000000..d9d2ec4
--- /dev/null
+++ b/net/ssh/hostkeymanager/app/templates/usage.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}{% block title %}r00t^2 Client Info Revealer || Usage{% endblock %}{% block body %}
+
Usage
+
Parameters
+
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.
+
Note: "Enabled" parameter values can be one of y, yes, 1, or true. "Disabled" parameter values can be one of n, no, 0, or false. The parameter names are case-sensitive but the values are not.
+
+
json: Force rendering in JSON format
+
+
It will display it nicely if you're in a browser, otherwise it will return raw/plaintext JSON.
+
Use raw if you want to force raw plaintext JSON output.
+
+
html: Force rendering in HTML
+
+
It will render HTML in clients that would normally render as JSON (e.g. curl, wget).
+
+
raw: Force output into a raw JSON string
+
+
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).
+
Overrides all other tags.
+
Has no effect for clients that would normally render as JSON (curl, wget, etc.).
+
+
tabs: Indentation for JSON output
+
+
Accepts a positive integer.
+
Default is 4 for "desktop" browsers (if json is enabled), and no indentation otherwise.
+
+
+
Examples
{% set scheme = 'https' if request.is_secure else 'http'%}
+