Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM jumpserver/lion-base:20260701_083042 AS stage-build
FROM jumpserver/lion-base:20260707_101243 AS stage-build
ARG TARGETARCH

ARG GOPROXY=https://goproxy.io
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/gin-gonic/gin v1.11.0
github.com/go-redis/redis/v8 v8.11.5
github.com/gorilla/websocket v1.5.3
github.com/jumpserver-dev/sdk-go v0.0.0-20260520022428-47f0d4d20dc6
github.com/jumpserver-dev/sdk-go v0.0.0-20260707072327-59c76e43904f
github.com/spf13/viper v1.21.0
golang.org/x/crypto v0.47.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/jumpserver-dev/sdk-go v0.0.0-20260520022428-47f0d4d20dc6 h1:K3DJEekApUoQUuVX9x6U8pvP/g7qaHy8TcCVmsIlf64=
github.com/jumpserver-dev/sdk-go v0.0.0-20260520022428-47f0d4d20dc6/go.mod h1:QmnL7BRb74/XY1nu8kWQWs6DHXt3uIaWkDCkQSRyZlM=
github.com/jumpserver-dev/sdk-go v0.0.0-20260707072327-59c76e43904f h1:7C5PoXuDuux091WUO3IGGfvKfQt7jGYZXWTSA8JovlU=
github.com/jumpserver-dev/sdk-go v0.0.0-20260707072327-59c76e43904f/go.mod h1:QmnL7BRb74/XY1nu8kWQWs6DHXt3uIaWkDCkQSRyZlM=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand Down
22 changes: 21 additions & 1 deletion pkg/session/permisson.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ type ActionPermission struct {
EnableUpload bool `json:"enable_upload"`
EnableDownload bool `json:"enable_download"`
EnableShare bool `json:"enable_share"`

ClipboardPolicy *model.ClipboardPolicy `json:"clipboard_policy,omitempty"`
}

