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.

+{% endblock %} diff --git a/net/ssh/hostkeymanager/app/templates/base.html b/net/ssh/hostkeymanager/app/templates/base.html new file mode 100644 index 0000000..5a5d7ab --- /dev/null +++ b/net/ssh/hostkeymanager/app/templates/base.html @@ -0,0 +1,35 @@ + + + + {% block title %}{% endblock %} + + + + + + + + + +
+
+ +
+ {% block body %}{% endblock %} + +
+ + + diff --git a/net/ssh/hostkeymanager/app/templates/html.html b/net/ssh/hostkeymanager/app/templates/html.html new file mode 100644 index 0000000..cfcd0a0 --- /dev/null +++ b/net/ssh/hostkeymanager/app/templates/html.html @@ -0,0 +1,38 @@ +

Client/Browser Information

+

This is information that your browser sends with its connection.

+

+

    +
  • Client IP: {{ visitor['ip'] }}
  • +
  • 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.

+

+ + + + + {% for k in visitor['headers'].keys()|sort(case_sensitive = True) %} + + + + {% endfor %} +
FieldValue
{{ k }}{{ visitor['headers'][k] if visitor['headers'][k] != '' else '(N/A)' }}
+

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'%} +

+ + + + + + + + + + + + + + + + + + + + +
URLBehavior
{{ scheme }}://{{ request.headers['host'] }}/Displays HTML and "Human" formatting if in a graphical browser, otherwise returns a raw, unformatted JSON string.
{{ scheme }}://{{ request.headers['host'] }}/?raw=1Renders a raw, unformatted JSON string if in a graphical browser, otherwise no effect. All other parameters ignored (if in a graphical browser).
{{ scheme }}://{{ request.headers['host'] }}/?html=1Forces HTML rendering on non-graphical clients.
{{ scheme }}://{{ request.headers['host'] }}/?json=1&tabs=4Returns 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").

+{% endblock %} diff --git a/net/ssh/hostkeymanager/app/views.py b/net/ssh/hostkeymanager/app/views.py new file mode 100644 index 0000000..2ffab44 --- /dev/null +++ b/net/ssh/hostkeymanager/app/views.py @@ -0,0 +1,57 @@ +import json +import re +from flask import render_template, make_response, request +from app import app + +@app.route('/', methods = ['GET']) #@app.route('/') +def index(): + hostkeys = None # TODO: hostkeys go here. dict? + # First we define interactive browsers + _intbrowsers = ['camino', 'chrome', 'firefox', 'galeon', + 'kmeleon', 'konqueror', 'links', 'lynx'] + # 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 + # 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 request.user_agent.browser in _intbrowsers: + 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 request.user_agent.browser in _intbrowsers or params['html']: + params['tabs'] = 4 + else: + params['tabs'] = None + j = json.dumps(hostkeys, indent = params['tabs']) + if (request.user_agent.browser in _intbrowsers and params['html'] and not params['raw']) or \ + (request.user_agent.browser not in _intbrowsers and params['html']): + return(render_template('index.html', hostkeys = hostkeys)) + 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')) diff --git a/net/ssh/hostkeymanager/config.py b/net/ssh/hostkeymanager/config.py new file mode 100644 index 0000000..299acdb --- /dev/null +++ b/net/ssh/hostkeymanager/config.py @@ -0,0 +1,8 @@ +# config.py + +# Flask debugging - DISABLE FOR PRODUCTION ENVIRONMENTS +#DEBUG = True +DEBUG = False + +# Path to your Sqlite3 DB +DB = '/var/local/db/optools/ssh_keys.sqlite3' diff --git a/net/ssh/hostkeymanager/run.py b/net/ssh/hostkeymanager/run.py new file mode 100644 index 0000000..3a43937 --- /dev/null +++ b/net/ssh/hostkeymanager/run.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == '__main__': + app.run() diff --git a/net/ssh/hostkeymanager/uwsgi.ini b/net/ssh/hostkeymanager/uwsgi.ini new file mode 100644 index 0000000..a351d83 --- /dev/null +++ b/net/ssh/hostkeymanager/uwsgi.ini @@ -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/ssh +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