From 0cda4e0905ffefa79755125227bfa7f2b4e8b5e8 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 7 Oct 2016 08:53:28 +0800 Subject: [PATCH 1/6] Record command history --- terminal/ssh_server.py | 82 +++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index f8c649f13..ce6f54322 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -209,28 +209,53 @@ class Navigation: class ProxyChannel: ENTER_CHAR = ['\r', '\n', '\r\n'] - input_data = [] output_data = [] + command = [] + output = [] def __init__(self, client_channel, backend_channel, client_addr): self.client_channel = client_channel self.backend_channel = backend_channel self.client_addr = client_addr self.in_input_mode = True + self.is_first_input = True + self.id = 0 - def stream_flow(self, input_=None, output_=None): - if input_: - self.in_input_mode = True - if input_ in ['\r', '\n', '\r\n']: - self.in_input_mode = False + # def stream_flow(self, input_=None, output_=None): + # if input_: + # self.in_input_mode = True + # if input_ in ['\r', '\n', '\r\n']: + # self.in_input_mode = False + # + # if output_: + # print(''.join(self.__class__.output_data)) + # if not self.in_input_mode: + # command = ''.join(self.__class__.output_data) + # del self.__class__.output_data + # self.__class__.output_data = [] + # self.__class__.output_data.append(output_) - if output_: - print(''.join(self.__class__.output_data)) - if not self.in_input_mode: - command = ''.join(self.__class__.output_data) - del self.__class__.output_data - self.__class__.output_data = [] - self.__class__.output_data.append(output_) + def get_output(self): + if self.in_input_mode is False: + self.__class__.output_data.pop() + result = ''.join(self.__class__.output_data) + self.__class__.output.append(result) + print('>>>>>>>>>>> output <<<<<<<<<<') + print(result) + print('>>>>>>>>>>> end output <<<<<<<<<<') + del self.__class__.output_data + self.__class__.output_data = [] + + def get_command(self, client_data): + if client_data in self.__class__.ENTER_CHAR: + self.in_input_mode = False + command = ''.join(self.__class__.output_data) + print('########### command ##########') + self.__class__.command.append(command) + print(command) + print('########### end command ##########') + del self.__class__.output_data + self.__class__.output_data = [] def proxy(self): client_channel = self.client_channel @@ -243,24 +268,17 @@ class ProxyChannel: if client_channel.change_window_size_event.is_set(): backend_channel.resize_pty(width=client_channel.width, height=client_channel.height) + # print(self.__class__.output) if client_channel in r: - self.in_input_mode = True - client_data = client_channel.recv(1024) + # Get output of the command + self.get_output() - if client_data in self.__class__.ENTER_CHAR: - self.in_input_mode = False - command = ''.join(self.__class__.output_data) - print('########### command ##########') - print(command) - print('########### end command ##########') - del self.__class__.output_data - self.__class__.output_data = [] - backend_channel.send(client_data) - output = ''.join(self.__class__.output_data) - print('>>>>>>>>>>> output <<<<<<<<<<') - print(output) - print('>>>>>>>>>>> end output <<<<<<<<<<') - continue + client_data = client_channel.recv(1024) + self.in_input_mode = True + self.is_first_input = False + + # Get command input + self.get_command(client_data) if len(client_data) == 0: logger.info('Logout from ssh server %(host)s: %(username)s' % { @@ -280,7 +298,8 @@ class ProxyChannel: 'username': backend_channel.username, }) break - self.__class__.output_data.append(backend_data) + if not self.is_first_input: + self.__class__.output_data.append(backend_data) client_channel.send(backend_data) @@ -346,9 +365,6 @@ class JumpServer: return backend_channel - def command_flow(self, input_=None, output_=None): - pass - def handle_ssh_request(self, client, addr): logger.info("Get ssh request from %(host)s:%(port)s" % { 'host': addr[0], From 59727656c3c5b91beacb6208cf755e3ca8d6b941 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 7 Oct 2016 23:54:29 +0800 Subject: [PATCH 2/6] Update table desgin doc and audit log --- apps/audits/models.py | 50 +++++++++++++++++++++++++++++++++++++- apps/users/api.py | 4 +-- docs/table_design.xml | 34 +++++++++++++++----------- requirements.txt | 1 - terminal/ssh_server.py | 55 +++++++++++++++++------------------------- 5 files changed, 93 insertions(+), 51 deletions(-) diff --git a/apps/audits/models.py b/apps/audits/models.py index bd4b2abe9..71f0ce353 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -1,5 +1,53 @@ +# -*- coding: utf-8 -*- +# + from __future__ import unicode_literals from django.db import models +from django.utils.translation import ugettext_lazy as _ -# Create your models here. + +class LoginLog(models.Model): + LOGIN_TYPE_CHOICE = ( + ('S', 'ssh'), + ('W', 'web'), + ) + + username = models.CharField(max_length=20, verbose_name=_('Username')) + name = models.CharField(max_length=20, blank=True, verbose_name=_('Name')) + login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=1, verbose_name=_('Login type')) + login_ip = models.GenericIPAddressField(verbose_name=_('Login ip')) + login_city = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Login city')) + user_agent = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('User agent')) + date_login = models.DateTimeField(auto_now=True, verbose_name=_('Date login')) + date_logout = models.DateTimeField(null=True, verbose_name=_('Date logout')) + + class Meta: + db_table = 'loginlog' + ordering = ['-date_login', 'username'] + + +class ProxyLog(models.Model): + LOGIN_TYPE_CHOICE = ( + ('S', 'ssh'), + ('W', 'web'), + ) + + username = models.CharField(max_length=20, verbose_name=_('Username')) + name = models.CharField(max_length=20, blank=True, verbose_name=_('Name')) + hostname = models.CharField(max_length=128, blank=True, verbose_name=_('Hostname')) + ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP')) + system_user = models.CharField(max_length=20, verbose_name=_('System user')) + login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=1, verbose_name=_('Login type')) + log_file = models.CharField(max_length=1000, blank=True, null=True) + is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) + date_start = models.DateTimeField(auto_now=True, verbose_name=_('Date start')) + date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished')) + + +class CommandLog(models.Model): + proxy_log = models.ForeignKey(ProxyLog, on_delete=models.CASCADE, related_name='proxy_log') + command = models.CharField(max_length=1000, blank=True) + output = models.TextField(blank=True) + date_start = models.DateTimeField(null=True) + date_finished = models.DateTimeField(null=True) diff --git a/apps/users/api.py b/apps/users/api.py index 9de263963..aaf67a9a4 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -12,10 +12,10 @@ from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView from .models import User, UserGroup from .serializers import UserDetailSerializer, UserAndGroupSerializer, \ GroupDetailSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer, GroupBulkUpdateSerializer -from common.mixins import BulkDeleteApiMixin +from common.mixins import BulkDeleteApiMixin, get_logger -logger = logging.getLogger('jumpserver.users.api') +logger = get_logger(__name__) class UserDetailApi(generics.RetrieveUpdateDestroyAPIView): diff --git a/docs/table_design.xml b/docs/table_design.xml index de825486b..acc1fe395 100644 --- a/docs/table_design.xml +++ b/docs/table_design.xml @@ -634,10 +634,13 @@ id - +
INTEGER NULL + +CHAR +NULL VARCHAR NULL @@ -663,7 +666,7 @@ id
- +
INTEGER NULL @@ -689,14 +692,20 @@ id
- +
INTEGER NULL + +VARCHAR +NULL CHAR NULL +VARCHAR +NULL + CHAR NULL @@ -705,18 +714,9 @@ CHAR NULL - -INTEGER -NULL CHAR NULL - -VARCHAR -NULL - -VARCHAR -NULL INTEGER NULL @@ -738,11 +738,17 @@ INTEGER NULL + +CHAR +NULL + +VARCHAR +NULL DATE NULL - -CHAR + +DATE NULL id diff --git a/requirements.txt b/requirements.txt index ddf45a598..da30cd976 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,4 +23,3 @@ sshpubkeys==2.2.0 djangorestframework-bulk==0.2.1 python-gssapi==0.6.4 tornado==4.4.2 - diff --git a/terminal/ssh_server.py b/terminal/ssh_server.py index ce6f54322..3d4f0072d 100644 --- a/terminal/ssh_server.py +++ b/terminal/ssh_server.py @@ -6,21 +6,21 @@ __version__ = '0.3.3' import sys import os -import base64 +# import base64 import time -from binascii import hexlify +# from binascii import hexlify import sys import threading -from multiprocessing.process import Process +# from multiprocessing.process import Process import traceback -import tty -import termios -import struct -import fcntl -import signal +# import tty +# import termios +# import struct +# import fcntl +# import signal import socket import select -import errno +# import errno import paramiko import django @@ -210,8 +210,7 @@ class Navigation: class ProxyChannel: ENTER_CHAR = ['\r', '\n', '\r\n'] output_data = [] - command = [] - output = [] + history = {} def __init__(self, client_channel, backend_channel, client_addr): self.client_channel = client_channel @@ -219,39 +218,30 @@ class ProxyChannel: self.client_addr = client_addr self.in_input_mode = True self.is_first_input = True - self.id = 0 - - # def stream_flow(self, input_=None, output_=None): - # if input_: - # self.in_input_mode = True - # if input_ in ['\r', '\n', '\r\n']: - # self.in_input_mode = False - # - # if output_: - # print(''.join(self.__class__.output_data)) - # if not self.in_input_mode: - # command = ''.join(self.__class__.output_data) - # del self.__class__.output_data - # self.__class__.output_data = [] - # self.__class__.output_data.append(output_) + self.no = 0 + self.command = '' + self.output = '' def get_output(self): if self.in_input_mode is False: - self.__class__.output_data.pop() - result = ''.join(self.__class__.output_data) - self.__class__.output.append(result) + # self.__class__.output_data.pop() + self.output = output = ''.join(self.__class__.output_data)[:200] + self.__class__.history[self.no]['output'] = self.output + self.__class__.history[self.no]['date_finished'] = time.time() print('>>>>>>>>>>> output <<<<<<<<<<') - print(result) + print(output) print('>>>>>>>>>>> end output <<<<<<<<<<') del self.__class__.output_data self.__class__.output_data = [] + self.no += 1 + print(self.__class__.history) def get_command(self, client_data): if client_data in self.__class__.ENTER_CHAR: self.in_input_mode = False - command = ''.join(self.__class__.output_data) + self.command = command = ''.join(self.__class__.output_data) + self.__class__.history[self.no] = {'date_started': time.time(), 'command': self.command} print('########### command ##########') - self.__class__.command.append(command) print(command) print('########### end command ##########') del self.__class__.output_data @@ -268,7 +258,6 @@ class ProxyChannel: if client_channel.change_window_size_event.is_set(): backend_channel.resize_pty(width=client_channel.width, height=client_channel.height) - # print(self.__class__.output) if client_channel in r: # Get output of the command self.get_output() From 0954f6d7e8771d08d6291f7b3ef82cea7034d0b1 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 9 Oct 2016 00:12:18 +0800 Subject: [PATCH 3/6] Add audits api --- apps/audits/api.py | 12 ++++++++++++ apps/audits/migrations/__init__.py | 0 apps/audits/models.py | 18 ++++++++++++++++-- apps/audits/serializers.py | 20 ++++++++++++++++++++ apps/audits/urls.py | 15 +++++++++++++++ apps/jumpserver/urls.py | 1 + apps/users/api.py | 5 ++--- 7 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 apps/audits/migrations/__init__.py create mode 100644 apps/audits/serializers.py diff --git a/apps/audits/api.py b/apps/audits/api.py index ecbf4289f..5c3a71d9e 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -1,3 +1,15 @@ # ~*~ coding: utf-8 ~*~ # + +from rest_framework import generics + +import serializers + + +class ProxyLogCreateApi(generics.CreateAPIView): + serializer_class = serializers.ProxyLogSerializer + + +class CommandLogCreateApi(generics.CreateAPIView): + serializer_class = serializers.CommandLogSerializer diff --git a/apps/audits/migrations/__init__.py b/apps/audits/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/audits/models.py b/apps/audits/models.py index 71f0ce353..33c635d10 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -23,7 +23,7 @@ class LoginLog(models.Model): date_logout = models.DateTimeField(null=True, verbose_name=_('Date logout')) class Meta: - db_table = 'loginlog' + db_table = 'login_log' ordering = ['-date_login', 'username'] @@ -44,10 +44,24 @@ class ProxyLog(models.Model): date_start = models.DateTimeField(auto_now=True, verbose_name=_('Date start')) date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished')) + def __unicode__(self): + return '%s-%s-%s-%s' % (self.username, self.hostname, self.system_user, self.id) + + class Meta: + db_table = 'proxy_log' + ordering = ['-date_start', 'username'] + class CommandLog(models.Model): - proxy_log = models.ForeignKey(ProxyLog, on_delete=models.CASCADE, related_name='proxy_log') + proxy_log = models.ForeignKey(ProxyLog, on_delete=models.CASCADE, related_name='command_log') command = models.CharField(max_length=1000, blank=True) output = models.TextField(blank=True) date_start = models.DateTimeField(null=True) date_finished = models.DateTimeField(null=True) + + def __unicode__(self): + return '%s: %s' % (self.id, self.command) + + class Meta: + db_table = 'command_log' + ordering = ['-date_start', 'command'] diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py new file mode 100644 index 000000000..74adc8ee0 --- /dev/null +++ b/apps/audits/serializers.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +from rest_framework import serializers + +import models + + +class ProxyLogSerializer(serializers.ModelSerializer): + class Meta: + model = models.ProxyLog + + +class CommandLogSerializer(serializers.ModelSerializer): + class Meta: + model = models.CommandLog + + +if __name__ == '__main__': + pass diff --git a/apps/audits/urls.py b/apps/audits/urls.py index 39b3350fb..1cbb6f089 100644 --- a/apps/audits/urls.py +++ b/apps/audits/urls.py @@ -1 +1,16 @@ from django.conf.urls import url + + +import api +import views + +app_name = 'audits' + +urlpatterns = [ +] + + +urlpatterns += [ + url(r'^v1/proxy-log$', api.ProxyLogCreateApi.as_view(), name='proxy-log-create-api'), + url(r'^v1/command-log$', api.CommandLogCreateApi.as_view(), name='command-log-create-api'), +] diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 7a1c3ae95..f4222aa49 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -25,6 +25,7 @@ urlpatterns = [ url(r'^(api/)?users/', include('users.urls')), url(r'^assets/', include('assets.urls')), url(r'^perms/', include('perms.urls')), + url(r'^(api/)?audits/', include('audits.urls')), ] diff --git a/apps/users/api.py b/apps/users/api.py index aaf67a9a4..4302cec6e 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -1,8 +1,6 @@ # ~*~ coding: utf-8 ~*~ # -import logging - from django.shortcuts import get_object_or_404 from rest_framework import generics, status @@ -12,7 +10,8 @@ from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView from .models import User, UserGroup from .serializers import UserDetailSerializer, UserAndGroupSerializer, \ GroupDetailSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer, GroupBulkUpdateSerializer -from common.mixins import BulkDeleteApiMixin, get_logger +from common.mixins import BulkDeleteApiMixin +from common.utils import get_logger logger = get_logger(__name__) From ba82c395f26a05cf2b3669600be334a2f0b5cfa3 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 9 Oct 2016 15:14:41 +0800 Subject: [PATCH 4/6] Move terminal to a new project --- terminal/__init__.py | 7 - terminal/logs/.gitkeep | 0 terminal/ssh_config.py | 102 -------- terminal/ssh_config_example.py | 96 -------- terminal/ssh_server.py | 416 --------------------------------- terminal/utils.py | 34 --- terminal/web_ssh_server.py | 3 - 7 files changed, 658 deletions(-) delete mode 100644 terminal/__init__.py delete mode 100644 terminal/logs/.gitkeep delete mode 100644 terminal/ssh_config.py delete mode 100644 terminal/ssh_config_example.py delete mode 100644 terminal/ssh_server.py delete mode 100644 terminal/utils.py delete mode 100644 terminal/web_ssh_server.py diff --git a/terminal/__init__.py b/terminal/__init__.py deleted file mode 100644 index f93d0bec7..000000000 --- a/terminal/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - - -if __name__ == '__main__': - pass diff --git a/terminal/logs/.gitkeep b/terminal/logs/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/terminal/ssh_config.py b/terminal/ssh_config.py deleted file mode 100644 index 910f7d70d..000000000 --- a/terminal/ssh_config.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/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.handlers.TimedRotatingFileHandler', - 'formatter': 'main', - 'filename': os.path.join(LOG_DIR, LOG_FILENAME), - 'when': 'D', - 'backupCount': 10, - }, - }, - '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' - diff --git a/terminal/ssh_config_example.py b/terminal/ssh_config_example.py deleted file mode 100644 index 238304fcd..000000000 --- a/terminal/ssh_config_example.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -import logging -import os - - -BASE_DIR = os.path.dirname(os.path.abspath(__name__)) - - -class Config: - 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' - }, - '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': LOG_DIR, - }, - }, - 'loggers': { - 'jumpserver': { - 'handlers': ['console', 'file'], - # 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info') - 'level': LOG_LEVEL, - }, - '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.ssh_server': { - 'handlers': ['console', 'file'], - # 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info') - 'level': LOG_LEVEL, - } - } - } - - 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/ssh_server.py b/terminal/ssh_server.py deleted file mode 100644 index 3d4f0072d..000000000 --- a/terminal/ssh_server.py +++ /dev/null @@ -1,416 +0,0 @@ -#!/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 -# from multiprocessing.process import Process -import traceback -# import tty -# import termios -# import struct -# import fcntl -# import signal -import socket -import select -# import errno -import paramiko -import django - -BASE_DIR = os.path.abspath(os.path.dirname(__file__)) -APP_DIR = os.path.join(os.path.dirname(BASE_DIR), 'apps') -sys.path.append(APP_DIR) -os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' - -try: - django.setup() -except IndexError: - pass - -from django.conf import settings -from users.utils import ssh_key_gen, check_user_is_valid -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') - channel_pools = [] - - 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 - self.user = None - self.channel_width = None - self.channel_height = None - - @classmethod - def host_key(cls): - return cls.get_host_key() - - @classmethod - def get_host_key(cls): - 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.host_key_path, 'w') as f: - f.write(ssh_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): - self.user = user = check_user_is_valid(username=username, password=password) - 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], - 'port': self.addr[1], - }) - return paramiko.AUTH_SUCCESSFUL - else: - 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 = user = check_user_is_valid(username=username, public_key=public_key) - - 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], - 'port': self.addr[1], - }) - return paramiko.AUTH_SUCCESSFUL - else: - 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], - }) - return paramiko.AUTH_FAILED - - def get_allowed_auths(self, username): - 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() - 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, - 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 - - -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, timeout=10): - self.ssh = ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - 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, - 'host': self.host, - 'port': self.port, - }) - channel.settimeout(100) - channel.host = self.host - channel.port = self.port - channel.username = self.username - return channel - - @property - def host_password(self): - return 'redhat' - - @property - def host_private_key(self): - return None - - -class Navigation: - 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(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') - # client_channel.send(self.username) - - def display(self): - self.display_banner() - - def return_to_connect(self): - pass - - -class ProxyChannel: - ENTER_CHAR = ['\r', '\n', '\r\n'] - output_data = [] - history = {} - - def __init__(self, client_channel, backend_channel, client_addr): - self.client_channel = client_channel - self.backend_channel = backend_channel - self.client_addr = client_addr - self.in_input_mode = True - self.is_first_input = True - self.no = 0 - self.command = '' - self.output = '' - - def get_output(self): - if self.in_input_mode is False: - # self.__class__.output_data.pop() - self.output = output = ''.join(self.__class__.output_data)[:200] - self.__class__.history[self.no]['output'] = self.output - self.__class__.history[self.no]['date_finished'] = time.time() - print('>>>>>>>>>>> output <<<<<<<<<<') - print(output) - print('>>>>>>>>>>> end output <<<<<<<<<<') - del self.__class__.output_data - self.__class__.output_data = [] - self.no += 1 - print(self.__class__.history) - - def get_command(self, client_data): - if client_data in self.__class__.ENTER_CHAR: - self.in_input_mode = False - self.command = command = ''.join(self.__class__.output_data) - self.__class__.history[self.no] = {'date_started': time.time(), 'command': self.command} - print('########### command ##########') - print(command) - print('########### end command ##########') - del self.__class__.output_data - self.__class__.output_data = [] - - def proxy(self): - client_channel = self.client_channel - backend_channel = self.backend_channel - client_addr = self.client_addr - - 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: - # Get output of the command - self.get_output() - - client_data = client_channel.recv(1024) - self.in_input_mode = True - self.is_first_input = False - - # Get command input - self.get_command(client_data) - - if len(client_data) == 0: - logger.info('Logout from ssh server %(host)s: %(username)s' % { - 'host': client_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: - 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, - }) - break - if not self.is_first_input: - self.__class__.output_data.append(backend_data) - client_channel.send(backend_data) - - -class JumpServer: - backend_server_pools = [] - backend_channel_pools = [] - client_channel_pools = [] - - CONTROL_CHAR = { - 'clear': '' - } - - def __init__(self): - self.listen_host = '0.0.0.0' - self.listen_port = 2222 - - def display_navigation(self, username, client_channel): - nav = Navigation(username, client_channel) - nav.display() - return 'j', 22, 'root' - - 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 - - transport.add_server_key(SSHServer.get_host_key()) - ssh_server = SSHServer(client, addr) - - try: - transport.start_server(server=ssh_server) - except paramiko.SSHException: - logger.warning('SSH negotiation failed.') - - client_channel = transport.accept(20) - if client_channel is None: - logger.warning('No ssh channel get.') - 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 - - 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) - - 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 - - def handle_ssh_request(self, client, addr): - logger.info("Get ssh request from %(host)s:%(port)s" % { - 'host': addr[0], - 'port': addr[1], - }) - - 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') - return - - proxy_channel = ProxyChannel(client_channel, backend_channel, addr) - proxy_channel.proxy() - - # Todo: catch other exception - except IndexError: - logger.info('Close with server %s from %s' % (addr[0], addr[1])) - sys.exit(100) - - def listen(self): - 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 = sock.accept() - 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() - sys.exit(1) - - -if __name__ == '__main__': - server = JumpServer() - try: - server.listen() - except KeyboardInterrupt: - sys.exit(1) - diff --git a/terminal/utils.py b/terminal/utils.py deleted file mode 100644 index c9c4f82cf..000000000 --- a/terminal/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -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) - - -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() diff --git a/terminal/web_ssh_server.py b/terminal/web_ssh_server.py deleted file mode 100644 index 3d98261b1..000000000 --- a/terminal/web_ssh_server.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -# - From 0c8922e30f865427e31b37ab68afc624470fd527 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 9 Oct 2016 19:27:49 +0800 Subject: [PATCH 5/6] Modify some bug --- apps/users/utils.py | 1 + requirements.txt | 12 ------------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/users/utils.py b/apps/users/utils.py index 2ec463e7e..de21a1539 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import logging import os +import re from django.conf import settings from django.contrib.auth.mixins import UserPassesTestMixin diff --git a/requirements.txt b/requirements.txt index da30cd976..4b3926c50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,13 @@ Django==1.10 django-bootstrap-form==3.2.1 -django-redis-sessions==0.5.6 -django-websocket-redis==0.4.6 -gevent==1.1.2 -greenlet==0.4.10 logging==0.4.9.6 Pillow==3.3.1 -pyte==0.5.2 -redis==2.10.5 -six==1.10.0 -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 django-simple-captcha==0.5.2 django-formtools==1.0 sshpubkeys==2.2.0 djangorestframework-bulk==0.2.1 -python-gssapi==0.6.4 -tornado==4.4.2 From 081af2f9532aa14683cd42afac92156212cb043b Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 10 Oct 2016 00:39:24 +0800 Subject: [PATCH 6/6] Add proxy log api for create or update --- apps/audits/api.py | 10 +++++++++- apps/audits/models.py | 1 + apps/audits/urls.py | 5 +++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/audits/api.py b/apps/audits/api.py index 5c3a71d9e..33186e0cc 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -6,8 +6,16 @@ from rest_framework import generics import serializers +from .models import ProxyLog -class ProxyLogCreateApi(generics.CreateAPIView): + +class ProxyLogListCreateApi(generics.ListCreateAPIView): + queryset = ProxyLog.objects.all() + serializer_class = serializers.ProxyLogSerializer + + +class ProxyLogDetailApi(generics.RetrieveUpdateDestroyAPIView): + queryset = ProxyLog.objects.all() serializer_class = serializers.ProxyLogSerializer diff --git a/apps/audits/models.py b/apps/audits/models.py index 33c635d10..eb6e5baeb 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -40,6 +40,7 @@ class ProxyLog(models.Model): system_user = models.CharField(max_length=20, verbose_name=_('System user')) login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=1, verbose_name=_('Login type')) log_file = models.CharField(max_length=1000, blank=True, null=True) + was_failed = models.BooleanField(default=False, verbose_name=_('Did connect failed')) is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) date_start = models.DateTimeField(auto_now=True, verbose_name=_('Date start')) date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished')) diff --git a/apps/audits/urls.py b/apps/audits/urls.py index 1cbb6f089..982f7fc94 100644 --- a/apps/audits/urls.py +++ b/apps/audits/urls.py @@ -11,6 +11,7 @@ urlpatterns = [ urlpatterns += [ - url(r'^v1/proxy-log$', api.ProxyLogCreateApi.as_view(), name='proxy-log-create-api'), - url(r'^v1/command-log$', api.CommandLogCreateApi.as_view(), name='command-log-create-api'), + url(r'^v1/proxy-log/$', api.ProxyLogListCreateApi.as_view(), name='proxy-log-list-create-api'), + url(r'^v1/proxy-log/(?P\d+)/$', api.ProxyLogDetailApi.as_view(), name='proxy-log-detail-api'), + url(r'^v1/command-log/$', api.CommandLogCreateApi.as_view(), name='command-log-create-api'), ]