mirror of https://github.com/jumpserver/jumpserver
				
				
				
			feat: 录像增加 cast 格式 (#7259)
* feat: 录像增加 cast 格式 * perf: 优化一下写法 * perf: 修改一点点 Co-authored-by: ibuler <ibuler@qq.com>pull/7343/head
							parent
							
								
									d72aa34513
								
							
						
					
					
						commit
						0afeed0ff1
					
				| 
						 | 
				
			
			@ -28,7 +28,6 @@ from ..hands import SystemUser
 | 
			
		|||
from ..models import Session
 | 
			
		||||
from .. import serializers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'SessionViewSet', 'SessionReplayViewSet', 'SessionJoinValidateAPI'
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +40,7 @@ class SessionViewSet(OrgBulkModelViewSet):
 | 
			
		|||
        'default': serializers.SessionSerializer,
 | 
			
		||||
        'display': serializers.SessionDisplaySerializer,
 | 
			
		||||
    }
 | 
			
		||||
    permission_classes = (IsOrgAdminOrAppUser, )
 | 
			
		||||
    permission_classes = (IsOrgAdminOrAppUser,)
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        "user", "asset", "system_user", "remote_addr", "protocol", "is_finished", 'login_from',
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +70,8 @@ class SessionViewSet(OrgBulkModelViewSet):
 | 
			
		|||
        os.chdir(current_dir)
 | 
			
		||||
        return file
 | 
			
		||||
 | 
			
		||||
    @action(methods=[GET], detail=True, renderer_classes=(PassthroughRenderer,), url_path='replay/download', url_name='replay-download')
 | 
			
		||||
    @action(methods=[GET], detail=True, renderer_classes=(PassthroughRenderer,), url_path='replay/download',
 | 
			
		||||
            url_name='replay-download')
 | 
			
		||||
    def download(self, request, *args, **kwargs):
 | 
			
		||||
        session = self.get_object()
 | 
			
		||||
        local_path, url = utils.get_session_replay_url(session)
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +102,7 @@ class SessionViewSet(OrgBulkModelViewSet):
 | 
			
		|||
 | 
			
		||||
    def get_permissions(self):
 | 
			
		||||
        if self.request.method.lower() in ['get', 'options']:
 | 
			
		||||
            self.permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor, )
 | 
			
		||||
            self.permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,)
 | 
			
		||||
        return super().get_permissions()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +119,9 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):
 | 
			
		|||
 | 
			
		||||
        if serializer.is_valid():
 | 
			
		||||
            file = serializer.validated_data['file']
 | 
			
		||||
            name, err = session.save_replay_to_storage(file)
 | 
			
		||||
            # 兼容旧版本 API 未指定 version 为 2 的情况
 | 
			
		||||
            version = serializer.validated_data.get('version', 2)
 | 
			
		||||
            name, err = session.save_replay_to_storage_with_version(file, version)
 | 
			
		||||
            if not name:
 | 
			
		||||
                msg = "Failed save replay `{}`: {}".format(session_id, err)
 | 
			
		||||
                logger.error(msg)
 | 
			
		||||
