diff --git a/apps/terminal/hands.py b/apps/terminal/hands.py index 38de26f22..065ff1a77 100644 --- a/apps/terminal/hands.py +++ b/apps/terminal/hands.py @@ -2,4 +2,5 @@ # -*- coding: utf-8 -*- # -from users.utils import ssh_key_gen +from users.utils import ssh_key_gen, check_user_is_valid + diff --git a/apps/terminal/keys/host_rsa_key b/apps/terminal/keys/host_rsa_key new file mode 100644 index 000000000..5b1aa9804 --- /dev/null +++ b/apps/terminal/keys/host_rsa_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAxreMFq9tp1hb2NOIkV7PqeyfS0GTJSfW2WcXuqkGPSVtPdYw +cJRY/s5eBn0KbO6JVj9yfwXNTKdnq1ODuuJDMnhTWXq1x7VICcou/69kFdSGiAzl +wfYP6LATEMCmpFRVi5UZip7SWopLE1JEw79nYWrbhUpDOpGTRKoIxoz+Uvg0h15G +pec3faL7PXj/J6j9pis44N1PCMBY3DVFPLopVcFpElbzwScNvZGBes90JoKLsqfD +Vjc9PUPjYjck9NFR6Xy0D5Gnw9MD5o0yK1l+3sibXjFBMOxA7aNxIlMhXQGol8R7 +3HBHWo/+Bwct3w1c8cPKfdPd8jGn3eWGYxupvQIDAQABAoIBADuyWDtYaDClsrHo +mlZRjUEW/KO3B2VaGoklF1PUAzPLUo4JEnQ/nJyvkj+QwNkIr+lhFhxiudIVWGd3 +p1M1Ncqrqx5uZr2gEAwg2Q2muwJz3hZxCXTDXvQgMRoPRgCH9UsBd7LVE4xvjy42 +wMGtdnkliNz5+khWA0/VZN2A7cYukrKzPwnhEMSrzYfnRwcOvp8pDp++Yjs3ZhQL +8+sgL1UDap5p5QZSQ98qJGNwmePAlTig+2Z5HvF+zussK2N7g5AcfghQFo5vCw/L +PXYtIfBH+Tv+6s7vMBMSLpbDcAZsxR9gDVUQi252Gu/nWClCzH3Kgu5ormHSOkYO +F6/n5AECgYEA69anuf52KWwYypVA73HiUbuzOdeuc1Br+s0uzOvpFX0HaqDxo8dm +N7FtUj/WnoqFivQrsrt4LpIzKn1XPNk7wMnwIZAQHNEI8sy7LBVh3RJOP2ZC2329 +ZHWxB3EVQ8Q5MbZy/AOn92UYwz8xIb0LweGYnHZlMp+xtOhdUR0/Z90CgYEA17R8 +EOeErksBRHotrEk0jLx+rrhK0JGcpXo/Dw6AOEp936DgHlqbkUURT2ejDOSQY/dN +7i4WeFJCVfFRNMbsitWxNmAdl3NJ5C2bV+7sz+oZfo5zP/e1RYCNLVjLxLYOHQ37 +GWwAlQr6fPcIZMCaPH+xq/0WSqcP96Lu6G0VG2ECgYB1XtcKkcFszAdqiu1OPXdN +BgUkfFqtuRCEOSlZgu71aswOHRslT09n2D13+Z1uObJMfUhiIzqkss4UD10jQ1mh +kN6ZVYEvVjkF3S4pulqCE2It207avbFMFeaMtZLHrxhnzU1cbtVhIkc4pHJnQBZh +30x8Uc/7ac6fIiWPAOdVYQKBgCi8rEWhA7zK64VcMa388VC29JHYukBjj5rs2GXm +ji6TWuxV/J2e7QxlZ9yALRntPJu0g+I8j//PQTnr5jM6ckfSDbLAOjZ1DnpqZpEX +zV+CzafKDVgCVxi2K3Np9qnC3C1+i3KEpCOBvEbHfK1Sdo6AazSZCpG0tV5GRipd +F4RhAoGAUJBoemipDjFoLSD3cpKpUXHIc6eieAI1GwYiL4CVugrvj5gO2B5c5yYb +3E8VWfuEHbBg0rmZIQ0sQf2ospZha7WBNhg9WB016aHyeZTIuHchfU4y3l2Jl8Re +enz4SSi6ZR6hgbJ9XzeiI+UTcDEuUzDUy9YktREuIBmMPXm7u5s= +-----END RSA PRIVATE KEY----- diff --git a/apps/terminal/server.py b/apps/terminal/server.py index dfcea1d21..a04e1f628 100644 --- a/apps/terminal/server.py +++ b/apps/terminal/server.py @@ -3,19 +3,6 @@ # import sys import os -import django - -BASE_DIR = os.path.dirname(__file__) -APP_DIR = os.path.abspath(os.path.dirname(BASE_DIR)) -sys.path.append(APP_DIR) - -os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' - -try: - django.setup() -except IndexError: - pass - import base64 from binascii import hexlify import sys @@ -30,14 +17,24 @@ import socket import select import errno import paramiko +import django from paramiko.py3compat import b, u, decodebytes -from .hands import ssh_key_gen +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) +APP_DIR = os.path.dirname(BASE_DIR) +sys.path.append(APP_DIR) +os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' +try: + django.setup() +except IndexError: + pass -paramiko.util.log_to_file('demo_server.log') +from django.conf import settings +from common.utils import get_logger +from hands import ssh_key_gen, check_user_is_valid -host_key = paramiko.RSAKey(filename='test_rsa.key') +logger = get_logger(__name__) class SSHService(paramiko.ServerInterface): @@ -46,30 +43,31 @@ class SSHService(paramiko.ServerInterface): # b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' # b'UWT10hcuO4Ks8=') # good_pub_key = paramiko.RSAKey(data=decodebytes(data)) + # host_key = paramiko.RSAKey(filename='test_rsa.key') - ssh_key_path = os.path.join(BASE_DIR, 'keys', 'ssh_host_key') - ssh_pub_key_path = ssh_key_path + '.pub' + host_key_path = os.path.join(BASE_DIR, 'keys', 'host_rsa_key') def __init__(self): self.event = threading.Event() + self.user = None + + @classmethod + def host_key(cls): + return cls.get_host_key() @classmethod def get_host_key(cls): - if os.path.isfile(cls.ssh_pub_key_path): - with open(cls.ssh_pub_key_path) as f: - ssh_pub_key = f.read() - else: - ssh_key, ssh_pub_key = cls.host_key_gen() - return ssh_pub_key + logger.debug("Get ssh server host key") + if not os.path.isfile(cls.host_key_path): + cls.host_key_gen() + return paramiko.RSAKey(filename=cls.host_key_path) @classmethod def host_key_gen(cls): + logger.debug("Generate ssh server host key") ssh_key, ssh_pub_key = ssh_key_gen() - with open(cls.ssh_key_path, 'w') as f: - with open(cls.ssh_pub_key_path, 'w') as f2: - f.write(ssh_key) - f2.write(ssh_pub_key) - return ssh_key, ssh_pub_key + with open(cls.host_key_path, 'w') as f: + f.write(ssh_key) def check_channel_request(self, kind, chanid): if kind == 'session': @@ -77,18 +75,30 @@ class SSHService(paramiko.ServerInterface): return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): - if (username == 'robey') and (password == 'foo'): + self.user = check_user_is_valid(username=username, password=password) + if self.user: + logger.info('User: %s password auth passed' % username) return paramiko.AUTH_SUCCESSFUL + else: + logger.warning('User: %s password auth failed' % username) return paramiko.AUTH_FAILED - def check_auth_publickey(self, username, key): - print('Auth attempt with key: ' + u(hexlify(key.get_fingerprint()))) - if (username == 'robey') and (key == self.good_pub_key): + def check_auth_publickey(self, username, public_key): + self.user = check_user_is_valid(username=username, public_key=public_key) + if self.user: + logger.info('User: %s public key auth passed' % username) return paramiko.AUTH_SUCCESSFUL + else: + logger.warning('User: %s public key auth failed' % username) return paramiko.AUTH_FAILED def get_allowed_auths(self, username): - return 'password,publickey' + auth_method_list = [] + if settings.CONFIG.SSH_PASSWORD_AUTH: + auth_method_list.append('password') + if settings.CONFIG.SSH_PUBLICK_KEY_AUTH: + auth_method_list.append('publickey') + return ','.join(auth_method_list) def check_channel_shell_request(self, channel): self.event.set() @@ -100,7 +110,7 @@ class SSHService(paramiko.ServerInterface): class SSHServer: - def __init__(self, host, port): + def __init__(self, host='127.0.0.1', port=2200): self.host = host self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -118,58 +128,57 @@ class SSHServer: return channel def handle_ssh_request(self, client, addr): - print('Got a connection!') + logger.info("Get connection from " + str(addr)) try: - t = paramiko.Transport(client, gss_kex=False) - t.set_gss_host(socket.getfqdn("")) + transport = paramiko.Transport(client, gss_kex=False) + transport.set_gss_host(socket.getfqdn("")) try: - t.load_server_moduli() + transport.load_server_moduli() except: - print('(Failed to load moduli -- gex will be unsupported.)') + logger.warning('(Failed to load moduli -- gex will be unsupported.)') raise - t.add_server_key(host_key) + + transport.add_server_key(SSHService.get_host_key()) service = SSHService() try: - t.start_server(server=service) + transport.start_server(server=service) except paramiko.SSHException: print('*** SSH negotiation failed.') return - chan = t.accept(20) - - if chan is None: + channel = transport.accept(20) + if channel is None: print('*** No channel.') return print('Authenticated!') - chan.settimeout(100) + channel.settimeout(100) - chan.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n') - chan.send('We are on fire all the time! Hooray! Candy corn for everyone!\r\n') - chan.send('Happy birthday to Robot Dave!\r\n\r\n') - server_chan = self.connect() + channel.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n') + channel.send('We are on fire all the time! Hooray! Candy corn for everyone!\r\n') + channel.send('Happy birthday to Robot Dave!\r\n\r\n') + server_channel = self.connect() if not service.event.is_set(): print('*** Client never asked for a shell.') return server_data = [] input_mode = True while True: - r, w, e = select.select([server_chan, chan], [], []) - + r, w, e = select.select([server_channel, channel], [], []) - if chan in r: - recv_data = chan.recv(1024).decode('utf8') + if channel in r: + recv_data = channel.recv(1024).decode('utf8') # print("From client: " + repr(recv_data)) if len(recv_data) == 0: break - server_chan.send(recv_data) + server_channel.send(recv_data) - if server_chan in r: - recv_data = server_chan.recv(1024).decode('utf8') + if server_channel in r: + recv_data = server_channel.recv(1024).decode('utf8') # print("From server: " + repr(recv_data)) if len(recv_data) == 0: break - chan.send(recv_data) + channel.send(recv_data) if len(recv_data) > 20: server_data.append('...') else: @@ -190,13 +199,14 @@ class SSHServer: print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) traceback.print_exc() try: - t.close() + transport.close() except: pass sys.exit(1) def listen(self): self.sock.listen(5) + print('Start ssh server %(host)s:%(port)s' % {'host': self.host, 'port': self.port}) while True: try: client, addr = self.sock.accept() @@ -209,7 +219,7 @@ class SSHServer: if __name__ == '__main__': - server = SSHServer('', 2200) + server = SSHServer(host='', port=2200) try: server.listen() except KeyboardInterrupt: diff --git a/apps/users/models.py b/apps/users/models.py index 200d3e038..f1a2e548b 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -109,6 +109,12 @@ class User(AbstractUser): else: return True + @property + def is_valid(self): + if self.is_active and not self.is_expired: + return True + return False + @property def private_key(self): return decrypt(self._private_key) diff --git a/apps/users/utils.py b/apps/users/utils.py index a94de773a..de21a1539 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -13,7 +13,8 @@ from django.utils.translation import ugettext as _ from paramiko.rsakey import RSAKey from common.tasks import send_mail_async -from common.utils import reverse +from common.utils import reverse, get_object_or_none +from .models import User try: @@ -203,3 +204,20 @@ def validate_ssh_pk(text): return optionState(text[1:]) return startState([n.strip() for n in text.splitlines()]) + + +def check_user_is_valid(**kwargs): + password = kwargs.pop('password', None) + public_key = kwargs.pop('public_key', None) + user = get_object_or_none(User, **kwargs) + + if password and not user.check_password(password): + user = None + + if public_key and not user.public_key == public_key: + user = None + + if user and user.is_valid: + return user + + return None diff --git a/config-example.py b/config-example.py index a1087a1aa..9bae73181 100644 --- a/config-example.py +++ b/config-example.py @@ -70,6 +70,10 @@ class Config: # EMAIL_USE_TLS = False # If port is 587, set True # EMAIL_SUBJECT_PREFIX = '[Jumpserver] ' + # SSH use password or public key for auth + SSH_PASSWORD_AUTH = False + SSH_PUBLIC_KEY_AUTH = True + def __init__(self): pass