diff --git a/Dockerfile b/Dockerfile index 2b4e50a3..68d1f233 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/go.mod b/go.mod index 4ec364df..e8ad729d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index fd36ce16..fdbd3769 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/session/permisson.go b/pkg/session/permisson.go index cb26f41e..31b65d75 100644 --- a/pkg/session/permisson.go +++ b/pkg/session/permisson.go @@ -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(), @@ -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 +} diff --git a/pkg/session/server.go b/pkg/session/server.go index 0019cbed..81cf2204 100644 --- a/pkg/session/server.go +++ b/pkg/session/server.go @@ -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(), diff --git a/pkg/tunnel/clipboard_policy.go b/pkg/tunnel/clipboard_policy.go new file mode 100644 index 00000000..3ecdf9f5 --- /dev/null +++ b/pkg/tunnel/clipboard_policy.go @@ -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 +} diff --git a/pkg/tunnel/conn.go b/pkg/tunnel/conn.go index cc12fb62..fbf9f1e3 100644 --- a/pkg/tunnel/conn.go +++ b/pkg/tunnel/conn.go @@ -59,6 +59,8 @@ type Connection struct { inputFilter *InputStreamInterceptingFilter + clipboardFilter *clipboardPolicyFilter + done chan struct{} traceLock sync.Mutex @@ -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 } @@ -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) } diff --git a/pkg/tunnel/server.go b/pkg/tunnel/server.go index d2a0fe59..82d3a782 100644 --- a/pkg/tunnel/server.go +++ b/pkg/tunnel/server.go @@ -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{ diff --git a/ui/src/components/ClipBoardText.vue b/ui/src/components/ClipBoardText.vue index d0e30df8..f2fa0073 100644 --- a/ui/src/components/ClipBoardText.vue +++ b/ui/src/components/ClipBoardText.vue @@ -1,5 +1,5 @@
- +