| 
						 | 
				
			
			@ -137,6 +139,8 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):
 | 
			
		|||
        if session.protocol in ('rdp', 'vnc'):
 | 
			
		||||
            # 需要考虑录像播放和离线播放器的约定,暂时不处理
 | 
			
		||||
            tp = 'guacamole'
 | 
			
		||||
        if url.endswith('.cast.gz'):
 | 
			
		||||
            tp = 'asciicast'
 | 
			
		||||
 | 
			
		||||
        download_url = reverse('api-terminal:session-replay-download', kwargs={'pk': session.id})
 | 
			
		||||
        data = {
 | 
			
		||||
| 
						 | 
				
			
			@ -168,7 +172,7 @@ class SessionReplayViewSet(AsyncApiMixin, viewsets.ViewSet):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class SessionJoinValidateAPI(views.APIView):
 | 
			
		||||
    permission_classes = (IsAppUser, )
 | 
			
		||||
    permission_classes = (IsAppUser,)
 | 
			
		||||
    serializer_class = serializers.SessionJoinValidateSerializer
 | 
			
		||||
 | 
			
		||||
    def post(self, request, *args, **kwargs):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,26 +56,65 @@ class Session(OrgModelMixin):
 | 
			
		|||
    upload_to = 'replay'
 | 
			
		||||
    ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}'
 | 
			
		||||
    _DATE_START_FIRST_HAS_REPLAY_RDP_SESSION = None
 | 
			
		||||
    SUFFIX_MAP = {1: '.gz', 2: '.replay.gz', 3: '.cast.gz'}
 | 
			
		||||
    DEFAULT_SUFFIXES = ['.replay.gz', '.cast.gz', '.gz']
 | 
			
		||||
 | 
			
		||||
    def get_rel_replay_path(self, version=2):
 | 
			
		||||
    # Todo: 将来干掉 local_path, 使用 default storage 实现
 | 
			
		||||
    def get_all_possible_local_path(self):
 | 
			
		||||
        """
 | 
			
		||||
        获取session日志的文件路径
 | 
			
		||||
        :param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz
 | 
			
		||||
        获取所有可能的本地存储录像文件路径
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        return [self.get_local_storage_path_by_suffix(suffix)
 | 
			
		||||
                for suffix in self.SUFFIX_MAP.values()]
 | 
			
		||||
 | 
			
		||||
    def get_all_possible_relative_path(self):
 | 
			
		||||
        """
 | 
			
		||||
        获取所有可能的外部存储录像文件路径
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        return [self.get_relative_path_by_suffix(suffix)
 | 
			
		||||
                for suffix in self.SUFFIX_MAP.values()]
 | 
			
		||||
 | 
			
		||||
    def get_local_storage_path_by_suffix(self, suffix='.cast.gz'):
 | 
			
		||||
        """
 | 
			
		||||
        local_path: replay/2021-12-08/session_id.cast.gz
 | 
			
		||||
        通过后缀名获取本地存储的录像文件路径
 | 
			
		||||
        :param suffix: .cast.gz | '.replay.gz' | '.gz'
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        rel_path = self.get_relative_path_by_suffix(suffix)
 | 
			
		||||
        if suffix == '.gz':
 | 
			
		||||
            # 兼容 v1 的版本
 | 
			
		||||
            return rel_path
 | 
			
		||||
        return os.path.join(self.upload_to, rel_path)
 | 
			
		||||
 | 
			
		||||
    def get_relative_path_by_suffix(self, suffix='.cast.gz'):
 | 
			
		||||
        """
 | 
			
		||||
        relative_path: 2021-12-08/session_id.cast.gz
 | 
			
		||||
        通过后缀名获取外部存储录像文件路径
 | 
			
		||||
        :param suffix: .cast.gz | '.replay.gz' | '.gz'
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        suffix = '.replay.gz'
 | 
			
		||||
        if version == 1:
 | 
			
		||||
            suffix = '.gz'
 | 
			
		||||
        date = self.date_start.strftime('%Y-%m-%d')
 | 
			
		||||
        return os.path.join(date, str(self.id) + suffix)
 | 
			
		||||
 | 
			
		||||
    def get_local_path(self, version=2):
 | 
			
		||||
        rel_path = self.get_rel_replay_path(version=version)
 | 
			
		||||
        if version == 2:
 | 
			
		||||
            local_path = os.path.join(self.upload_to, rel_path)
 | 
			
		||||
        else:
 | 
			
		||||
            local_path = rel_path
 | 
			
		||||
        return local_path
 | 
			
		||||
    def get_local_path_by_relative_path(self, rel_path):
 | 
			
		||||
        """
 | 
			
		||||
        2021-12-08/session_id.cast.gz
 | 
			
		||||
        :param rel_path:
 | 
			
		||||
        :return: replay/2021-12-08/session_id.cast.gz
 | 
			
		||||
        """
 | 
			
		||||
        return '{}/{}'.format(self.upload_to, rel_path)
 | 
			
		||||
 | 
			
		||||
    def get_relative_path_by_local_path(self, local_path):
 | 
			
		||||
        return local_path.replace('{}/'.format(self.upload_to), '')
 | 
			
		||||
 | 
			
		||||
    def find_ok_relative_path_in_storage(self, storage):
 | 
			
		||||
        session_paths = self.get_all_possible_relative_path()
 | 
			
		||||
        for rel_path in session_paths:
 | 
			
		||||
            if storage.exists(rel_path):
 | 
			
		||||
                return rel_path
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def asset_obj(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -133,8 +172,9 @@ class Session(OrgModelMixin):
 | 
			
		|||
        else:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
    def save_replay_to_storage(self, f):
 | 
			
		||||
        local_path = self.get_local_path()
 | 
			
		||||
    def save_replay_to_storage_with_version(self, f, version=2):
 | 
			
		||||
        suffix = self.SUFFIX_MAP.get(version, '.cast.gz')
 | 
			
		||||
        local_path = self.get_local_storage_path_by_suffix(suffix)
 | 
			
		||||
        try:
 | 
			
		||||
            name = default_storage.save(local_path, f)
 | 
			
		||||
        except OSError as e:
 | 
			
		||||
| 
						 | 
				
			
			@ -148,7 +188,7 @@ class Session(OrgModelMixin):
 | 
			
		|||
    @classmethod
 | 
			
		||||
    def set_sessions_active(cls, session_ids):
 | 
			
		||||
        data = {cls.ACTIVE_CACHE_KEY_PREFIX.format(i): i for i in session_ids}
 | 
			
		||||
        cache.set_many(data, timeout=5*60)
 | 
			
		||||
        cache.set_many(data, timeout=5 * 60)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_active_sessions(cls):
 | 
			
		||||
| 
						 | 
				
			
			@ -195,7 +235,7 @@ class Session(OrgModelMixin):
 | 
			
		|||
            for user, asset, system_user in ziped:
 | 
			
		||||
                ip = random_ip()
 | 
			
		||||
                date_start = random_datetime(month_ago, now)
 | 
			
		||||
                date_end = random_datetime(date_start, date_start+timezone.timedelta(hours=2))
 | 
			
		||||
                date_end = random_datetime(date_start, date_start + timezone.timedelta(hours=2))
 | 
			
		||||
                data = dict(
 | 
			
		||||
                    user=str(user), user_id=user.id,
 | 
			
		||||
                    asset=str(asset), asset_id=asset.id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,7 @@ class SessionDisplaySerializer(SessionSerializer):
 | 
			
		|||
 | 
			
		||||
class ReplaySerializer(serializers.Serializer):
 | 
			
		||||
    file = serializers.FileField(allow_empty_file=True)
 | 
			
		||||
    version = serializers.IntegerField(write_only=True, required=False, min_value=2, max_value=3)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SessionJoinValidateSerializer(serializers.Serializer):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,7 +85,7 @@ def upload_session_replay_to_external_storage(session_id):
 | 
			
		|||
        logger.error(f'Session replay not found, may be upload error: {local_path}')
 | 
			
		||||
        return
 | 
			
		||||
    abs_path = default_storage.path(local_path)
 | 
			
		||||
    remote_path = session.get_rel_replay_path()
 | 
			
		||||
    remote_path = session.get_relative_path_by_local_path(abs_path)
 | 
			
		||||
    ok, err = server_replay_storage.upload(abs_path, remote_path)
 | 
			
		||||
    if not ok:
 | 
			
		||||
        logger.error(f'Session replay upload to external error: {err}')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,30 +1,28 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
import os
 | 
			
		||||
from itertools import groupby
 | 
			
		||||
from itertools import groupby, chain
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.files.storage import default_storage
 | 
			
		||||
from django.utils.translation import ugettext as _
 | 
			
		||||
 | 
			
		||||
import jms_storage
 | 
			
		||||
 | 
			
		||||
from common.tasks import send_mail_async
 | 
			
		||||
from common.utils import get_logger, reverse
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
from . import const
 | 
			
		||||
from .models import ReplayStorage, Session, Command
 | 
			
		||||
from .models import ReplayStorage
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def find_session_replay_local(session):
 | 
			
		||||
    # 新版本和老版本的文件后缀不同
 | 
			
		||||
    session_path = session.get_rel_replay_path()  # 存在外部存储上的路径
 | 
			
		||||
    local_path = session.get_local_path()
 | 
			
		||||
    local_path_v1 = session.get_local_path(version=1)
 | 
			
		||||
    # 存在外部存储上,所有可能的路径名
 | 
			
		||||
    session_paths = session.get_all_possible_relative_path()
 | 
			
		||||
 | 
			
		||||
    # 去default storage中查找
 | 
			
		||||
    for _local_path in (local_path, local_path_v1, session_path):
 | 
			
		||||
    # 存在本地存储上,所有可能的路径名
 | 
			
		||||
    local_paths = session.get_all_possible_local_path()
 | 
			
		||||
 | 
			
		||||
    for _local_path in chain(session_paths, local_paths):
 | 
			
		||||
        if default_storage.exists(_local_path):
 | 
			
		||||
            url = default_storage.url(_local_path)
 | 
			
		||||
            return _local_path, url
 | 
			
		||||
| 
						 | 
				
			
			@ -32,8 +30,6 @@ def find_session_replay_local(session):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def download_session_replay(session):
 | 
			
		||||
    session_path = session.get_rel_replay_path()  # 存在外部存储上的路径
 | 
			
		||||
    local_path = session.get_local_path()
 | 
			
		||||
    replay_storages = ReplayStorage.objects.all()
 | 
			
		||||
    configs = {
 | 
			
		||||
        storage.name: storage.config
 | 
			
		||||
| 
						 | 
				
			
			@ -45,13 +41,23 @@ def download_session_replay(session):
 | 
			
		|||
    if not configs:
 | 
			
		||||
        msg = "Not found replay file, and not remote storage set"
 | 
			
		||||
        return None, msg
 | 
			
		||||
    storage = jms_storage.get_multi_object_storage(configs)
 | 
			
		||||
 | 
			
		||||
    # 获取外部存储路径名
 | 
			
		||||
    session_path = session.find_ok_relative_path_in_storage(storage)
 | 
			
		||||
    if not session_path:
 | 
			
		||||
        msg = "Not found session replay file"
 | 
			
		||||
        return None, msg
 | 
			
		||||
 | 
			
		||||
    # 通过外部存储路径名后缀,构造真实的本地存储路径
 | 
			
		||||
    local_path = session.get_local_path_by_relative_path(session_path)
 | 
			
		||||
 | 
			
		||||
    # 保存到storage的路径
 | 
			
		||||
    target_path = os.path.join(default_storage.base_location, local_path)
 | 
			
		||||
    target_dir = os.path.dirname(target_path)
 | 
			
		||||
    if not os.path.isdir(target_dir):
 | 
			
		||||
        os.makedirs(target_dir, exist_ok=True)
 | 
			
		||||
    storage = jms_storage.get_multi_object_storage(configs)
 | 
			
		||||
 | 
			
		||||
    ok, err = storage.download(session_path, target_path)
 | 
			
		||||
    if not ok:
 | 
			
		||||
        msg = "Failed download replay file: {}".format(err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue