From c7f3aaa654b04b4d1ee3fb90df44f17a558975c6 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 20 Sep 2016 23:34:37 +0800 Subject: [PATCH 01/30] Change appname webterminal to terminal --- apps/jumpserver/settings.py | 2 +- apps/jumpserver/urls.py | 2 +- apps/{webterminal => terminal}/__init__.py | 0 apps/{webterminal => terminal}/admin.py | 0 apps/{webterminal => terminal}/apps.py | 4 ++-- apps/{webterminal => terminal}/models.py | 0 apps/{webterminal => terminal}/templates/main.html | 0 apps/{webterminal => terminal}/tests.py | 0 apps/terminal/urls.py | 10 ++++++++++ apps/{webterminal => terminal}/views.py | 0 apps/webterminal/urls.py | 11 ----------- connect.py | 7 +++++++ server.py | 7 +++++++ 13 files changed, 28 insertions(+), 15 deletions(-) rename apps/{webterminal => terminal}/__init__.py (100%) rename apps/{webterminal => terminal}/admin.py (100%) rename apps/{webterminal => terminal}/apps.py (55%) rename apps/{webterminal => terminal}/models.py (100%) rename apps/{webterminal => terminal}/templates/main.html (100%) rename apps/{webterminal => terminal}/tests.py (100%) create mode 100644 apps/terminal/urls.py rename apps/{webterminal => terminal}/views.py (100%) delete mode 100644 apps/webterminal/urls.py create mode 100644 connect.py create mode 100644 server.py diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index cb6f7670c..b9ff532fe 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -54,7 +54,7 @@ INSTALLED_APPS = [ 'users.apps.UsersConfig', 'assets.apps.AssetsConfig', 'perms.apps.PermsConfig', - 'webterminal.apps.WebterminalConfig', + 'terminal.apps.TerminalConfig', 'ops.apps.OpsConfig', 'audits.apps.AuditsConfig', 'common.apps.CommonConfig', diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index e593e60a8..21b144da8 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -25,7 +25,7 @@ urlpatterns = [ url(r'^(api/)?users/', include('users.urls')), url(r'^assets/', include('assets.urls')), url(r'^perms/', include('perms.urls')), - url(r'^terminal/', include('webterminal.urls')), + url(r'^terminal/', include('terminal.urls')), ] diff --git a/apps/webterminal/__init__.py b/apps/terminal/__init__.py similarity index 100% rename from apps/webterminal/__init__.py rename to apps/terminal/__init__.py diff --git a/apps/webterminal/admin.py b/apps/terminal/admin.py similarity index 100% rename from apps/webterminal/admin.py rename to apps/terminal/admin.py diff --git a/apps/webterminal/apps.py b/apps/terminal/apps.py similarity index 55% rename from apps/webterminal/apps.py rename to apps/terminal/apps.py index 0e6932f9a..c81fa232b 100644 --- a/apps/webterminal/apps.py +++ b/apps/terminal/apps.py @@ -3,5 +3,5 @@ from __future__ import unicode_literals from django.apps import AppConfig -class WebterminalConfig(AppConfig): - name = 'webterminal' +class TerminalConfig(AppConfig): + name = 'terminal' diff --git a/apps/webterminal/models.py b/apps/terminal/models.py similarity index 100% rename from apps/webterminal/models.py rename to apps/terminal/models.py diff --git a/apps/webterminal/templates/main.html b/apps/terminal/templates/main.html similarity index 100% rename from apps/webterminal/templates/main.html rename to apps/terminal/templates/main.html diff --git a/apps/webterminal/tests.py b/apps/terminal/tests.py similarity index 100% rename from apps/webterminal/tests.py rename to apps/terminal/tests.py diff --git a/apps/terminal/urls.py b/apps/terminal/urls.py new file mode 100644 index 000000000..6afe754bc --- /dev/null +++ b/apps/terminal/urls.py @@ -0,0 +1,10 @@ +# coding:utf-8 +from django.conf.urls import url + +import views + +app_name = 'terminal' + +urlpatterns = [ + url(r'^web-terminal$', views.TerminalView.as_view(), name='web-terminal'), +] diff --git a/apps/webterminal/views.py b/apps/terminal/views.py similarity index 100% rename from apps/webterminal/views.py rename to apps/terminal/views.py diff --git a/apps/webterminal/urls.py b/apps/webterminal/urls.py deleted file mode 100644 index b8c6af98a..000000000 --- a/apps/webterminal/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -# coding:utf-8 -from django.conf.urls import url -from .views import * -from django.contrib import admin -admin.autodiscover() - -app_name = 'webterminal' - -urlpatterns = [ - url(r'^$', TerminalView.as_view(), name='webterminal'), -] \ No newline at end of file diff --git a/connect.py b/connect.py new file mode 100644 index 000000000..f93d0bec7 --- /dev/null +++ b/connect.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + + +if __name__ == '__main__': + pass diff --git a/server.py b/server.py new file mode 100644 index 000000000..f93d0bec7 --- /dev/null +++ b/server.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + + +if __name__ == '__main__': + pass From 771cf3994491b2172db5e07e7a415219e750eb88 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 21 Sep 2016 00:38:17 +0800 Subject: [PATCH 02/30] ssh server --- test_rsa.key | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test_rsa.key diff --git a/test_rsa.key b/test_rsa.key new file mode 100644 index 000000000..e69de29bb From e020aaa368ce582a61dedc1868e8bbd0668ab95b Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 21 Sep 2016 00:43:19 +0800 Subject: [PATCH 03/30] ssh server --- connect.py | 31 ++++++++++-- server.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++- test_rsa.key | 15 ++++++ 3 files changed, 177 insertions(+), 4 deletions(-) diff --git a/connect.py b/connect.py index f93d0bec7..51020d8f1 100644 --- a/connect.py +++ b/connect.py @@ -1,7 +1,32 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# +# + +import sys +import os + +# reload(sys) +# sys.setdefaultencoding('utf8') + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.join(BASE_DIR, 'apps')) + +import re +import time +import datetime +import textwrap +import getpass +import readline +import django +import paramiko +import errno +import pyte +import operator +import struct, fcntl, signal, socket, select +from io import open as copen +import uuid + + +os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' -if __name__ == '__main__': - pass diff --git a/server.py b/server.py index f93d0bec7..b2e902218 100644 --- a/server.py +++ b/server.py @@ -3,5 +3,138 @@ # +import base64 +from binascii import hexlify +import os +import socket +import sys +import threading +import traceback + +import paramiko +from paramiko.py3compat import b, u, decodebytes + + +paramiko.util.log_to_file('demo_server.log') + +host_key = paramiko.RSAKey(filename='test_rsa.key') + + +class Server(paramiko.ServerInterface): + # 'data' is the output of base64.encodestring(str(key)) + # (using the "user_rsa_key" files) + data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' + b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' + b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' + b'UWT10hcuO4Ks8=') + good_pub_key = paramiko.RSAKey(data=decodebytes(data)) + + def __init__(self): + self.event = threading.Event() + + def check_channel_request(self, kind, chanid): + if kind == 'session': + return paramiko.OPEN_SUCCEEDED + return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + + def check_auth_password(self, username, password): + print(username, password) + if (username == 'robey') and (password == 'foo'): + return paramiko.AUTH_SUCCESSFUL + 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): + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def get_allowed_auths(self, username): + return 'password,publickey' + + def check_channel_shell_request(self, channel): + self.event.set() + return True + + def check_channel_pty_request(self, channel, term, width, height, pixelwidth, + pixelheight, modes): + return True + + +def handle_ssh_request(client, addr): + print('Got a connection!') + + try: + t = paramiko.Transport(client, gss_kex=False) + t.set_gss_host(socket.getfqdn("")) + try: + t.load_server_moduli() + except: + print('(Failed to load moduli -- gex will be unsupported.)') + raise + t.add_server_key(host_key) + server = Server() + try: + t.start_server(server=server) + except paramiko.SSHException: + print('*** SSH negotiation failed.') + return + + while True: + # wait for auth + chan = t.accept(20) + if chan is None: + print('*** No channel.') + return + print('Authenticated!') + + server.event.wait(10) + if not server.event.is_set(): + print('*** Client never asked for a shell.') + return + + 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') + chan.send('Username: ') + f = chan.makefile('rU') + username = f.readline().strip('\r\n') + chan.send('\r\nI don\'t like you, ' + username + '.\r\n') + chan.close() + + except Exception as e: + print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) + traceback.print_exc() + try: + t.close() + except: + pass + sys.exit(1) + + +def run_server(): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('', 2200)) + except Exception as e: + print('*** Bind failed: ' + str(e)) + traceback.print_exc() + sys.exit(1) + + try: + sock.listen(100) + print('Listening for connection ...') + client, addr = sock.accept() + + t = threading.Thread(target=handle_ssh_request, args=(client, addr)) + t.start() + + except Exception as e: + print('*** Listen/accept failed: ' + str(e)) + traceback.print_exc() + sys.exit(1) + + if __name__ == '__main__': - pass + run_server() \ No newline at end of file diff --git a/test_rsa.key b/test_rsa.key index e69de29bb..f50e9c538 100644 --- a/test_rsa.key +++ b/test_rsa.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz +oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/ +d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB +gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0 +EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon +soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H +tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU +avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA +4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g +H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv +qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV +HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc +nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7 +-----END RSA PRIVATE KEY----- From db2d00f828b6fb42d774aca5ee3b187bdba2afdb Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 22 Sep 2016 00:37:13 +0800 Subject: [PATCH 04/30] implement a some server --- server.py | 147 +++++++++++++++++++++++++------------------------ test_server.py | 44 +++++++++++++++ 2 files changed, 119 insertions(+), 72 deletions(-) create mode 100644 test_server.py diff --git a/server.py b/server.py index b2e902218..d27a4f313 100644 --- a/server.py +++ b/server.py @@ -57,84 +57,87 @@ class Server(paramiko.ServerInterface): return True def check_channel_pty_request(self, channel, term, width, height, pixelwidth, - pixelheight, modes): + pixelheight, modes): return True -def handle_ssh_request(client, addr): - print('Got a connection!') +class SSHServer: + def __init__(self, host, port): + self.host = host + self.port = port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.bind((self.host, self.port)) - try: - t = paramiko.Transport(client, gss_kex=False) - t.set_gss_host(socket.getfqdn("")) - try: - t.load_server_moduli() - except: - print('(Failed to load moduli -- gex will be unsupported.)') - raise - t.add_server_key(host_key) - server = Server() - try: - t.start_server(server=server) - except paramiko.SSHException: - print('*** SSH negotiation failed.') - return + @staticmethod + def handle_ssh_request(client, addr): + print('Got a connection!') + try: + t = paramiko.Transport(client, gss_kex=False) + t.set_gss_host(socket.getfqdn("")) + try: + t.load_server_moduli() + except: + print('(Failed to load moduli -- gex will be unsupported.)') + raise + t.add_server_key(host_key) + server = Server() + server.add_prompt(">>") + try: + t.start_server(server=server) + except paramiko.SSHException: + print('*** SSH negotiation failed.') + return + + while True: + # wait for auth + chan = t.accept(20) + if chan is None: + print('*** No channel.') + return + print('Authenticated!') + + server.event.wait(10) + if not server.event.is_set(): + print('*** Client never asked for a shell.') + return + + 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') + chan.send('Username: ') + f = chan.makefile('rU') + username = f.readline().strip('\r\n') + chan.send('\r\nI don\'t like you, ' + username + '.\r\n') + chan.close() + + except Exception as e: + print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) + traceback.print_exc() + try: + t.close() + except: + pass + sys.exit(1) + + def listen(self): + self.sock.listen(5) while True: - # wait for auth - chan = t.accept(20) - if chan is None: - print('*** No channel.') - return - print('Authenticated!') - - server.event.wait(10) - if not server.event.is_set(): - print('*** Client never asked for a shell.') - return - - 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') - chan.send('Username: ') - f = chan.makefile('rU') - username = f.readline().strip('\r\n') - chan.send('\r\nI don\'t like you, ' + username + '.\r\n') - chan.close() - - except Exception as e: - print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) - traceback.print_exc() - try: - t.close() - except: - pass - sys.exit(1) - - -def run_server(): - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(('', 2200)) - except Exception as e: - print('*** Bind failed: ' + str(e)) - traceback.print_exc() - sys.exit(1) - - try: - sock.listen(100) - print('Listening for connection ...') - client, addr = sock.accept() - - t = threading.Thread(target=handle_ssh_request, args=(client, addr)) - t.start() - - except Exception as e: - print('*** Listen/accept failed: ' + str(e)) - traceback.print_exc() - sys.exit(1) + try: + client, addr = self.sock.accept() + print('Listening for connection ...') + threading.Thread(target=self.handle_ssh_request, args=(client, addr)).start() + except Exception as e: + print('*** Bind failed: ' + str(e)) + traceback.print_exc() + sys.exit(1) if __name__ == '__main__': - run_server() \ No newline at end of file + server = SSHServer('', 2200) + try: + server.listen() + except KeyboardInterrupt: + sys.exit(1) + diff --git a/test_server.py b/test_server.py new file mode 100644 index 000000000..b6314f2bc --- /dev/null +++ b/test_server.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +import socket +import sys +import threading + + +class ThreadSocket: + def __init__(self, host, port): + self.host = host + self.port = port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.bind((self.host, self.port)) + + def listen(self): + self.sock.listen(5) + while True: + client, address = self.sock.accept() + client.settimeout(60) + threading.Thread(target=self.handle_client_request, args=(client, address)).start() + + def handle_client_request(self, client, address): + print("Get client: %s" % str(address)) + while True: + try: + data = client.recv(1024) + print("sleep : %s" % str(address)) + if data: + client.send(data) + else: + raise IndexError('Client has disconnected') + except: + client.close() + + +if __name__ == '__main__': + server = ThreadSocket('', 9000) + try: + server.listen() + except KeyboardInterrupt: + sys.exit(1) From f946a4bfb3c84f2240bd45147102956814abe2a4 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 22 Sep 2016 23:26:44 +0800 Subject: [PATCH 05/30] finish example --- server.py | 75 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/server.py b/server.py index d27a4f313..0e348a0ec 100644 --- a/server.py +++ b/server.py @@ -6,10 +6,13 @@ import base64 from binascii import hexlify import os -import socket import sys import threading import traceback +import tty +import termios +import struct, fcntl, signal, socket, select +import errno import paramiko from paramiko.py3compat import b, u, decodebytes @@ -20,7 +23,7 @@ paramiko.util.log_to_file('demo_server.log') host_key = paramiko.RSAKey(filename='test_rsa.key') -class Server(paramiko.ServerInterface): +class SSHService(paramiko.ServerInterface): # 'data' is the output of base64.encodestring(str(key)) # (using the "user_rsa_key" files) data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' @@ -68,11 +71,19 @@ class SSHServer: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.host, self.port)) + self.server_ssh = None + self.server_chan = None - @staticmethod - def handle_ssh_request(client, addr): + def connect(self): + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname='127.0.0.1', port=22, username='root', password='redhat') + self.server_ssh = ssh + self.server_chan = channel = ssh.invoke_shell(term='xterm') + return channel + + def handle_ssh_request(self, client, addr): print('Got a connection!') - try: t = paramiko.Transport(client, gss_kex=False) t.set_gss_host(socket.getfqdn("")) @@ -82,35 +93,45 @@ class SSHServer: print('(Failed to load moduli -- gex will be unsupported.)') raise t.add_server_key(host_key) - server = Server() - server.add_prompt(">>") + service = SSHService() try: - t.start_server(server=server) + t.start_server(server=service) except paramiko.SSHException: print('*** SSH negotiation failed.') return + chan = t.accept(20) + + if chan is None: + print('*** No channel.') + return + print('Authenticated!') + + chan.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() + if not service.event.is_set(): + print('*** Client never asked for a shell.') + return while True: - # wait for auth - chan = t.accept(20) - if chan is None: - print('*** No channel.') - return - print('Authenticated!') + r, w, e = select.select([server_chan, chan], [], []) - server.event.wait(10) - if not server.event.is_set(): - print('*** Client never asked for a shell.') - return + if chan in r: + recv_data = chan.recv(1024).decode('utf8') + print("From client: " + repr(recv_data)) + if len(recv_data) == 0: + break + server_chan.send(recv_data) - 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') - chan.send('Username: ') - f = chan.makefile('rU') - username = f.readline().strip('\r\n') - chan.send('\r\nI don\'t like you, ' + username + '.\r\n') - chan.close() + if server_chan in r: + recv_data = server_chan.recv(1024).decode('utf8') + print("From server: " + repr(recv_data)) + if len(recv_data) == 0: + break + chan.send(recv_data) except Exception as e: print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) @@ -127,7 +148,7 @@ class SSHServer: try: client, addr = self.sock.accept() print('Listening for connection ...') - threading.Thread(target=self.handle_ssh_request, args=(client, addr)).start() + threading.Thread(target=self.handle_ssh_request, args=( client, addr)).start() except Exception as e: print('*** Bind failed: ' + str(e)) traceback.print_exc() From de0f8c24f7c33acf33256ecc4f6b34f96ecde487 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 22 Sep 2016 23:56:27 +0800 Subject: [PATCH 06/30] finish example --- server.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 0e348a0ec..c8e01d52f 100644 --- a/server.py +++ b/server.py @@ -116,22 +116,41 @@ class SSHServer: 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], [], []) + if chan in r: recv_data = chan.recv(1024).decode('utf8') - print("From client: " + repr(recv_data)) + # print("From client: " + repr(recv_data)) if len(recv_data) == 0: break server_chan.send(recv_data) if server_chan in r: recv_data = server_chan.recv(1024).decode('utf8') - print("From server: " + repr(recv_data)) + # print("From server: " + repr(recv_data)) if len(recv_data) == 0: break chan.send(recv_data) + if len(recv_data) > 20: + server_data.append('...') + else: + server_data.append(recv_data) + try: + if repr(server_data[-2]) == u'\r\n': + result = server_data.pop() + server_data.pop() + command = ''.join(server_data) + server_data = [] + print(">>> Command: %s" % command) + print(result) + except IndexError: + pass + print(server_data) + except Exception as e: print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) From ab18fe466bbbb84b3a0e6effcf7d5abcfc332b70 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sat, 24 Sep 2016 21:47:10 +0800 Subject: [PATCH 07/30] stash --- apps/terminal/hands.py | 5 +++ apps/terminal/keys/README.md | 0 server.py => apps/terminal/server.py | 60 ++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 apps/terminal/hands.py create mode 100644 apps/terminal/keys/README.md rename server.py => apps/terminal/server.py (79%) diff --git a/apps/terminal/hands.py b/apps/terminal/hands.py new file mode 100644 index 000000000..38de26f22 --- /dev/null +++ b/apps/terminal/hands.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +from users.utils import ssh_key_gen diff --git a/apps/terminal/keys/README.md b/apps/terminal/keys/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/server.py b/apps/terminal/server.py similarity index 79% rename from server.py rename to apps/terminal/server.py index c8e01d52f..dfcea1d21 100644 --- a/server.py +++ b/apps/terminal/server.py @@ -1,22 +1,39 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # +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 os import sys import threading import traceback import tty import termios -import struct, fcntl, signal, socket, select +import struct +import fcntl +import signal +import socket +import select import errno - import paramiko from paramiko.py3compat import b, u, decodebytes +from .hands import ssh_key_gen + paramiko.util.log_to_file('demo_server.log') @@ -24,24 +41,42 @@ host_key = paramiko.RSAKey(filename='test_rsa.key') class SSHService(paramiko.ServerInterface): - # 'data' is the output of base64.encodestring(str(key)) - # (using the "user_rsa_key" files) - data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' - b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' - b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' - b'UWT10hcuO4Ks8=') - good_pub_key = paramiko.RSAKey(data=decodebytes(data)) + # data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' + # b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' + # b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' + # b'UWT10hcuO4Ks8=') + # good_pub_key = paramiko.RSAKey(data=decodebytes(data)) + + ssh_key_path = os.path.join(BASE_DIR, 'keys', 'ssh_host_key') + ssh_pub_key_path = ssh_key_path + '.pub' def __init__(self): self.event = threading.Event() + @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 + + @classmethod + def host_key_gen(cls): + 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 + def check_channel_request(self, kind, chanid): if kind == 'session': return paramiko.OPEN_SUCCEEDED return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): - print(username, password) if (username == 'robey') and (password == 'foo'): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED @@ -151,7 +186,6 @@ class SSHServer: pass print(server_data) - except Exception as e: print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) traceback.print_exc() @@ -167,7 +201,7 @@ class SSHServer: try: client, addr = self.sock.accept() print('Listening for connection ...') - threading.Thread(target=self.handle_ssh_request, args=( client, addr)).start() + threading.Thread(target=self.handle_ssh_request, args=(client, addr)).start() except Exception as e: print('*** Bind failed: ' + str(e)) traceback.print_exc() From cfef3744548e357cf61464b4ed2b796a167e8871 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Sep 2016 00:11:31 +0800 Subject: [PATCH 08/30] Update ssh server --- apps/terminal/hands.py | 3 +- apps/terminal/keys/host_rsa_key | 27 +++++++ apps/terminal/server.py | 130 +++++++++++++++++--------------- apps/users/models.py | 6 ++ apps/users/utils.py | 20 ++++- config-example.py | 4 + 6 files changed, 128 insertions(+), 62 deletions(-) create mode 100644 apps/terminal/keys/host_rsa_key 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 From d3e9c8c9c096d4c741a09e7d7058d2f7650ff0e4 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Sep 2016 00:21:32 +0800 Subject: [PATCH 09/30] Replace ssh server dir --- .gitignore | 1 + apps/terminal/hands.py | 1 - apps/terminal/keys/README.md | 0 apps/terminal/keys/host_rsa_key | 27 ------------------------- ssh_server/__init__.py | 7 +++++++ {apps/terminal => ssh_server}/server.py | 10 +++++---- 6 files changed, 14 insertions(+), 32 deletions(-) delete mode 100644 apps/terminal/keys/README.md delete mode 100644 apps/terminal/keys/host_rsa_key create mode 100644 ssh_server/__init__.py rename {apps/terminal => ssh_server}/server.py (95%) diff --git a/.gitignore b/.gitignore index f8166b97a..38b5a999c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ db.sqlite3 config.py migrations/ *.log +host_rsa_key diff --git a/apps/terminal/hands.py b/apps/terminal/hands.py index 065ff1a77..0133d394f 100644 --- a/apps/terminal/hands.py +++ b/apps/terminal/hands.py @@ -2,5 +2,4 @@ # -*- coding: utf-8 -*- # -from users.utils import ssh_key_gen, check_user_is_valid diff --git a/apps/terminal/keys/README.md b/apps/terminal/keys/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/terminal/keys/host_rsa_key b/apps/terminal/keys/host_rsa_key deleted file mode 100644 index 5b1aa9804..000000000 --- a/apps/terminal/keys/host_rsa_key +++ /dev/null @@ -1,27 +0,0 @@ ------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/ssh_server/__init__.py b/ssh_server/__init__.py new file mode 100644 index 000000000..f93d0bec7 --- /dev/null +++ b/ssh_server/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + + +if __name__ == '__main__': + pass diff --git a/apps/terminal/server.py b/ssh_server/server.py similarity index 95% rename from apps/terminal/server.py rename to ssh_server/server.py index a04e1f628..e93e8761b 100644 --- a/apps/terminal/server.py +++ b/ssh_server/server.py @@ -21,7 +21,7 @@ import django from paramiko.py3compat import b, u, decodebytes BASE_DIR = os.path.abspath(os.path.dirname(__file__)) -APP_DIR = os.path.dirname(BASE_DIR) +APP_DIR = os.path.join(os.path.dirname(BASE_DIR), 'apps') sys.path.append(APP_DIR) os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' @@ -32,7 +32,7 @@ except IndexError: from django.conf import settings from common.utils import get_logger -from hands import ssh_key_gen, check_user_is_valid +from users.utils import ssh_key_gen, check_user_is_valid logger = get_logger(__name__) @@ -45,7 +45,7 @@ class SSHService(paramiko.ServerInterface): # good_pub_key = paramiko.RSAKey(data=decodebytes(data)) # host_key = paramiko.RSAKey(filename='test_rsa.key') - host_key_path = os.path.join(BASE_DIR, 'keys', 'host_rsa_key') + host_key_path = os.path.join(BASE_DIR, 'host_rsa_key') def __init__(self): self.event = threading.Event() @@ -211,7 +211,9 @@ class SSHServer: try: client, addr = self.sock.accept() print('Listening for connection ...') - threading.Thread(target=self.handle_ssh_request, args=(client, addr)).start() + t = threading.Thread(target=self.handle_ssh_request, args=(client, addr)) + t.daemon = True + t.start() except Exception as e: print('*** Bind failed: ' + str(e)) traceback.print_exc() From 216163f4367d77a2c95820cb81fda2ea186b58b5 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Sep 2016 11:30:02 +0800 Subject: [PATCH 10/30] stash --- config-example.py => config_example.py | 0 {ssh_server => terminal}/__init__.py | 0 terminal/config_example.py | 93 +++++++++++++++++++ terminal/logs/.gitkeep | 0 .../server.py => terminal/ssh_server.py | 17 +--- terminal/utils.py | 8 ++ terminal/web_server.py | 7 ++ 7 files changed, 113 insertions(+), 12 deletions(-) rename config-example.py => config_example.py (100%) rename {ssh_server => terminal}/__init__.py (100%) create mode 100644 terminal/config_example.py create mode 100644 terminal/logs/.gitkeep rename ssh_server/server.py => terminal/ssh_server.py (91%) create mode 100644 terminal/utils.py create mode 100644 terminal/web_server.py diff --git a/config-example.py b/config_example.py similarity index 100% rename from config-example.py rename to config_example.py diff --git a/ssh_server/__init__.py b/terminal/__init__.py similarity index 100% rename from ssh_server/__init__.py rename to terminal/__init__.py diff --git a/terminal/config_example.py b/terminal/config_example.py new file mode 100644 index 000000000..9cde69b99 --- /dev/null +++ b/terminal/config_example.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +import logging +import os + + +BASE_DIR = os.path.dirname(os.path.abspath(__name__)) +LOG_LEVEL_CHOICES = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL +} + + +class Config: + LOG_LEVEL = '' + LOG_DIR = os.path.join(BASE_DIR, 'logs') + LOGGING = { + 'version': 1, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'main': { + 'datefmt': '%Y-%m-%d %H:%M:%S', + 'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s', + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'logging.NullHandler', + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'main' + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'formatter': 'main', + 'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log') + }, + }, + 'loggers': { + 'django': { + 'handlers': ['null'], + 'propagate': False, + 'level': LOG_LEVEL, + }, + 'django.request': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.server': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'jumpserver': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + }, + 'jumpserver.users.api': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + }, + 'jumpserver.users.view': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + } + } + } + + def __init__(self): + pass + + def __getattr__(self, item): + return None + + + +if __name__ == '__main__': + pass diff --git a/terminal/logs/.gitkeep b/terminal/logs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ssh_server/server.py b/terminal/ssh_server.py similarity index 91% rename from ssh_server/server.py rename to terminal/ssh_server.py index e93e8761b..7d6a6dee3 100644 --- a/ssh_server/server.py +++ b/terminal/ssh_server.py @@ -37,14 +37,7 @@ from users.utils import ssh_key_gen, check_user_is_valid logger = get_logger(__name__) -class SSHService(paramiko.ServerInterface): - # data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' - # b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' - # b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' - # b'UWT10hcuO4Ks8=') - # good_pub_key = paramiko.RSAKey(data=decodebytes(data)) - # host_key = paramiko.RSAKey(filename='test_rsa.key') - +class SSHServerInterface(paramiko.ServerInterface): host_key_path = os.path.join(BASE_DIR, 'host_rsa_key') def __init__(self): @@ -138,10 +131,10 @@ class SSHServer: logger.warning('(Failed to load moduli -- gex will be unsupported.)') raise - transport.add_server_key(SSHService.get_host_key()) - service = SSHService() + transport.add_server_key(SSHServerInterface.get_host_key()) + ssh_interface = SSHServerInterface() try: - transport.start_server(server=service) + transport.start_server(server=ssh_interface) except paramiko.SSHException: print('*** SSH negotiation failed.') return @@ -158,7 +151,7 @@ class SSHServer: 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(): + if not ssh_interface.event.is_set(): print('*** Client never asked for a shell.') return server_data = [] diff --git a/terminal/utils.py b/terminal/utils.py new file mode 100644 index 000000000..f5c5d234f --- /dev/null +++ b/terminal/utils.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +import logging + + + diff --git a/terminal/web_server.py b/terminal/web_server.py new file mode 100644 index 000000000..f93d0bec7 --- /dev/null +++ b/terminal/web_server.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + + +if __name__ == '__main__': + pass From ebb30424fa88fc3b9a7c4be5790322ed279e7471 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Sep 2016 19:53:55 +0800 Subject: [PATCH 11/30] Use process except thread --- terminal/ssh_config.py | 105 ++++++++++++++ ...onfig_example.py => ssh_config_example.py} | 55 +++---- terminal/ssh_server.py | 134 +++++++++++------- terminal/utils.py | 9 ++ terminal/{web_server.py => web_ssh_server.py} | 0 5 files changed, 224 insertions(+), 79 deletions(-) create mode 100644 terminal/ssh_config.py rename terminal/{config_example.py => ssh_config_example.py} (66%) rename terminal/{web_server.py => web_ssh_server.py} (100%) diff --git a/terminal/ssh_config.py b/terminal/ssh_config.py new file mode 100644 index 000000000..52a0424c1 --- /dev/null +++ b/terminal/ssh_config.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +import logging +import os + + +BASE_DIR = os.path.dirname(os.path.abspath(__name__)) + + +class Config: + SSH_HOST = '' + SSH_PORT = 2200 + LOG_LEVEL = 'INFO' + LOG_DIR = os.path.join(BASE_DIR, 'logs') + LOG_FILENAME = 'ssh_server.log' + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'main': { + 'datefmt': '%Y-%m-%d %H:%M:%S', + 'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s', + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'logging.NullHandler', + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'main', + 'stream': 'ext://sys.stdout', + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'formatter': 'main', + 'mode': 'a', + 'filename': os.path.join(LOG_DIR, LOG_FILENAME), + }, + }, + 'loggers': { + 'jumpserver': { + 'handlers': ['console', 'file'], + # 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info') + 'level': LOG_LEVEL, + 'propagate': True, + }, + 'jumpserver.web_ssh_server': { + 'handlers': ['console', 'file'], + # 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info') + 'level': LOG_LEVEL, + 'propagate': True, + }, + 'jumpserver.ssh_server': { + 'handlers': ['console', 'file'], + # 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info') + 'level': LOG_LEVEL, + 'propagate': True, + } + } + } + + def __init__(self): + pass + + def __getattr__(self, item): + return None + + +class DevelopmentConfig(Config): + pass + + +class ProductionConfig(Config): + pass + + +class TestingConfig(Config): + pass + + +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig, + 'default': DevelopmentConfig, +} + +env = 'default' + + +if __name__ == '__main__': + pass + diff --git a/terminal/config_example.py b/terminal/ssh_config_example.py similarity index 66% rename from terminal/config_example.py rename to terminal/ssh_config_example.py index 9cde69b99..238304fcd 100644 --- a/terminal/config_example.py +++ b/terminal/ssh_config_example.py @@ -7,20 +7,14 @@ import os BASE_DIR = os.path.dirname(os.path.abspath(__name__)) -LOG_LEVEL_CHOICES = { - 'debug': logging.DEBUG, - 'info': logging.INFO, - 'warning': logging.WARNING, - 'error': logging.ERROR, - 'critical': logging.CRITICAL -} class Config: - LOG_LEVEL = '' + LOG_LEVEL = 'INFO' LOG_DIR = os.path.join(BASE_DIR, 'logs') LOGGING = { 'version': 1, + 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' @@ -47,35 +41,23 @@ class Config: 'level': 'DEBUG', 'class': 'logging.FileHandler', 'formatter': 'main', - 'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log') + 'filename': LOG_DIR, }, }, 'loggers': { - 'django': { - 'handlers': ['null'], - 'propagate': False, - 'level': LOG_LEVEL, - }, - 'django.request': { - 'handlers': ['console', 'file'], - 'level': LOG_LEVEL, - 'propagate': False, - }, - 'django.server': { - 'handlers': ['console', 'file'], - 'level': LOG_LEVEL, - 'propagate': False, - }, 'jumpserver': { 'handlers': ['console', 'file'], + # 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info') 'level': LOG_LEVEL, }, - 'jumpserver.users.api': { + 'jumpserver.web_ssh_server': { 'handlers': ['console', 'file'], + # 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info') 'level': LOG_LEVEL, }, - 'jumpserver.users.view': { + 'jumpserver.ssh_server': { 'handlers': ['console', 'file'], + # 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info') 'level': LOG_LEVEL, } } @@ -88,6 +70,27 @@ class Config: return None +class DevelopmentConfig(Config): + pass + + +class ProductionConfig(Config): + pass + + +class TestingConfig(Config): + pass + + +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig, + 'default': DevelopmentConfig, +} + +env = 'default' + if __name__ == '__main__': pass diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 7d6a6dee3..94a0b70e8 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -7,6 +7,7 @@ import base64 from binascii import hexlify import sys import threading +from multiprocessing import process import traceback import tty import termios @@ -31,17 +32,21 @@ except IndexError: pass from django.conf import settings -from common.utils import get_logger from users.utils import ssh_key_gen, check_user_is_valid +from utils import get_logger + logger = get_logger(__name__) class SSHServerInterface(paramiko.ServerInterface): host_key_path = os.path.join(BASE_DIR, 'host_rsa_key') + channel_pools = [] - def __init__(self): + def __init__(self, client, addr): self.event = threading.Event() + self.client = client + self.addr = addr self.user = None @classmethod @@ -70,19 +75,35 @@ class SSHServerInterface(paramiko.ServerInterface): def check_auth_password(self, username, password): self.user = check_user_is_valid(username=username, password=password) if self.user: - logger.info('User: %s password auth passed' % username) + logger.info('Accepted password for %(user)s from %(host)s port %(port)s ' % { + 'user': username, + 'host': self.addr[0], + 'port': self.addr[1], + }) return paramiko.AUTH_SUCCESSFUL else: - logger.warning('User: %s password auth failed' % username) + logger.info('Authentication password failed for %(user)s from %(host)s port %(port)s ' % { + 'user': username, + 'host': self.addr[0], + 'port': self.addr[1], + }) return paramiko.AUTH_FAILED 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) + logger.info('Accepted public key for %(user)s from %(host)s port %(port)s ' % { + 'user': username, + 'host': self.addr[0], + 'port': self.addr[1], + }) return paramiko.AUTH_SUCCESSFUL else: - logger.warning('User: %s public key auth failed' % username) + logger.info('Authentication public key failed for %(user)s from %(host)s port %(port)s ' % { + 'user': username, + 'host': self.addr[0], + 'port': self.addr[1], + }) return paramiko.AUTH_FAILED def get_allowed_auths(self, username): @@ -95,12 +116,20 @@ class SSHServerInterface(paramiko.ServerInterface): def check_channel_shell_request(self, channel): self.event.set() + self.__class__.channel_pools.append(channel) return True def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes): return True + def check_channel_window_change_request(self, channel, width, height, pixelwidth, pixelheight): + logger.info('Change window size %s * %s' % (width, height)) + logger.info('Change length %s ' % len(self.__class__.channel_pools)) + # for channel in self.__class__.channel_pools: + # channel.send("Hello world") + return True + class SSHServer: def __init__(self, host='127.0.0.1', port=2200): @@ -110,18 +139,22 @@ class SSHServer: self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.host, self.port)) self.server_ssh = None - self.server_chan = None + self.server_channel = None + self.client_channel = None def connect(self): ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(hostname='127.0.0.1', port=22, username='root', password='redhat') self.server_ssh = ssh - self.server_chan = channel = ssh.invoke_shell(term='xterm') + self.server_channel = channel = ssh.invoke_shell(term='xterm') return channel def handle_ssh_request(self, client, addr): - logger.info("Get connection from " + str(addr)) + logger.info("Get connection from %(host)s:%(port)s" % { + 'host': addr[0], + 'port': addr[1], + }) try: transport = paramiko.Transport(client, gss_kex=False) transport.set_gss_host(socket.getfqdn("")) @@ -132,70 +165,63 @@ class SSHServer: raise transport.add_server_key(SSHServerInterface.get_host_key()) - ssh_interface = SSHServerInterface() + ssh_interface = SSHServerInterface(client, addr) try: transport.start_server(server=ssh_interface) except paramiko.SSHException: print('*** SSH negotiation failed.') return - channel = transport.accept(20) - if channel is None: + self.client_channel = client_channel = transport.accept(20) + if client_channel is None: print('*** No channel.') return print('Authenticated!') - channel.settimeout(100) + client_channel.settimeout(100) - 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') + client_channel.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n') + client_channel.send('We are on fire all the time! Hooray! Candy corn for everyone!\r\n') + client_channel.send('Happy birthday to Robot Dave!\r\n\r\n') server_channel = self.connect() if not ssh_interface.event.is_set(): print('*** Client never asked for a shell.') return - server_data = [] - input_mode = True - while True: - r, w, e = select.select([server_channel, channel], [], []) - if channel in r: - recv_data = channel.recv(1024).decode('utf8') - # print("From client: " + repr(recv_data)) - if len(recv_data) == 0: + while True: + r, w, x = select.select([client_channel, server_channel], [], []) + + if client_channel in r: + data_client = client_channel.recv(1024) + logger.info(data_client) + if len(data_client) == 0: break - server_channel.send(recv_data) + # client_channel.send(data_client) + server_channel.send(data_client) if server_channel in r: - recv_data = server_channel.recv(1024).decode('utf8') - # print("From server: " + repr(recv_data)) - if len(recv_data) == 0: + data_server = server_channel.recv(1024) + if len(data_server) == 0: break - channel.send(recv_data) - if len(recv_data) > 20: - server_data.append('...') - else: - server_data.append(recv_data) - try: - if repr(server_data[-2]) == u'\r\n': - result = server_data.pop() - server_data.pop() - command = ''.join(server_data) - server_data = [] - print(">>> Command: %s" % command) - print(result) - except IndexError: - pass - print(server_data) + client_channel.send(data_server) - except Exception as e: - print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) - traceback.print_exc() - try: - transport.close() - except: - pass - sys.exit(1) + # if len(recv_data) > 20: + # server_data.append('...') + # else: + # server_data.append(recv_data) + # try: + # if repr(server_data[-2]) == u'\r\n': + # result = server_data.pop() + # server_data.pop() + # command = ''.join(server_data) + # server_data = [] + # except IndexError: + # pass + + except Exception: + client_channel.close() + server_channel.close() + logger.info('Close with server %s from %s' % ('127.0.0.1', '127.0.0.1')) def listen(self): self.sock.listen(5) @@ -204,7 +230,9 @@ class SSHServer: try: client, addr = self.sock.accept() print('Listening for connection ...') - t = threading.Thread(target=self.handle_ssh_request, args=(client, addr)) + # t = threading.Thread(target=self.handle_ssh_request, args=(client, addr)) + t = process.Process(target=self.handle_ssh_request, args=(client, addr)) + t.daemon = True t.start() except Exception as e: diff --git a/terminal/utils.py b/terminal/utils.py index f5c5d234f..1aba6ba02 100644 --- a/terminal/utils.py +++ b/terminal/utils.py @@ -3,6 +3,15 @@ # import logging +from logging.config import dictConfig +from ssh_config import config, env +CONFIG_SSH_SERVER = config.get(env) + + +def get_logger(name): + dictConfig(CONFIG_SSH_SERVER.LOGGING) + return logging.getLogger('jumpserver.%s' % name) + diff --git a/terminal/web_server.py b/terminal/web_ssh_server.py similarity index 100% rename from terminal/web_server.py rename to terminal/web_ssh_server.py From 5e33c2dc6b2b1cb0cfe278d50046a7e7b22a5605 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Sep 2016 20:41:56 +0800 Subject: [PATCH 12/30] Update ssh config --- terminal/ssh_config.py | 4 ---- terminal/ssh_server.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/terminal/ssh_config.py b/terminal/ssh_config.py index 52a0424c1..a23ebbce7 100644 --- a/terminal/ssh_config.py +++ b/terminal/ssh_config.py @@ -99,7 +99,3 @@ config = { env = 'default' - -if __name__ == '__main__': - pass - diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 94a0b70e8..c0f520f1f 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -126,8 +126,8 @@ class SSHServerInterface(paramiko.ServerInterface): def check_channel_window_change_request(self, channel, width, height, pixelwidth, pixelheight): logger.info('Change window size %s * %s' % (width, height)) logger.info('Change length %s ' % len(self.__class__.channel_pools)) - # for channel in self.__class__.channel_pools: - # channel.send("Hello world") + for channel in self.__class__.channel_pools: + channel.send("Hello world") return True From 2c64b784870da5a27c05d45d53d0c789a58e71cb Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Sep 2016 21:21:25 +0800 Subject: [PATCH 13/30] Update terminal --- terminal/ssh_server.py | 57 ++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index c0f520f1f..c70152173 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -124,13 +124,42 @@ class SSHServerInterface(paramiko.ServerInterface): return True def check_channel_window_change_request(self, channel, width, height, pixelwidth, pixelheight): - logger.info('Change window size %s * %s' % (width, height)) - logger.info('Change length %s ' % len(self.__class__.channel_pools)) - for channel in self.__class__.channel_pools: - channel.send("Hello world") return True +class BackendServer: + def __init__(self, host, port, username): + self.host = host + self.port = port + self.username = username + self.ssh = None + self.channel = None + + def connect(self, term='xterm', width=80, height=24): + self.ssh = ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname=self.host, port=self.port, username=self.username, password=self.host_password, + pkey=self.host_private_key, look_for_keys=False, allow_agent=True, compress=True) + self.channel = channel = ssh.invoke_shell(term=term, width=width, height=height) + return channel + + @property + def host_password(self): + return 'redhat' + + @property + def host_private_key(self): + return 'redhat' + + +class Navigation: + def __init__(self, username): + self.username = username + + def display(self): + pass + + class SSHServer: def __init__(self, host='127.0.0.1', port=2200): self.host = host @@ -142,13 +171,14 @@ class SSHServer: self.server_channel = None self.client_channel = None - def connect(self): - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(hostname='127.0.0.1', port=22, username='root', password='redhat') - self.server_ssh = ssh - self.server_channel = channel = ssh.invoke_shell(term='xterm') - return channel + def invoke_with_backend(self): + pass + + def display_navigation(self): + pass + + def make_client_channel(self): + pass def handle_ssh_request(self, client, addr): logger.info("Get connection from %(host)s:%(port)s" % { @@ -173,6 +203,8 @@ class SSHServer: return self.client_channel = client_channel = transport.accept(20) + # self.client_channel = client_channel = transport.open_session() + # client_channel.get_pty(term='xterm') if client_channel is None: print('*** No channel.') return @@ -219,9 +251,8 @@ class SSHServer: # pass except Exception: - client_channel.close() - server_channel.close() logger.info('Close with server %s from %s' % ('127.0.0.1', '127.0.0.1')) + sys.exit(100) def listen(self): self.sock.listen(5) From 4b7419559c7b387bddf79ee540ec32dc0dd9feb5 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Sep 2016 23:11:09 +0800 Subject: [PATCH 14/30] Update ssh_server to some class --- terminal/ssh_server.py | 196 ++++++++++++++++++++++++----------------- terminal/utils.py | 2 + 2 files changed, 119 insertions(+), 79 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index c70152173..0b8261963 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -1,9 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# +# + +__version__ = '0.3.3' + import sys import os import base64 +import time from binascii import hexlify import sys import threading @@ -19,7 +23,6 @@ import select import errno import paramiko import django -from paramiko.py3compat import b, u, decodebytes BASE_DIR = os.path.abspath(os.path.dirname(__file__)) APP_DIR = os.path.join(os.path.dirname(BASE_DIR), 'apps') @@ -33,13 +36,13 @@ except IndexError: from django.conf import settings from users.utils import ssh_key_gen, check_user_is_valid -from utils import get_logger +from utils import get_logger, SSHServerException logger = get_logger(__name__) -class SSHServerInterface(paramiko.ServerInterface): +class SSHServer(paramiko.ServerInterface): host_key_path = os.path.join(BASE_DIR, 'host_rsa_key') channel_pools = [] @@ -47,7 +50,10 @@ class SSHServerInterface(paramiko.ServerInterface): self.event = threading.Event() self.client = client self.addr = addr + self.username = None self.user = None + self.channel_width = None + self.channel_height = None @classmethod def host_key(cls): @@ -73,34 +79,37 @@ class SSHServerInterface(paramiko.ServerInterface): return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): - self.user = check_user_is_valid(username=username, password=password) + self.user = user = check_user_is_valid(username=username, password=password) + self.username = username = user.username if self.user: - logger.info('Accepted password for %(user)s from %(host)s port %(port)s ' % { - 'user': username, + logger.info('Accepted password for %(username)s from %(host)s port %(port)s ' % { + 'username': username, 'host': self.addr[0], 'port': self.addr[1], }) return paramiko.AUTH_SUCCESSFUL else: - logger.info('Authentication password failed for %(user)s from %(host)s port %(port)s ' % { - 'user': username, + logger.info('Authentication password failed for %(username)s from %(host)s port %(port)s ' % { + 'username': username, 'host': self.addr[0], 'port': self.addr[1], }) return paramiko.AUTH_FAILED def check_auth_publickey(self, username, public_key): - self.user = check_user_is_valid(username=username, public_key=public_key) + self.user = user = check_user_is_valid(username=username, public_key=public_key) + self.username = username = user.username + if self.user: - logger.info('Accepted public key for %(user)s from %(host)s port %(port)s ' % { - 'user': username, + logger.info('Accepted public key for %(username)s from %(host)s port %(port)s ' % { + 'username': username, 'host': self.addr[0], 'port': self.addr[1], }) return paramiko.AUTH_SUCCESSFUL else: - logger.info('Authentication public key failed for %(user)s from %(host)s port %(port)s ' % { - 'user': username, + logger.info('Authentication public key failed for %(username)s from %(host)s port %(port)s ' % { + 'username': username, 'host': self.addr[0], 'port': self.addr[1], }) @@ -135,12 +144,18 @@ class BackendServer: self.ssh = None self.channel = None - def connect(self, term='xterm', width=80, height=24): + def connect(self, term='xterm', width=80, height=24, timeout=10): self.ssh = ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(hostname=self.host, port=self.port, username=self.username, password=self.host_password, - pkey=self.host_private_key, look_for_keys=False, allow_agent=True, compress=True) + pkey=self.host_private_key, look_for_keys=False, allow_agent=True, compress=True, timeout=timeout) self.channel = channel = ssh.invoke_shell(term=term, width=width, height=height) + logger.info('Connect %(username)s@%(host)s:%(port)s successfully' % { + 'username': self.username, + 'host': self.host, + 'port': self.port, + }) + channel.settimeout(100) return channel @property @@ -149,90 +164,108 @@ class BackendServer: @property def host_private_key(self): - return 'redhat' + return None class Navigation: - def __init__(self, username): + def __init__(self, username, client_channel): self.username = username + self.client_channel = client_channel + + def display_banner(self): + client_channel = self.client_channel + client_channel.send('\r\n\r\n\t\tWelcome to use Jumpserver open source system !\r\n\r\n') + client_channel.send('If use find some bug please contact us \r\n') + # client_channel.send(self.username) def display(self): + self.display_banner() + + def return_to_connect(self): pass -class SSHServer: - 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) - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.sock.bind((self.host, self.port)) - self.server_ssh = None - self.server_channel = None +class JumpServer: + def __init__(self): + self.listen_host = '0.0.0.0' + self.listen_port = 2222 + self.username = None + self.backend_host = None + self.backend_port = None + self.backend_username = None + self.backend_channel = None self.client_channel = None + self.sock = None - def invoke_with_backend(self): - pass + def display_navigation(self, username, client_channel): + nav = Navigation(username, client_channel) + nav.display() + return '127.0.0.1', 22, 'root' - def display_navigation(self): - pass + def get_client_channel(self, client, addr): + transport = paramiko.Transport(client, gss_kex=False) + transport.set_gss_host(socket.getfqdn("")) + try: + transport.load_server_moduli() + except: + logger.warning('Failed to load moduli -- gex will be unsupported.') + raise - def make_client_channel(self): - pass + transport.add_server_key(SSHServer.get_host_key()) + ssh_server = SSHServer(client, addr) + self.username = ssh_server.username + + try: + transport.start_server(server=ssh_server) + except paramiko.SSHException: + logger.warning('SSH negotiation failed.') + + self.client_channel = client_channel = transport.accept(20) + if client_channel is None: + logger.warning('No channel get.') + raise SSHServerException('No channel get.') + + if not ssh_server.event.is_set(): + logger.warning('Client never asked for a shell.') + raise SSHServerException('Client never asked for a shell.') + return client_channel + + def get_backend_channel(self, host, port, username): + backend_server = BackendServer(host, port, username) + self.backend_channel = backend_channel = backend_server.connect() + if not backend_channel: + logger.warning('Connect %(username)s@%(host)s:%(port)s failed' % { + 'username': username, + 'host': host, + 'port': port, + }) + + return backend_channel def handle_ssh_request(self, client, addr): - logger.info("Get connection from %(host)s:%(port)s" % { + logger.info("Get ssh request from %(host)s:%(port)s" % { 'host': addr[0], 'port': addr[1], }) try: - transport = paramiko.Transport(client, gss_kex=False) - transport.set_gss_host(socket.getfqdn("")) - try: - transport.load_server_moduli() - except: - logger.warning('(Failed to load moduli -- gex will be unsupported.)') - raise + client_channel = self.get_client_channel(client, addr) + host, port, username = self.display_navigation(self.username, client_channel) + backend_channel = self.get_backend_channel(host, port, username) - transport.add_server_key(SSHServerInterface.get_host_key()) - ssh_interface = SSHServerInterface(client, addr) - try: - transport.start_server(server=ssh_interface) - except paramiko.SSHException: - print('*** SSH negotiation failed.') - return - - self.client_channel = client_channel = transport.accept(20) - # self.client_channel = client_channel = transport.open_session() - # client_channel.get_pty(term='xterm') - if client_channel is None: - print('*** No channel.') - return - print('Authenticated!') - - client_channel.settimeout(100) - - client_channel.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n') - client_channel.send('We are on fire all the time! Hooray! Candy corn for everyone!\r\n') - client_channel.send('Happy birthday to Robot Dave!\r\n\r\n') - server_channel = self.connect() - if not ssh_interface.event.is_set(): - print('*** Client never asked for a shell.') - return + print(client_channel.get_id(), backend_channel.get_id()) while True: - r, w, x = select.select([client_channel, server_channel], [], []) + r, w, x = select.select([client_channel, backend_channel], [], []) if client_channel in r: data_client = client_channel.recv(1024) logger.info(data_client) if len(data_client) == 0: break - # client_channel.send(data_client) - server_channel.send(data_client) + backend_channel.send(data_client) - if server_channel in r: - data_server = server_channel.recv(1024) + if backend_channel in r: + data_server = backend_channel.recv(1024) if len(data_server) == 0: break client_channel.send(data_server) @@ -250,30 +283,35 @@ class SSHServer: # except IndexError: # pass - except Exception: + except IndexError: logger.info('Close with server %s from %s' % ('127.0.0.1', '127.0.0.1')) sys.exit(100) def listen(self): - self.sock.listen(5) - print('Start ssh server %(host)s:%(port)s' % {'host': self.host, 'port': self.port}) + self.sock = sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((self.listen_host, self.listen_port)) + sock.listen(5) + + print(time.ctime()) + print('Jumpserver version %s, more see https://www.jumpserver.org' % __version__) + print('Starting ssh server at %(host)s:%(port)s' % {'host': self.listen_host, 'port': self.listen_port}) + print('Quit the server with CONTROL-C.') + while True: try: client, addr = self.sock.accept() - print('Listening for connection ...') - # t = threading.Thread(target=self.handle_ssh_request, args=(client, addr)) t = process.Process(target=self.handle_ssh_request, args=(client, addr)) - t.daemon = True t.start() except Exception as e: - print('*** Bind failed: ' + str(e)) + logger.error('Bind failed: ' + str(e)) traceback.print_exc() sys.exit(1) if __name__ == '__main__': - server = SSHServer(host='', port=2200) + server = JumpServer() try: server.listen() except KeyboardInterrupt: diff --git a/terminal/utils.py b/terminal/utils.py index 1aba6ba02..da648e05f 100644 --- a/terminal/utils.py +++ b/terminal/utils.py @@ -15,3 +15,5 @@ def get_logger(name): return logging.getLogger('jumpserver.%s' % name) +class SSHServerException(Exception): + pass From e627b14e55ede827c20efdb6964b16fce001c2b3 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Sep 2016 23:38:42 +0800 Subject: [PATCH 15/30] finish ssh server use more class --- terminal/ssh_server.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 0b8261963..0105d4cf4 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -186,15 +186,13 @@ class Navigation: class JumpServer: + backend_server_pools = [] + backend_channel_pools = [] + client_channel_pools = [] + def __init__(self): self.listen_host = '0.0.0.0' self.listen_port = 2222 - self.username = None - self.backend_host = None - self.backend_port = None - self.backend_username = None - self.backend_channel = None - self.client_channel = None self.sock = None def display_navigation(self, username, client_channel): @@ -220,7 +218,8 @@ class JumpServer: except paramiko.SSHException: logger.warning('SSH negotiation failed.') - self.client_channel = client_channel = transport.accept(20) + client_channel = transport.accept(20) + self.__class__.client_channel_pools.append(client_channel) if client_channel is None: logger.warning('No channel get.') raise SSHServerException('No channel get.') @@ -232,7 +231,9 @@ class JumpServer: def get_backend_channel(self, host, port, username): backend_server = BackendServer(host, port, username) - self.backend_channel = backend_channel = backend_server.connect() + backend_channel = backend_server.connect() + self.__class__.backend_server_pools.append(backend_server) + self.__class__.backend_channel_pools.append(backend_channel) if not backend_channel: logger.warning('Connect %(username)s@%(host)s:%(port)s failed' % { 'username': username, @@ -249,26 +250,23 @@ class JumpServer: }) try: client_channel = self.get_client_channel(client, addr) - host, port, username = self.display_navigation(self.username, client_channel) + host, port, username = self.display_navigation('root', client_channel) backend_channel = self.get_backend_channel(host, port, username) - print(client_channel.get_id(), backend_channel.get_id()) - while True: r, w, x = select.select([client_channel, backend_channel], [], []) if client_channel in r: - data_client = client_channel.recv(1024) - logger.info(data_client) - if len(data_client) == 0: + client_data = client_channel.recv(1024) + if len(client_data) == 0: break - backend_channel.send(data_client) + backend_channel.send(client_data) if backend_channel in r: - data_server = backend_channel.recv(1024) - if len(data_server) == 0: + backend_data = backend_channel.recv(1024) + if len(backend_data) == 0: break - client_channel.send(data_server) + client_channel.send(backend_data) # if len(recv_data) > 20: # server_data.append('...') @@ -301,7 +299,7 @@ class JumpServer: while True: try: client, addr = self.sock.accept() - t = process.Process(target=self.handle_ssh_request, args=(client, addr)) + t = threading.Thread(target=self.handle_ssh_request, args=(client, addr)) t.daemon = True t.start() except Exception as e: From badd319bb49de2caa14fcfe0cd65bb8bd1c8ee88 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 26 Sep 2016 00:05:23 +0800 Subject: [PATCH 16/30] Modify some bug and add some logging --- terminal/ssh_server.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 0105d4cf4..583d89095 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -11,7 +11,7 @@ import time from binascii import hexlify import sys import threading -from multiprocessing import process +from multiprocessing.process import Process import traceback import tty import termios @@ -126,6 +126,9 @@ class SSHServer(paramiko.ServerInterface): def check_channel_shell_request(self, channel): self.event.set() self.__class__.channel_pools.append(channel) + channel.username = self.username + channel.addr = self.addr + return True def check_channel_pty_request(self, channel, term, width, height, pixelwidth, @@ -150,12 +153,15 @@ class BackendServer: ssh.connect(hostname=self.host, port=self.port, username=self.username, password=self.host_password, pkey=self.host_private_key, look_for_keys=False, allow_agent=True, compress=True, timeout=timeout) self.channel = channel = ssh.invoke_shell(term=term, width=width, height=height) - logger.info('Connect %(username)s@%(host)s:%(port)s successfully' % { + logger.info('Connect backend server %(username)s@%(host)s:%(port)s successfully' % { 'username': self.username, 'host': self.host, 'port': self.port, }) channel.settimeout(100) + channel.host = self.host + channel.port = self.port + channel.username = self.username return channel @property @@ -175,7 +181,8 @@ class Navigation: def display_banner(self): client_channel = self.client_channel client_channel.send('\r\n\r\n\t\tWelcome to use Jumpserver open source system !\r\n\r\n') - client_channel.send('If use find some bug please contact us \r\n') + client_channel.send('If you find some bug please contact us \r\n') + client_channel.send('See more at https://www.jumpserver.org\r\n') # client_channel.send(self.username) def display(self): @@ -211,7 +218,6 @@ class JumpServer: transport.add_server_key(SSHServer.get_host_key()) ssh_server = SSHServer(client, addr) - self.username = ssh_server.username try: transport.start_server(server=ssh_server) @@ -259,12 +265,20 @@ class JumpServer: if client_channel in r: client_data = client_channel.recv(1024) if len(client_data) == 0: + logger.info('Logout from ssh server %(host)s: %(username)s' % { + 'host': addr[0], + 'username': client_channel.username, + }) break backend_channel.send(client_data) if backend_channel in r: backend_data = backend_channel.recv(1024) if len(backend_data) == 0: + logger.info('Logout from backend server %(host)s: %(username)s' % { + 'host': backend_channel.host, + 'username': backend_channel.username, + }) break client_channel.send(backend_data) @@ -281,8 +295,9 @@ class JumpServer: # except IndexError: # pass + # Todo: catch other exception except IndexError: - logger.info('Close with server %s from %s' % ('127.0.0.1', '127.0.0.1')) + logger.info('Close with server %s from %s' % (addr[0], addr[1])) sys.exit(100) def listen(self): @@ -299,9 +314,9 @@ class JumpServer: while True: try: client, addr = self.sock.accept() - t = threading.Thread(target=self.handle_ssh_request, args=(client, addr)) - t.daemon = True - t.start() + process = Process(target=self.handle_ssh_request, args=(client, addr)) + process.daemon = True + process.start() except Exception as e: logger.error('Bind failed: ' + str(e)) traceback.print_exc() From be1a374b1466e1680d399a1ec3da17830049174e Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 26 Sep 2016 22:16:21 +0800 Subject: [PATCH 17/30] Modify log --- terminal/ssh_config.py | 5 +++-- terminal/ssh_server.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/terminal/ssh_config.py b/terminal/ssh_config.py index a23ebbce7..910f7d70d 100644 --- a/terminal/ssh_config.py +++ b/terminal/ssh_config.py @@ -43,10 +43,11 @@ class Config: }, 'file': { 'level': 'DEBUG', - 'class': 'logging.FileHandler', + 'class': 'logging.handlers.TimedRotatingFileHandler', 'formatter': 'main', - 'mode': 'a', 'filename': os.path.join(LOG_DIR, LOG_FILENAME), + 'when': 'D', + 'backupCount': 10, }, }, 'loggers': { diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 583d89095..176c31b1c 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -275,6 +275,8 @@ class JumpServer: if backend_channel in r: backend_data = backend_channel.recv(1024) if len(backend_data) == 0: + client_channel.send('Disconnect from %s \r\n' % backend_channel.host) + client_channel.close() logger.info('Logout from backend server %(host)s: %(username)s' % { 'host': backend_channel.host, 'username': backend_channel.username, From b17a12662c3a6e4eb3157ad1d1e1906ae8773f12 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 27 Sep 2016 00:10:14 +0800 Subject: [PATCH 18/30] modify change windows size --- terminal/ssh_server.py | 27 +++++++++++++++++++++++---- terminal/utils.py | 15 +++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 176c31b1c..e8e8f60c8 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -36,7 +36,7 @@ except IndexError: from django.conf import settings from users.utils import ssh_key_gen, check_user_is_valid -from utils import get_logger, SSHServerException +from utils import get_logger, SSHServerException, control_char logger = get_logger(__name__) @@ -48,6 +48,7 @@ class SSHServer(paramiko.ServerInterface): def __init__(self, client, addr): self.event = threading.Event() + self.change_window_size_event = threading.Event() self.client = client self.addr = addr self.username = None @@ -133,9 +134,15 @@ class SSHServer(paramiko.ServerInterface): def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes): + channel.change_window_size_event = threading.Event() + channel.width = width + channel.height = height return True def check_channel_window_change_request(self, channel, width, height, pixelwidth, pixelheight): + channel.change_window_size_event.set() + channel.width = width + channel.height = height return True @@ -180,6 +187,7 @@ class Navigation: def display_banner(self): client_channel = self.client_channel + client_channel.send(control_char.clear) client_channel.send('\r\n\r\n\t\tWelcome to use Jumpserver open source system !\r\n\r\n') client_channel.send('If you find some bug please contact us \r\n') client_channel.send('See more at https://www.jumpserver.org\r\n') @@ -197,6 +205,10 @@ class JumpServer: backend_channel_pools = [] client_channel_pools = [] + CONTROL_CHAR = { + 'clear': '' + } + def __init__(self): self.listen_host = '0.0.0.0' self.listen_port = 2222 @@ -235,9 +247,9 @@ class JumpServer: raise SSHServerException('Client never asked for a shell.') return client_channel - def get_backend_channel(self, host, port, username): + def get_backend_channel(self, host, port, username, term='xterm', width=80, height=24): backend_server = BackendServer(host, port, username) - backend_channel = backend_server.connect() + backend_channel = backend_server.connect(term=term, width=width, height=height) self.__class__.backend_server_pools.append(backend_server) self.__class__.backend_channel_pools.append(backend_channel) if not backend_channel: @@ -257,11 +269,16 @@ class JumpServer: try: client_channel = self.get_client_channel(client, addr) host, port, username = self.display_navigation('root', client_channel) - backend_channel = self.get_backend_channel(host, port, username) + backend_channel = self.get_backend_channel(host, port, username, + width=client_channel.width, + height=client_channel.height) while True: r, w, x = select.select([client_channel, backend_channel], [], []) + if client_channel.change_window_size_event.is_set(): + backend_channel.resize_pty(width=client_channel.width, height=client_channel.height) + if client_channel in r: client_data = client_channel.recv(1024) if len(client_data) == 0: @@ -270,6 +287,7 @@ class JumpServer: 'username': client_channel.username, }) break + print('CC: ' + repr(client_data)) backend_channel.send(client_data) if backend_channel in r: @@ -282,6 +300,7 @@ class JumpServer: 'username': backend_channel.username, }) break + print('SS: ' + repr(backend_data)) client_channel.send(backend_data) # if len(recv_data) > 20: diff --git a/terminal/utils.py b/terminal/utils.py index da648e05f..c9c4f82cf 100644 --- a/terminal/utils.py +++ b/terminal/utils.py @@ -15,5 +15,20 @@ def get_logger(name): return logging.getLogger('jumpserver.%s' % name) +class ControlChar: + CHARS = { + 'clear': '\x1b[H\x1b[2J', + } + + def __init__(self): + pass + + def __getattr__(self, item): + return self.__class__.CHARS.get(item, '') + + class SSHServerException(Exception): pass + + +control_char = ControlChar() From acf51238d097e602540f0151a34e6152076f2540 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 29 Sep 2016 18:01:26 +0800 Subject: [PATCH 19/30] Modify ssh server bug for using public key --- apps/assets/migrations/__init__.py | 0 apps/common/utils.py | 10 +++++-- apps/perms/migrations/__init__.py | 0 apps/users/migrations/__init__.py | 0 connect.py | 32 ---------------------- requirements.txt | 1 + terminal/ssh_server.py | 25 ++++++++++++++++- test_rsa.key | 15 ---------- test_server.py | 44 ------------------------------ 9 files changed, 33 insertions(+), 94 deletions(-) delete mode 100644 apps/assets/migrations/__init__.py delete mode 100644 apps/perms/migrations/__init__.py delete mode 100644 apps/users/migrations/__init__.py delete mode 100644 connect.py delete mode 100644 test_rsa.key delete mode 100644 test_server.py diff --git a/apps/assets/migrations/__init__.py b/apps/assets/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/common/utils.py b/apps/common/utils.py index 3546f610f..da83cfca7 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -30,11 +30,17 @@ def get_object_or_none(model, **kwargs): def encrypt(*args, **kwargs): - return signing.dumps(*args, **kwargs) + try: + return signing.dumps(*args, **kwargs) + except signing.BadSignature: + return '' def decrypt(*args, **kwargs): - return signing.loads(*args, **kwargs) + try: + return signing.loads(*args, **kwargs) + except signing.BadSignature: + return '' def date_expired_default(): diff --git a/apps/perms/migrations/__init__.py b/apps/perms/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/users/migrations/__init__.py b/apps/users/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/connect.py b/connect.py deleted file mode 100644 index 51020d8f1..000000000 --- a/connect.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -import sys -import os - -# reload(sys) -# sys.setdefaultencoding('utf8') - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -sys.path.append(os.path.join(BASE_DIR, 'apps')) - -import re -import time -import datetime -import textwrap -import getpass -import readline -import django -import paramiko -import errno -import pyte -import operator -import struct, fcntl, signal, socket, select -from io import open as copen -import uuid - - -os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' - - diff --git a/requirements.txt b/requirements.txt index 311786d8d..4bac3860e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ django-simple-captcha==0.5.2 django-formtools==1.0 sshpubkeys==2.2.0 djangorestframework-bulk==0.2.1 +python-gssapi==0.6.4 diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index e8e8f60c8..36b0f9afd 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -41,6 +41,8 @@ from utils import get_logger, SSHServerException, control_char logger = get_logger(__name__) +paramiko.util.log_to_file(os.path.join(BASE_DIR, 'logs', 'paramiko.log')) + class SSHServer(paramiko.ServerInterface): host_key_path = os.path.join(BASE_DIR, 'host_rsa_key') @@ -79,6 +81,27 @@ class SSHServer(paramiko.ServerInterface): return paramiko.OPEN_SUCCEEDED return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + def check_auth_gssapi_with_mic(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def check_auth_gssapi_keyex(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def enable_auth_gssapi(self): + UseGSSAPI = True + GSSAPICleanupCredentials = False + return UseGSSAPI + def check_auth_password(self, username, password): self.user = user = check_user_is_valid(username=username, password=password) self.username = username = user.username @@ -99,9 +122,9 @@ class SSHServer(paramiko.ServerInterface): def check_auth_publickey(self, username, public_key): self.user = user = check_user_is_valid(username=username, public_key=public_key) - self.username = username = user.username if self.user: + self.username = username = user.username logger.info('Accepted public key for %(username)s from %(host)s port %(port)s ' % { 'username': username, 'host': self.addr[0], diff --git a/test_rsa.key b/test_rsa.key deleted file mode 100644 index f50e9c538..000000000 --- a/test_rsa.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz -oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/ -d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB -gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0 -EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon -soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H -tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU -avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA -4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g -H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv -qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV -HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc -nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7 ------END RSA PRIVATE KEY----- diff --git a/test_server.py b/test_server.py deleted file mode 100644 index b6314f2bc..000000000 --- a/test_server.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -import socket -import sys -import threading - - -class ThreadSocket: - def __init__(self, host, port): - self.host = host - self.port = port - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.sock.bind((self.host, self.port)) - - def listen(self): - self.sock.listen(5) - while True: - client, address = self.sock.accept() - client.settimeout(60) - threading.Thread(target=self.handle_client_request, args=(client, address)).start() - - def handle_client_request(self, client, address): - print("Get client: %s" % str(address)) - while True: - try: - data = client.recv(1024) - print("sleep : %s" % str(address)) - if data: - client.send(data) - else: - raise IndexError('Client has disconnected') - except: - client.close() - - -if __name__ == '__main__': - server = ThreadSocket('', 9000) - try: - server.listen() - except KeyboardInterrupt: - sys.exit(1) From e3c620e1382265049d4740ac70a0bed80362afe2 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 29 Sep 2016 18:35:52 +0800 Subject: [PATCH 20/30] Debug some bug for auth failed and exit --- terminal/ssh_server.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 36b0f9afd..fb98dbf3a 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -104,8 +104,8 @@ class SSHServer(paramiko.ServerInterface): def check_auth_password(self, username, password): self.user = user = check_user_is_valid(username=username, password=password) - self.username = username = user.username if self.user: + self.username = username = user.username logger.info('Accepted password for %(username)s from %(host)s port %(port)s ' % { 'username': username, 'host': self.addr[0], @@ -113,6 +113,7 @@ class SSHServer(paramiko.ServerInterface): }) return paramiko.AUTH_SUCCESSFUL else: + self.client.close() logger.info('Authentication password failed for %(username)s from %(host)s port %(port)s ' % { 'username': username, 'host': self.addr[0], @@ -235,7 +236,6 @@ class JumpServer: def __init__(self): self.listen_host = '0.0.0.0' self.listen_port = 2222 - self.sock = None def display_navigation(self, username, client_channel): nav = Navigation(username, client_channel) @@ -262,12 +262,12 @@ class JumpServer: client_channel = transport.accept(20) self.__class__.client_channel_pools.append(client_channel) if client_channel is None: - logger.warning('No channel get.') - raise SSHServerException('No channel get.') + logger.warning('No ssh channel get.') + client.close() + sys.exit(404) if not ssh_server.event.is_set(): logger.warning('Client never asked for a shell.') - raise SSHServerException('Client never asked for a shell.') return client_channel def get_backend_channel(self, host, port, username, term='xterm', width=80, height=24): @@ -345,7 +345,7 @@ class JumpServer: sys.exit(100) def listen(self): - self.sock = sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((self.listen_host, self.listen_port)) sock.listen(5) @@ -357,7 +357,7 @@ class JumpServer: while True: try: - client, addr = self.sock.accept() + client, addr = sock.accept() process = Process(target=self.handle_ssh_request, args=(client, addr)) process.daemon = True process.start() From b4c64991390661e51276edb5a41a21b1ab2a9b27 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 29 Sep 2016 21:36:15 +0800 Subject: [PATCH 21/30] Try to fix ssh server close client bug --- terminal/ssh_server.py | 62 ++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index fb98dbf3a..6d79c46a6 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -81,27 +81,6 @@ class SSHServer(paramiko.ServerInterface): return paramiko.OPEN_SUCCEEDED return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED - def check_auth_gssapi_with_mic(self, username, - gss_authenticated=paramiko.AUTH_FAILED, - cc_file=None): - - if gss_authenticated == paramiko.AUTH_SUCCESSFUL: - return paramiko.AUTH_SUCCESSFUL - return paramiko.AUTH_FAILED - - def check_auth_gssapi_keyex(self, username, - gss_authenticated=paramiko.AUTH_FAILED, - cc_file=None): - - if gss_authenticated == paramiko.AUTH_SUCCESSFUL: - return paramiko.AUTH_SUCCESSFUL - return paramiko.AUTH_FAILED - - def enable_auth_gssapi(self): - UseGSSAPI = True - GSSAPICleanupCredentials = False - return UseGSSAPI - def check_auth_password(self, username, password): self.user = user = check_user_is_valid(username=username, password=password) if self.user: @@ -153,7 +132,6 @@ class SSHServer(paramiko.ServerInterface): self.__class__.channel_pools.append(channel) channel.username = self.username channel.addr = self.addr - return True def check_channel_pty_request(self, channel, term, width, height, pixelwidth, @@ -181,8 +159,14 @@ class BackendServer: def connect(self, term='xterm', width=80, height=24, timeout=10): self.ssh = ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(hostname=self.host, port=self.port, username=self.username, password=self.host_password, - pkey=self.host_private_key, look_for_keys=False, allow_agent=True, compress=True, timeout=timeout) + + try: + ssh.connect(hostname=self.host, port=self.port, username=self.username, password=self.host_password, + pkey=self.host_private_key, look_for_keys=False, allow_agent=True, compress=True, timeout=timeout) + except Exception: + logger.warning('Connect backend server %s failed' % self.host) + return None + self.channel = channel = ssh.invoke_shell(term=term, width=width, height=height) logger.info('Connect backend server %(username)s@%(host)s:%(port)s successfully' % { 'username': self.username, @@ -260,12 +244,11 @@ class JumpServer: logger.warning('SSH negotiation failed.') client_channel = transport.accept(20) - self.__class__.client_channel_pools.append(client_channel) if client_channel is None: logger.warning('No ssh channel get.') - client.close() - sys.exit(404) + return None + self.__class__.client_channel_pools.append(client_channel) if not ssh_server.event.is_set(): logger.warning('Client never asked for a shell.') return client_channel @@ -273,14 +256,17 @@ class JumpServer: def get_backend_channel(self, host, port, username, term='xterm', width=80, height=24): backend_server = BackendServer(host, port, username) backend_channel = backend_server.connect(term=term, width=width, height=height) - self.__class__.backend_server_pools.append(backend_server) - self.__class__.backend_channel_pools.append(backend_channel) - if not backend_channel: + + if backend_channel is None: logger.warning('Connect %(username)s@%(host)s:%(port)s failed' % { 'username': username, 'host': host, 'port': port, }) + return None + + self.__class__.backend_server_pools.append(backend_server) + self.__class__.backend_channel_pools.append(backend_channel) return backend_channel @@ -289,12 +275,28 @@ class JumpServer: 'host': addr[0], 'port': addr[1], }) + raise IndexError + dir(client) + client.close() + return False + try: client_channel = self.get_client_channel(client, addr) + if client_channel is None: + client.close() + return + host, port, username = self.display_navigation('root', client_channel) backend_channel = self.get_backend_channel(host, port, username, width=client_channel.width, height=client_channel.height) + if backend_channel is None: + client.shutdown() + client.close() + client.send('Close') + print(client) + print(dir(client)) + return while True: r, w, x = select.select([client_channel, backend_channel], [], []) From 0d25a8f5b765d4411eab1f93120ba9f406a3a93a Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 29 Sep 2016 23:52:07 +0800 Subject: [PATCH 22/30] Use thread replace process --- terminal/ssh_server.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 6d79c46a6..5b9f87b2b 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -92,7 +92,6 @@ class SSHServer(paramiko.ServerInterface): }) return paramiko.AUTH_SUCCESSFUL else: - self.client.close() logger.info('Authentication password failed for %(username)s from %(host)s port %(port)s ' % { 'username': username, 'host': self.addr[0], @@ -275,10 +274,6 @@ class JumpServer: 'host': addr[0], 'port': addr[1], }) - raise IndexError - dir(client) - client.close() - return False try: client_channel = self.get_client_channel(client, addr) @@ -360,9 +355,9 @@ class JumpServer: while True: try: client, addr = sock.accept() - process = Process(target=self.handle_ssh_request, args=(client, addr)) - process.daemon = True - process.start() + thread = threading.Thread(target=self.handle_ssh_request, args=(client, addr)) + thread.daemon = True + thread.start() except Exception as e: logger.error('Bind failed: ' + str(e)) traceback.print_exc() From d9866e1f38ebd8a78cc8c2e0238f39c1ac90e4c9 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 30 Sep 2016 00:07:55 +0800 Subject: [PATCH 23/30] Debug to find command --- terminal/ssh_server.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index 5b9f87b2b..698a58338 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -293,6 +293,9 @@ class JumpServer: print(dir(client)) return + input_data = [] + output_data = [] + id_ = 0 while True: r, w, x = select.select([client_channel, backend_channel], [], []) @@ -307,8 +310,9 @@ class JumpServer: 'username': client_channel.username, }) break - print('CC: ' + repr(client_data)) backend_channel.send(client_data) + input_data.append('%s: %s' % (id_, client_data[:5])) + id_ += 1 if backend_channel in r: backend_data = backend_channel.recv(1024) @@ -320,8 +324,10 @@ class JumpServer: 'username': backend_channel.username, }) break - print('SS: ' + repr(backend_data)) client_channel.send(backend_data) + output_data.append('%s: %s' % (id_-1, backend_data[:5])) + print('in: %s' % input_data) + print('out: %s' % output_data) # if len(recv_data) > 20: # server_data.append('...') From d40aa49d8c9a144d90d5b8032edc6a109ad801e8 Mon Sep 17 00:00:00 2001 From: "xiaokong1937@gmail.com" <763691951@qq.com> Date: Sat, 1 Oct 2016 20:26:43 +0800 Subject: [PATCH 24/30] user bulk import through Excel and close #20 --- apps/common/mixins.py | 9 +++ apps/static/js/jquery.form.min.js | 11 +++ apps/users/forms.py | 7 ++ .../templates/users/_user_import_modal.html | 19 ++++++ apps/users/templates/users/user_list.html | 22 +++++- apps/users/urls.py | 1 + apps/users/views.py | 67 ++++++++++++++++++- requirements.txt | 1 + 8 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 apps/static/js/jquery.form.min.js create mode 100644 apps/users/templates/users/_user_import_modal.html diff --git a/apps/common/mixins.py b/apps/common/mixins.py index 997f2fbba..4fef5e005 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -1,6 +1,7 @@ # coding: utf-8 from django.db import models +from django.http import JsonResponse from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -36,3 +37,11 @@ class NoDeleteModelMixin(models.Model): self.is_discard = True self.discard_time = now() return self.save() + + +class JSONResponseMixin(object): + + """JSON mixin""" + + def render_json_response(self, context): + return JsonResponse(context) diff --git a/apps/static/js/jquery.form.min.js b/apps/static/js/jquery.form.min.js new file mode 100644 index 000000000..7321a3b06 --- /dev/null +++ b/apps/static/js/jquery.form.min.js @@ -0,0 +1,11 @@ +/*! + * jQuery Form Plugin + * version: 3.51.0-2014.06.20 + * Requires jQuery v1.5 or later + * Copyright (c) 2014 M. Alsup + * Examples and documentation at: http://malsup.com/jquery/form/ + * Project repository: https://github.com/malsup/form + * Dual licensed under the MIT and GPL licenses. + * https://github.com/malsup/form#copyright-and-license + */ +!function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):e("undefined"!=typeof jQuery?jQuery:window.Zepto)}(function(e){"use strict";function t(t){var r=t.data;t.isDefaultPrevented()||(t.preventDefault(),e(t.target).ajaxSubmit(r))}function r(t){var r=t.target,a=e(r);if(!a.is("[type=submit],[type=image]")){var n=a.closest("[type=submit]");if(0===n.length)return;r=n[0]}var i=this;if(i.clk=r,"image"==r.type)if(void 0!==t.offsetX)i.clk_x=t.offsetX,i.clk_y=t.offsetY;else if("function"==typeof e.fn.offset){var o=a.offset();i.clk_x=t.pageX-o.left,i.clk_y=t.pageY-o.top}else i.clk_x=t.pageX-r.offsetLeft,i.clk_y=t.pageY-r.offsetTop;setTimeout(function(){i.clk=i.clk_x=i.clk_y=null},100)}function a(){if(e.fn.ajaxSubmit.debug){var t="[jquery.form] "+Array.prototype.join.call(arguments,"");window.console&&window.console.log?window.console.log(t):window.opera&&window.opera.postError&&window.opera.postError(t)}}var n={};n.fileapi=void 0!==e("").get(0).files,n.formdata=void 0!==window.FormData;var i=!!e.fn.prop;e.fn.attr2=function(){if(!i)return this.attr.apply(this,arguments);var e=this.prop.apply(this,arguments);return e&&e.jquery||"string"==typeof e?e:this.attr.apply(this,arguments)},e.fn.ajaxSubmit=function(t){function r(r){var a,n,i=e.param(r,t.traditional).split("&"),o=i.length,s=[];for(a=0;o>a;a++)i[a]=i[a].replace(/\+/g," "),n=i[a].split("="),s.push([decodeURIComponent(n[0]),decodeURIComponent(n[1])]);return s}function o(a){for(var n=new FormData,i=0;i').val(m.extraData[d].value).appendTo(w)[0]:e('').val(m.extraData[d]).appendTo(w)[0]);m.iframeTarget||v.appendTo("body"),g.attachEvent?g.attachEvent("onload",s):g.addEventListener("load",s,!1),setTimeout(t,15);try{w.submit()}catch(h){var x=document.createElement("form").submit;x.apply(w)}}finally{w.setAttribute("action",i),w.setAttribute("enctype",c),r?w.setAttribute("target",r):f.removeAttr("target"),e(l).remove()}}function s(t){if(!x.aborted&&!F){if(M=n(g),M||(a("cannot access response document"),t=k),t===D&&x)return x.abort("timeout"),void S.reject(x,"timeout");if(t==k&&x)return x.abort("server abort"),void S.reject(x,"error","server abort");if(M&&M.location.href!=m.iframeSrc||T){g.detachEvent?g.detachEvent("onload",s):g.removeEventListener("load",s,!1);var r,i="success";try{if(T)throw"timeout";var o="xml"==m.dataType||M.XMLDocument||e.isXMLDoc(M);if(a("isXml="+o),!o&&window.opera&&(null===M.body||!M.body.innerHTML)&&--O)return a("requeing onLoad callback, DOM not available"),void setTimeout(s,250);var u=M.body?M.body:M.documentElement;x.responseText=u?u.innerHTML:null,x.responseXML=M.XMLDocument?M.XMLDocument:M,o&&(m.dataType="xml"),x.getResponseHeader=function(e){var t={"content-type":m.dataType};return t[e.toLowerCase()]},u&&(x.status=Number(u.getAttribute("status"))||x.status,x.statusText=u.getAttribute("statusText")||x.statusText);var c=(m.dataType||"").toLowerCase(),l=/(json|script|text)/.test(c);if(l||m.textarea){var f=M.getElementsByTagName("textarea")[0];if(f)x.responseText=f.value,x.status=Number(f.getAttribute("status"))||x.status,x.statusText=f.getAttribute("statusText")||x.statusText;else if(l){var p=M.getElementsByTagName("pre")[0],h=M.getElementsByTagName("body")[0];p?x.responseText=p.textContent?p.textContent:p.innerText:h&&(x.responseText=h.textContent?h.textContent:h.innerText)}}else"xml"==c&&!x.responseXML&&x.responseText&&(x.responseXML=X(x.responseText));try{E=_(x,c,m)}catch(y){i="parsererror",x.error=r=y||i}}catch(y){a("error caught: ",y),i="error",x.error=r=y||i}x.aborted&&(a("upload aborted"),i=null),x.status&&(i=x.status>=200&&x.status<300||304===x.status?"success":"error"),"success"===i?(m.success&&m.success.call(m.context,E,"success",x),S.resolve(x.responseText,"success",x),d&&e.event.trigger("ajaxSuccess",[x,m])):i&&(void 0===r&&(r=x.statusText),m.error&&m.error.call(m.context,x,i,r),S.reject(x,"error",r),d&&e.event.trigger("ajaxError",[x,m,r])),d&&e.event.trigger("ajaxComplete",[x,m]),d&&!--e.active&&e.event.trigger("ajaxStop"),m.complete&&m.complete.call(m.context,x,i),F=!0,m.timeout&&clearTimeout(j),setTimeout(function(){m.iframeTarget?v.attr("src",m.iframeSrc):v.remove(),x.responseXML=null},100)}}}var c,l,m,d,p,v,g,x,y,b,T,j,w=f[0],S=e.Deferred();if(S.abort=function(e){x.abort(e)},r)for(l=0;l'),v.css({position:"absolute",top:"-1000px",left:"-1000px"})),g=v[0],x={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(t){var r="timeout"===t?"timeout":"aborted";a("aborting upload... "+r),this.aborted=1;try{g.contentWindow.document.execCommand&&g.contentWindow.document.execCommand("Stop")}catch(n){}v.attr("src",m.iframeSrc),x.error=r,m.error&&m.error.call(m.context,x,r,t),d&&e.event.trigger("ajaxError",[x,m,r]),m.complete&&m.complete.call(m.context,x,r)}},d=m.global,d&&0===e.active++&&e.event.trigger("ajaxStart"),d&&e.event.trigger("ajaxSend",[x,m]),m.beforeSend&&m.beforeSend.call(m.context,x,m)===!1)return m.global&&e.active--,S.reject(),S;if(x.aborted)return S.reject(),S;y=w.clk,y&&(b=y.name,b&&!y.disabled&&(m.extraData=m.extraData||{},m.extraData[b]=y.value,"image"==y.type&&(m.extraData[b+".x"]=w.clk_x,m.extraData[b+".y"]=w.clk_y)));var D=1,k=2,A=e("meta[name=csrf-token]").attr("content"),L=e("meta[name=csrf-param]").attr("content");L&&A&&(m.extraData=m.extraData||{},m.extraData[L]=A),m.forceSync?o():setTimeout(o,10);var E,M,F,O=50,X=e.parseXML||function(e,t){return window.ActiveXObject?(t=new ActiveXObject("Microsoft.XMLDOM"),t.async="false",t.loadXML(e)):t=(new DOMParser).parseFromString(e,"text/xml"),t&&t.documentElement&&"parsererror"!=t.documentElement.nodeName?t:null},C=e.parseJSON||function(e){return window.eval("("+e+")")},_=function(t,r,a){var n=t.getResponseHeader("content-type")||"",i="xml"===r||!r&&n.indexOf("xml")>=0,o=i?t.responseXML:t.responseText;return i&&"parsererror"===o.documentElement.nodeName&&e.error&&e.error("parsererror"),a&&a.dataFilter&&(o=a.dataFilter(o,r)),"string"==typeof o&&("json"===r||!r&&n.indexOf("json")>=0?o=C(o):("script"===r||!r&&n.indexOf("javascript")>=0)&&e.globalEval(o)),o};return S}if(!this.length)return a("ajaxSubmit: skipping submit process - no element selected"),this;var u,c,l,f=this;"function"==typeof t?t={success:t}:void 0===t&&(t={}),u=t.type||this.attr2("method"),c=t.url||this.attr2("action"),l="string"==typeof c?e.trim(c):"",l=l||window.location.href||"",l&&(l=(l.match(/^([^#]+)/)||[])[1]),t=e.extend(!0,{url:l,success:e.ajaxSettings.success,type:u||e.ajaxSettings.type,iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank"},t);var m={};if(this.trigger("form-pre-serialize",[this,t,m]),m.veto)return a("ajaxSubmit: submit vetoed via form-pre-serialize trigger"),this;if(t.beforeSerialize&&t.beforeSerialize(this,t)===!1)return a("ajaxSubmit: submit aborted via beforeSerialize callback"),this;var d=t.traditional;void 0===d&&(d=e.ajaxSettings.traditional);var p,h=[],v=this.formToArray(t.semantic,h);if(t.data&&(t.extraData=t.data,p=e.param(t.data,d)),t.beforeSubmit&&t.beforeSubmit(v,this,t)===!1)return a("ajaxSubmit: submit aborted via beforeSubmit callback"),this;if(this.trigger("form-submit-validate",[v,this,t,m]),m.veto)return a("ajaxSubmit: submit vetoed via form-submit-validate trigger"),this;var g=e.param(v,d);p&&(g=g?g+"&"+p:p),"GET"==t.type.toUpperCase()?(t.url+=(t.url.indexOf("?")>=0?"&":"?")+g,t.data=null):t.data=g;var x=[];if(t.resetForm&&x.push(function(){f.resetForm()}),t.clearForm&&x.push(function(){f.clearForm(t.includeHidden)}),!t.dataType&&t.target){var y=t.success||function(){};x.push(function(r){var a=t.replaceTarget?"replaceWith":"html";e(t.target)[a](r).each(y,arguments)})}else t.success&&x.push(t.success);if(t.success=function(e,r,a){for(var n=t.context||this,i=0,o=x.length;o>i;i++)x[i].apply(n,[e,r,a||f,f])},t.error){var b=t.error;t.error=function(e,r,a){var n=t.context||this;b.apply(n,[e,r,a,f])}}if(t.complete){var T=t.complete;t.complete=function(e,r){var a=t.context||this;T.apply(a,[e,r,f])}}var j=e("input[type=file]:enabled",this).filter(function(){return""!==e(this).val()}),w=j.length>0,S="multipart/form-data",D=f.attr("enctype")==S||f.attr("encoding")==S,k=n.fileapi&&n.formdata;a("fileAPI :"+k);var A,L=(w||D)&&!k;t.iframe!==!1&&(t.iframe||L)?t.closeKeepAlive?e.get(t.closeKeepAlive,function(){A=s(v)}):A=s(v):A=(w||D)&&k?o(v):e.ajax(t),f.removeData("jqxhr").data("jqxhr",A);for(var E=0;Ec;c++)if(d=u[c],f=d.name,f&&!d.disabled)if(t&&o.clk&&"image"==d.type)o.clk==d&&(a.push({name:f,value:e(d).val(),type:d.type}),a.push({name:f+".x",value:o.clk_x},{name:f+".y",value:o.clk_y}));else if(m=e.fieldValue(d,!0),m&&m.constructor==Array)for(r&&r.push(d),l=0,h=m.length;h>l;l++)a.push({name:f,value:m[l]});else if(n.fileapi&&"file"==d.type){r&&r.push(d);var v=d.files;if(v.length)for(l=0;li;i++)r.push({name:a,value:n[i]});else null!==n&&"undefined"!=typeof n&&r.push({name:this.name,value:n})}}),e.param(r)},e.fn.fieldValue=function(t){for(var r=[],a=0,n=this.length;n>a;a++){var i=this[a],o=e.fieldValue(i,t);null===o||"undefined"==typeof o||o.constructor==Array&&!o.length||(o.constructor==Array?e.merge(r,o):r.push(o))}return r},e.fieldValue=function(t,r){var a=t.name,n=t.type,i=t.tagName.toLowerCase();if(void 0===r&&(r=!0),r&&(!a||t.disabled||"reset"==n||"button"==n||("checkbox"==n||"radio"==n)&&!t.checked||("submit"==n||"image"==n)&&t.form&&t.form.clk!=t||"select"==i&&-1==t.selectedIndex))return null;if("select"==i){var o=t.selectedIndex;if(0>o)return null;for(var s=[],u=t.options,c="select-one"==n,l=c?o+1:u.length,f=c?o:0;l>f;f++){var m=u[f];if(m.selected){var d=m.value;if(d||(d=m.attributes&&m.attributes.value&&!m.attributes.value.specified?m.text:m.value),c)return d;s.push(d)}}return s}return e(t).val()},e.fn.clearForm=function(t){return this.each(function(){e("input,select,textarea",this).clearFields(t)})},e.fn.clearFields=e.fn.clearInputs=function(t){var r=/^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i;return this.each(function(){var a=this.type,n=this.tagName.toLowerCase();r.test(a)||"textarea"==n?this.value="":"checkbox"==a||"radio"==a?this.checked=!1:"select"==n?this.selectedIndex=-1:"file"==a?/MSIE/.test(navigator.userAgent)?e(this).replaceWith(e(this).clone(!0)):e(this).val(""):t&&(t===!0&&/hidden/.test(a)||"string"==typeof t&&e(this).is(t))&&(this.value="")})},e.fn.resetForm=function(){return this.each(function(){("function"==typeof this.reset||"object"==typeof this.reset&&!this.reset.nodeType)&&this.reset()})},e.fn.enable=function(e){return void 0===e&&(e=!0),this.each(function(){this.disabled=!e})},e.fn.selected=function(t){return void 0===t&&(t=!0),this.each(function(){var r=this.type;if("checkbox"==r||"radio"==r)this.checked=t;else if("option"==this.tagName.toLowerCase()){var a=e(this).parent("select");t&&a[0]&&"select-one"==a[0].type&&a.find("option").selected(!1),this.selected=t}})},e.fn.ajaxSubmit.debug=!1}); \ No newline at end of file diff --git a/apps/users/forms.py b/apps/users/forms.py index 8c21011ce..efe98e67b 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -34,6 +34,13 @@ class UserCreateForm(forms.ModelForm): } +class UserBulkImportForm(forms.ModelForm): + + class Meta: + model = User + fields = ['username', 'email', 'enable_otp', 'role'] + + class UserUpdateForm(forms.ModelForm): class Meta: diff --git a/apps/users/templates/users/_user_import_modal.html b/apps/users/templates/users/_user_import_modal.html new file mode 100644 index 000000000..99a61d126 --- /dev/null +++ b/apps/users/templates/users/_user_import_modal.html @@ -0,0 +1,19 @@ +{% extends '_modal.html' %} +{% load i18n %} +{% block modal_id %}user_import_modal{% endblock %} +{% block modal_title%}{% trans "Import User" %}{% endblock %} +{% block modal_body %} +

{% trans "Hint: your excel should organized in the following format." %}

+

{% trans "* You should have a very worksheet named `users`." %}

+

{% trans "* Rows in this worksheet: username, email, enable_opt(0, 1), role(one of ['Admin', 'User'])" %}

+
+ {% csrf_token %} +
+ +
+ +
+
+
+{% endblock %} +{% block modal_confirm_id %}btn_user_import{% endblock %} diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index 36029c59b..2e34b560b 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -17,7 +17,8 @@ div.dataTables_wrapper div.dataTables_filter { {% endblock %} {% block table_search %}{% endblock %} {% block table_container %} - + + @@ -51,10 +52,12 @@ div.dataTables_wrapper div.dataTables_filter { {% include "users/_user_bulk_update_modal.html" %} +{% include "users/_user_import_modal.html" %} {% endblock %} {% block content_bottom_left %} {% endblock %} {% block custom_foot_js %} + {% endblock %} diff --git a/apps/users/urls.py b/apps/users/urls.py index 5847e30b5..eb7797d61 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -23,6 +23,7 @@ urlpatterns = [ url(r'^user/(?P[0-9]+)/granted-asset', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), url(r'^user/(?P[0-9]+)/login-history', views.UserDetailView.as_view(), name='user-login-history'), url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'), + url(r'^import/$', views.BulkImportUserView.as_view(), name='user-import'), url(r'^user/(?P[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'), url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'), url(r'^user/(?P[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'), diff --git a/apps/users/views.py b/apps/users/views.py index 8dec28a22..930d1cea9 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals +from django import forms from django.conf import settings from django.contrib.auth import login as auth_login, logout as auth_logout from django.contrib.auth.mixins import LoginRequiredMixin @@ -23,10 +24,11 @@ from django.views.generic.detail import DetailView from formtools.wizard.views import SessionWizardView +from common.mixins import JSONResponseMixin from common.utils import get_object_or_none, get_logger from .models import User, UserGroup from .forms import UserCreateForm, UserUpdateForm, UserGroupForm, UserLoginForm, UserInfoForm, UserKeyForm, \ - UserPrivateAssetPermissionForm + UserPrivateAssetPermissionForm, UserBulkImportForm from .utils import AdminUserRequiredMixin, user_add_success_next, send_reset_password_mail from .hands import AssetPermission, get_user_granted_asset_groups, get_user_granted_assets @@ -443,3 +445,66 @@ class UserGrantedAssetView(AdminUserRequiredMixin, SingleObjectMixin, ListView): } kwargs.update(context) return super(UserGrantedAssetView, self).get_context_data(**kwargs) + + +class FileForm(forms.Form): + excel = forms.FileField() + + +class BulkImportUserView(AdminUserRequiredMixin, JSONResponseMixin, FormView): + form_class = FileForm + + def form_invalid(self, form): + try: + error = form.errors.values()[-1][-1] + except Exception as e: + print e + error = _('Invalid file.') + data = { + 'success': False, + 'msg': error + } + return self.render_json_response(data) + + def form_valid(self, form): + from openpyxl import load_workbook + try: + wb = load_workbook(form.cleaned_data['excel']) + ws = wb['users'] + except Exception as e: + print e + error = _('Not a valid Excel file.') + data = { + 'success': False, + 'msg': error + } + return self.render_json_response(data) + + errors = [] + for index, row in enumerate(ws.rows): + user_data = [cell.value for cell in row] + if len(user_data) != 4: + errors.append("Row {}: invalid user data format.".format(index)) + continue + username, email, enable_otp, role = user_data + data = { + 'username': username, + 'email': email, + 'enable_otp': True if enable_otp in ['T', '1', 1, True] else False, + 'role': role + } + form = UserBulkImportForm(data, auto_id=False) + if form.is_valid(): + form.save() + else: + form_errors = form.errors.as_data() + for key, err_list in form_errors.iteritems(): + error_line = "{} :".format(key) + for errs in err_list: + error_line = "{}{}".format(error_line, ";".join([err for err in errs.messages])) + errors.append("Row {}: {}".format(index, error_line)) + data = { + 'success': True if not errors else False, + 'msg': 'ok' if not errors else '
'.join(errors) + } + return self.render_json_response(data) diff --git a/requirements.txt b/requirements.txt index 311786d8d..ac60097ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ wcwidth==0.1.7 websocket-client==0.37.0 djangorestframework==3.4.5 ForgeryPy==0.1 +openpyxl==2.4.0 paramiko==2.0.2 celery==3.1.23 ansible==2.1.1.0 From 6856fad0c025f7f5049412a3b32160b03e1bc421 Mon Sep 17 00:00:00 2001 From: "xiaokong1937@gmail.com" <763691951@qq.com> Date: Sun, 2 Oct 2016 11:09:27 +0800 Subject: [PATCH 25/30] integrate the user-group list page with its api; --- apps/common/mixins.py | 16 ++ apps/users/api.py | 65 ++----- apps/users/serializers.py | 64 ++++--- .../templates/users/user_group_detail.html | 2 +- .../templates/users/user_group_list.html | 164 ++++++++++++------ apps/users/templates/users/user_list.html | 5 +- apps/users/urls.py | 19 +- apps/users/views.py | 19 +- 8 files changed, 182 insertions(+), 172 deletions(-) diff --git a/apps/common/mixins.py b/apps/common/mixins.py index 4fef5e005..b12416623 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -45,3 +45,19 @@ class JSONResponseMixin(object): def render_json_response(self, context): return JsonResponse(context) + + +class BulkDeleteApiMixin(object): + + def filter_queryset(self, queryset): + id_list = self.request.query_params.get('id__in') + if id_list: + import json + try: + ids = json.loads(id_list) + except Exception as e: + print e + return queryset + if isinstance(ids, list): + queryset = queryset.filter(id__in=ids) + return queryset diff --git a/apps/users/api.py b/apps/users/api.py index 361f532a4..9de263963 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -9,51 +9,28 @@ from rest_framework import generics, status from rest_framework.response import Response from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView -from .serializers import UserSerializer, UserGroupSerializer, UserAttributeSerializer, GroupUserEditSerializer, \ - GroupEditSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer from .models import User, UserGroup +from .serializers import UserDetailSerializer, UserAndGroupSerializer, \ + GroupDetailSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer, GroupBulkUpdateSerializer +from common.mixins import BulkDeleteApiMixin logger = logging.getLogger('jumpserver.users.api') -class UserListAddApi(generics.ListCreateAPIView): +class UserDetailApi(generics.RetrieveUpdateDestroyAPIView): queryset = User.objects.all() - serializer_class = UserSerializer + serializer_class = UserDetailSerializer -class UserDetailDeleteUpdateApi(generics.RetrieveUpdateDestroyAPIView): +class UserAndGroupEditApi(generics.RetrieveUpdateAPIView): queryset = User.objects.all() - serializer_class = UserSerializer - - def delete(self, request, *args, **kwargs): - print(self.request.data) - return super(UserDetailDeleteUpdateApi, self).delete(request, *args, **kwargs) - - -class UserGroupListAddApi(generics.ListCreateAPIView): - queryset = UserGroup.objects.all() - serializer_class = UserGroupSerializer - - -class UserGroupDetailDeleteUpdateApi(generics.RetrieveUpdateDestroyAPIView): - queryset = UserGroup.objects.all() - serializer_class = UserGroupSerializer - - -class UserAttributeApi(generics.RetrieveUpdateDestroyAPIView): - queryset = User.objects.all() - serializer_class = UserAttributeSerializer - - -class GroupUserEditApi(generics.RetrieveUpdateAPIView): - queryset = User.objects.all() - serializer_class = GroupUserEditSerializer + serializer_class = UserAndGroupSerializer class UserResetPasswordApi(generics.UpdateAPIView): queryset = User.objects.all() - serializer_class = GroupUserEditSerializer + serializer_class = UserDetailSerializer def perform_update(self, serializer): # Note: we are not updating the user object here. @@ -68,7 +45,7 @@ class UserResetPasswordApi(generics.UpdateAPIView): class UserResetPKApi(generics.UpdateAPIView): queryset = User.objects.all() - serializer_class = GroupUserEditSerializer + serializer_class = UserDetailSerializer def perform_update(self, serializer): user = self.get_object() @@ -88,9 +65,9 @@ class UserUpdatePKApi(generics.UpdateAPIView): user.save() -class GroupEditApi(generics.RetrieveUpdateDestroyAPIView): +class GroupDetailApi(generics.RetrieveUpdateDestroyAPIView): queryset = UserGroup.objects.all() - serializer_class = GroupEditSerializer + serializer_class = GroupDetailSerializer def perform_update(self, serializer): users = serializer.validated_data.get('users') @@ -105,27 +82,19 @@ class GroupEditApi(generics.RetrieveUpdateDestroyAPIView): serializer.save() -class UserBulkUpdateApi(ListBulkCreateUpdateDestroyAPIView): +class UserListUpdateApi(BulkDeleteApiMixin, ListBulkCreateUpdateDestroyAPIView): queryset = User.objects.all() serializer_class = UserBulkUpdateSerializer - def filter_queryset(self, queryset): - id_list = self.request.query_params.get('id__in') - if id_list: - import json - try: - ids = json.loads(id_list) - except Exception as e: - logger.error(str(e)) - return queryset - if isinstance(ids, list): - queryset = queryset.filter(id__in=ids) - return queryset + +class GroupListUpdateApi(BulkDeleteApiMixin, ListBulkCreateUpdateDestroyAPIView): + queryset = UserGroup.objects.all() + serializer_class = GroupBulkUpdateSerializer class DeleteUserFromGroupApi(generics.DestroyAPIView): queryset = UserGroup.objects.all() - serializer_class = GroupEditSerializer + serializer_class = GroupDetailSerializer def destroy(self, request, *args, **kwargs): group = self.get_object() diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 366e62fec..cfce66ab7 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -8,47 +8,13 @@ from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin from .models import User, UserGroup -class UserSerializer(serializers.ModelSerializer): - groups = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='users:user-group-detail-api') - - class Meta: - model = User - exclude = [ - 'password', 'first_name', 'last_name', 'secret_key_otp', - 'private_key', 'public_key', 'avatar', - ] - - -class UserGroupSerializer(serializers.ModelSerializer): - users = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='users:user-detail-api') - - class Meta: - model = UserGroup - fields = '__all__' - - -class GroupEditSerializer(serializers.ModelSerializer): - - class Meta: - model = UserGroup - fields = ['id', 'name', 'comment', 'date_created', 'created_by', 'users'] - - -class UserAttributeSerializer(serializers.ModelSerializer): +class UserDetailSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['avatar', 'wechat', 'phone', 'enable_otp', 'comment', 'is_active', 'name'] -class GroupUserEditSerializer(serializers.ModelSerializer): - groups = serializers.PrimaryKeyRelatedField(many=True, queryset=UserGroup.objects.all()) - - class Meta: - model = User - fields = ['id', 'groups'] - - class UserPKUpdateSerializer(serializers.ModelSerializer): class Meta: @@ -70,6 +36,21 @@ class UserPKUpdateSerializer(serializers.ModelSerializer): return value +class UserAndGroupSerializer(serializers.ModelSerializer): + groups = serializers.PrimaryKeyRelatedField(many=True, queryset=UserGroup.objects.all()) + + class Meta: + model = User + fields = ['id', 'groups'] + + +class GroupDetailSerializer(serializers.ModelSerializer): + + class Meta: + model = UserGroup + fields = ['id', 'name', 'comment', 'date_created', 'created_by', 'users'] + + class UserBulkUpdateSerializer(BulkSerializerMixin, serializers.ModelSerializer): group_display = serializers.SerializerMethodField() active_display = serializers.SerializerMethodField() @@ -88,3 +69,16 @@ class UserBulkUpdateSerializer(BulkSerializerMixin, serializers.ModelSerializer) def get_active_display(self, obj): # TODO: user ative state return not (obj.is_expired and obj.is_active) + + +class GroupBulkUpdateSerializer(BulkSerializerMixin, serializers.ModelSerializer): + + user_amount = serializers.SerializerMethodField() + + class Meta: + model = UserGroup + list_serializer_class = BulkListSerializer + fields = ['id', 'name', 'comment', 'user_amount'] + + def get_user_amount(self, obj): + return obj.users.count() diff --git a/apps/users/templates/users/user_group_detail.html b/apps/users/templates/users/user_group_detail.html index 4f380542f..3e00d0055 100644 --- a/apps/users/templates/users/user_group_detail.html +++ b/apps/users/templates/users/user_group_detail.html @@ -218,7 +218,7 @@ $(document).on('click', '.btn_remove', function(){ users: plain_id_list.map(Number) }; $('#select_user_modal').modal('hide'); - var the_url = "{% url 'users:user-group-edit-api' pk=object.id %}"; + var the_url = "{% url 'users:user-group-detail-api' pk=object.id %}"; var success = function() { toastr.success('{% trans "The selected users has been added to current group." %}'); var html = ""; diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html index 6eee65f21..0463c3305 100644 --- a/apps/users/templates/users/user_group_list.html +++ b/apps/users/templates/users/user_group_list.html @@ -1,71 +1,85 @@ {% extends '_base_list.html' %} {% load i18n static %} -{% load common_tags %} {% block custom_head_css_js %} - - -{% endblock %} +{{ block.super }} + {% endblock %} - -{% block table_head %} -
- - - - - -{% endblock %} - -{% block table_body %} - {% for user_group in user_group_list %} - - - - - - - +{% block table_search %}{% endblock %} +{% block table_container %} + +
- - {% trans "Name" %}{% trans "User Amount" %}{% trans "Asset Amount" %}{% trans "Comment" %}
- - - - {{ user_group.name }} - - {{ user_group.users.count }}999{{ user_group.comment|truncatewords:8 }} - {% trans "Edit" %} - {% trans "Delete" %} -
+ + + + + + + + - {% endfor %} -{% endblock %} - -{% block content_bottom_left %} - -
- -
- -
+
+
+
+
{% trans 'Name' %}{% trans 'User Amount' %}{% trans 'Asset Amount' %}{% trans 'Comment' %}{% trans 'Action' %}
+
+
+ +
+
- +
+
{% endblock %} +{% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index 2e34b560b..b08a44b53 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -1,7 +1,5 @@ {% extends '_base_list.html' %} {% load i18n static %} -{% get_current_language as LANGUAGE_CODE %} -{% load common_tags %} {% block custom_head_css_js %} {{ block.super }}