func NewActionPermission(perm *model.Permission, connectType string) *ActionPermission {
func NewActionPermission(perm *model.Permission, connectType string,
clipboardPolicy *model.ClipboardPolicy) *ActionPermission {
action := ActionPermission{
EnableConnect: perm.EnableConnect(),
EnableCopy: perm.EnableCopy(),
Expand Down Expand Up @@ -47,5 +50,22 @@ func NewActionPermission(perm *model.Permission, connectType string) *ActionPerm
action.EnablePaste = false
action.EnableCopy = false
}
action.applyClipboardPolicy(clipboardPolicy)
return &action
}

func (a *ActionPermission) applyClipboardPolicy(policy *model.ClipboardPolicy) {
if policy == nil {
return
}
a.ClipboardPolicy = policy
a.EnableCopy = a.EnableCopy && clipboardPolicyItemEnabled(policy.Copy)
a.EnablePaste = a.EnablePaste && clipboardPolicyItemEnabled(policy.Paste)
}

func clipboardPolicyItemEnabled(item *model.ClipboardPolicyItem) bool {
if item == nil {
return true
}
return item.Enabled
}
2 changes: 1 addition & 1 deletion pkg/session/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ func (s *Server) Create(ctx *gin.Context, opts ...TunnelOption) (sess TunnelSess
sess.ExpireInfo = opt.ExpireInfo
sess.Permission = &perm
sess.Account = opt.Account
sess.ActionPerm = NewActionPermission(&perm, targetType)
sess.ActionPerm = NewActionPermission(&perm, targetType, opt.authInfo.ClipboardPolicy)
jmsSession := model.Session{
ID: sess.ID,
User: sess.User.String(),
Expand Down
138 changes: 138 additions & 0 deletions pkg/tunnel/clipboard_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package tunnel

import (
"encoding/base64"
"math"
"strings"
"unicode/utf8"

"lion/pkg/guacd"
"lion/pkg/session"
)

type clipboardTransfer struct {
index string
text bool
allow bool
limit int
size int
}

const bytesPerMegabyte = 1024 * 1024

type clipboardPolicyFilter struct {
perm *session.ActionPermission
toClient map[string]*clipboardTransfer
toServer map[string]*clipboardTransfer
}

func newClipboardPolicyFilter(perm *session.ActionPermission) *clipboardPolicyFilter {
return &clipboardPolicyFilter{
perm: perm,
toClient: map[string]*clipboardTransfer{},
toServer: map[string]*clipboardTransfer{},
}
}

func (f *clipboardPolicyFilter) filterToClient(instruction *guacd.Instruction) *guacd.Instruction {
return f.filter(instruction, f.toClient, true)
}

func (f *clipboardPolicyFilter) filterToServer(instruction *guacd.Instruction) *guacd.Instruction {
return f.filter(instruction, f.toServer, false)
}

func (f *clipboardPolicyFilter) streamPolicy(toClient, text bool) (bool, int) {
if f == nil || f.perm == nil {
return true, 0
}
enabled := f.perm.EnablePaste
if toClient {
enabled = f.perm.EnableCopy
}
if f.perm.ClipboardPolicy == nil {
return enabled, 0
}
item := f.perm.ClipboardPolicy.Paste
if toClient {
item = f.perm.ClipboardPolicy.Copy
}
if item == nil {
return enabled, 0
}
if text {
return enabled, item.TextLimit
}
return enabled, fileSizeLimitBytes(item.FileSizeLimit)
}

func fileSizeLimitBytes(limitMB int) int {
if limitMB <= 0 {
return 0
}
if limitMB > math.MaxInt/bytesPerMegabyte {
return math.MaxInt
}
return limitMB * bytesPerMegabyte
}

func (f *clipboardPolicyFilter) filter(instruction *guacd.Instruction,
transfers map[string]*clipboardTransfer, toClient bool) *guacd.Instruction {
if f == nil || f.perm == nil || instruction == nil {
return instruction
}
switch instruction.Opcode {
case guacd.InstructionStreamingClipboard:
if len(instruction.Args) < 2 {
return instruction
}
text := strings.HasPrefix(instruction.Args[1], "text/")
allow, limit := f.streamPolicy(toClient, text)
transfer := &clipboardTransfer{
index: instruction.Args[0],
text: text,
allow: allow,
limit: limit,
}
transfers[transfer.index] = transfer
if !transfer.allow {
return nil
}
case guacd.InstructionStreamingBlob:
if len(instruction.Args) < 2 {
return instruction
}
transfer := transfers[instruction.Args[0]]
if transfer == nil {
return instruction
}
if !transfer.allow {
return nil
}
if transfer.limit > 0 {
blob, err := base64.StdEncoding.DecodeString(instruction.Args[1])
if err != nil {
transfer.allow = false
return nil
}
if transfer.text {
transfer.size += utf8.RuneCount(blob)
} else {
transfer.size += len(blob)
}
if transfer.size > transfer.limit {
transfer.allow = false
return nil
}
}
case guacd.InstructionStreamingEnd:
if len(instruction.Args) > 0 {
transfer := transfers[instruction.Args[0]]
delete(transfers, instruction.Args[0])
if transfer != nil && !transfer.allow {
return nil
}
}
}
return instruction
}
15 changes: 15 additions & 0 deletions pkg/tunnel/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type Connection struct {

inputFilter *InputStreamInterceptingFilter

clipboardFilter *clipboardPolicyFilter

done chan struct{}

traceLock sync.Mutex
Expand Down Expand Up @@ -119,6 +121,12 @@ func (t *Connection) readTunnelInstruction() (*guacd.Instruction, error) {
continue
}
}
if t.clipboardFilter != nil {
newInstruction = t.clipboardFilter.filterToClient(newInstruction)
if newInstruction == nil {
continue
}
}
return newInstruction, nil
}

Expand Down Expand Up @@ -293,6 +301,13 @@ func (t *Connection) Run(ctx *gin.Context) (err error) {
default:
}
}
if t.clipboardFilter != nil {
filtered := t.clipboardFilter.filterToServer(&ret)
if filtered == nil {
continue
}
message = []byte(filtered.String())
}
} else {
logger.Errorf("Session[%s] parse instruction err %s", t, err)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/tunnel/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ func (g *GuacamoleTunnelServer) Connect(ctx *gin.Context) {
}
conn.outputFilter = &outFilter
conn.inputFilter = &inputFilter
conn.clipboardFilter = newClipboardPolicyFilter(tunnelSession.ActionPerm)
logger.Infof("Session[%s] connect success", sessionId)
g.Cache.Add(&conn)
replayRecorder := &ReplayRecorder{
Expand Down
40 changes: 34 additions & 6 deletions ui/src/components/ClipBoardText.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { readClipboardText } from '@/utils/clipboard';
import { useDebounceFn } from '@vueuse/core';
import { NInput } from 'naive-ui';
Expand All @@ -15,15 +15,42 @@ const message = useMessage();
const props = defineProps<{
remoteText?: string;
disabled?: boolean;
copyDisabled?: boolean;
pasteDisabled?: boolean;
textLimit?: number;
}>();

const showRemoteText = ref<boolean>(false);
const defaultMaxlength = 1024 * 4;
const maxlength = computed(() => {
if (!props.textLimit || props.textLimit <= 0 || props.textLimit > defaultMaxlength) {
return defaultMaxlength;
}
return props.textLimit;
});

const getTextLength = (text: string) => Array.from(text).length;

const validateTextLimit = (text: string) => {
if (props.disabled || props.pasteDisabled) {
message.warning(`${t('Paste')} ${t('NoPermission')}`);
return false;
}
if (getTextLength(text) > maxlength.value) {
message.warning(`${t('Paste')} ${t('ClipboardTextLimitExceeded')}: ${maxlength.value}`);
return false;
}
return true;
};

// 手动读取剪贴板内容
const loadClipboardText = async () => {
try {
isLoading.value = true;
const text = await readClipboardText();
if (!validateTextLimit(text)) {
return;
}
inputValue.value = text;
await handleInput(text);
} catch (error) {
Expand All @@ -36,6 +63,9 @@ const loadClipboardText = async () => {

// 处理输入事件
const handleInput = useDebounceFn((value: string) => {
if (!validateTextLimit(value)) {
return;
}
emit('update:text', value);
}, 300);

Expand All @@ -60,14 +90,12 @@ const size = {
minRows: 4,
maxRows: 6,
};

const maxlength = 1024 * 4;
</script>

<template>
<CardContainer :title="t('Clipboard')">
<n-form-item :label="t('ShowRemoteClip')" label-placement="left">
<n-switch v-model:value="showRemoteText" :disabled="props.disabled" />
<n-switch v-model:value="showRemoteText" :disabled="props.disabled || props.copyDisabled" />
</n-form-item>
<n-input
v-model:value="inputValue"
Expand All @@ -80,7 +108,7 @@ const maxlength = 1024 * 4;
show-count
clearable
:placeholder="t('AutoPasteOnClick')"
:disabled="props.disabled"
:disabled="props.disabled || props.pasteDisabled"
>
</n-input>
<n-form-item v-if="showRemoteText">
Expand All @@ -91,7 +119,7 @@ const maxlength = 1024 * 4;
readonly
placeholder=""
show-count
:disabled="props.disabled"
:disabled="props.disabled || props.copyDisabled"
/>
</n-form-item>
</CardContainer>
Expand Down
Loading