import base64 import json import os import urllib.parse from struct import pack from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.serialization import pkcs7 from django.conf import settings from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework import status, serializers from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.request import Request from rest_framework.response import Response from accounts.const import AliasAccount from accounts.utils import validate_account_username from acls.notifications import AssetLoginReminderMsg from common.api import JMSModelViewSet from common.exceptions import JMSException from common.utils import random_string, get_logger, get_request_ip_or_data from common.utils.django import get_request_os from common.utils.http import is_true, is_false from orgs.mixins.api import RootOrgViewMixin from orgs.utils import tmp_to_org from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil, WebMethod from terminal.models import EndpointRule, Endpoint from users.const import FileNameConflictResolution from users.const import RDPSmartSize, RDPColorQuality from users.models import Preference from .face import FaceMonitorContext from ..mixins import AuthFaceMixin from ..models import ConnectionToken, AdminConnectionToken, date_expired_default from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer, ConnectionTokenReusableSerializer, ConnectTokenVirtualAppOptionSerializer, AdminConnectionTokenSerializer, ) __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet', 'AdminConnectionTokenViewSet'] logger = get_logger(__name__) class RDPFileClientProtocolURLMixin: request: Request get_serializer: callable RDP_SIGN_SECURE_SETTINGS = [ ('full address:s:', 'Full Address'), ('alternate full address:s:', 'Alternate Full Address'), ('pcb:s:', 'PCB'), ('use redirection server name:i:', 'Use Redirection Server Name'), ('server port:i:', 'Server Port'), ('negotiate security layer:i:', 'Negotiate Security Layer'), ('enablecredsspsupport:i:', 'EnableCredSspSupport'), ('disableconnectionsharing:i:', 'DisableConnectionSharing'), ('autoreconnection enabled:i:', 'AutoReconnection Enabled'), ('gatewayhostname:s:', 'GatewayHostname'), ('gatewayusagemethod:i:', 'GatewayUsageMethod'), ('gatewayprofileusagemethod:i:', 'GatewayProfileUsageMethod'), ('gatewaycredentialssource:i:', 'GatewayCredentialsSource'), ('support url:s:', 'Support URL'), ('promptcredentialonce:i:', 'PromptCredentialOnce'), ('require pre-authentication:i:', 'Require pre-authentication'), ('pre-authentication server address:s:', 'Pre-authentication server address'), ('alternate shell:s:', 'Alternate Shell'), ('shell working directory:s:', 'Shell Working Directory'), ('remoteapplicationprogram:s:', 'RemoteApplicationProgram'), ('remoteapplicationexpandworkingdir:s:', 'RemoteApplicationExpandWorkingdir'), ('remoteapplicationmode:i:', 'RemoteApplicationMode'), ('remoteapplicationguid:s:', 'RemoteApplicationGuid'), ('remoteapplicationname:s:', 'RemoteApplicationName'), ('remoteapplicationicon:s:', 'RemoteApplicationIcon'), ('remoteapplicationfile:s:', 'RemoteApplicationFile'), ('remoteapplicationfileextensions:s:', 'RemoteApplicationFileExtensions'), ('remoteapplicationcmdline:s:', 'RemoteApplicationCmdLine'), ('remoteapplicationexpandcmdline:s:', 'RemoteApplicationExpandCmdLine'), ('prompt for credentials:i:', 'Prompt For Credentials'), ('authentication level:i:', 'Authentication Level'), ('audiomode:i:', 'AudioMode'), ('redirectdrives:i:', 'RedirectDrives'), ('redirectprinters:i:', 'RedirectPrinters'), ('redirectcomports:i:', 'RedirectCOMPorts'), ('redirectsmartcards:i:', 'RedirectSmartCards'), ('redirectposdevices:i:', 'RedirectPOSDevices'), ('redirectclipboard:i:', 'RedirectClipboard'), ('devicestoredirect:s:', 'DevicesToRedirect'), ('drivestoredirect:s:', 'DrivesToRedirect'), ('loadbalanceinfo:s:', 'LoadBalanceInfo'), ('redirectdirectx:i:', 'RedirectDirectX'), ('rdgiskdcproxy:i:', 'RDGIsKDCProxy'), ('kdcproxyname:s:', 'KDCProxyName'), ('eventloguploadaddress:s:', 'EventLogUploadAddress'), ('redirectwebauthn:i:', 'RedirectWebAuthn'), ] @classmethod def _collect_rdp_sign_lines(cls, settings_lines): signnames = [] signlines = [] for prefix, sign_name in cls.RDP_SIGN_SECURE_SETTINGS: for line in settings_lines: if line.startswith(prefix): signnames.append(sign_name) signlines.append(line) return signnames, signlines @classmethod def _try_sign_rdp_content(cls, content): if not settings.RDP_SIGN_ENABLED: return content cert_dir = os.path.join(settings.PROJECT_DIR, 'data', 'certs') if not os.path.exists(cert_dir): logger.error(f'rdp sign cert dir [{cert_dir}] not exists') return content certfile = os.path.join(cert_dir, settings.RDP_SIGN_CERT) if not os.path.exists(certfile): logger.error(f'rdp sign cert file [{certfile}] not exists') return content keyfile = os.path.join(cert_dir, settings.RDP_SIGN_CERT_KEY) if not os.path.exists(keyfile): logger.warning(f'rdp sign cert file [{keyfile}] not exists') keyfile = None return content settings_lines = [] full_address = None alternate_full_address = None for line in content.splitlines(): line = line.strip() if not line: continue if line.startswith('signature:s:') or line.startswith('signscope:s:'): continue if line.startswith('full address:s:'): full_address = line[15:] elif line.startswith('alternate full address:s:'): alternate_full_address = line[25:] settings_lines.append(line) # Keep alternate full address aligned with full address to prevent tampering. if full_address and not alternate_full_address: settings_lines.append(f'alternate full address:s:{full_address}') signnames, signlines = cls._collect_rdp_sign_lines(settings_lines) if not signnames or not signlines: return content msgtext = '\r\n'.join(signlines) + '\r\n' + 'signscope:s:' + ','.join(signnames) + '\r\n' + '\x00' msgblob = msgtext.encode('UTF-16LE') try: with open(certfile, 'rb') as f: cert_pem = f.read() cert = x509.load_pem_x509_certificate(cert_pem) if keyfile: with open(keyfile, 'rb') as f: key_pem = f.read() else: key_pem = cert_pem private_key = serialization.load_pem_private_key(key_pem, password=None) pkcs7_der = ( pkcs7.PKCS7SignatureBuilder() .set_data(msgblob) .add_signer(cert, private_key, hashes.SHA256()) .sign( serialization.Encoding.DER, [ pkcs7.PKCS7Options.Binary, pkcs7.PKCS7Options.DetachedSignature, pkcs7.PKCS7Options.NoAttributes, ], ) ) except OSError as e: logger.warning('RDP file sign failed to read cert/key: %s', e) return content except ValueError as e: logger.warning('RDP file sign failed (invalid cert/key PEM): %s', e) return content except Exception as e: logger.warning('RDP file sign failed: %s', e) return content msgsig = pack('