├── .dockerignore
├── ui
├── src
│ ├── locales
│ │ ├── modules
│ │ │ ├── en.json
│ │ │ ├── ja.json
│ │ │ ├── zh.json
│ │ │ ├── zh_Hant.json
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ └── date.ts
│ ├── styles
│ │ ├── fonts
│ │ │ ├── OpenSans-Bold.ttf
│ │ │ ├── OpenSans-Light.ttf
│ │ │ ├── OpenSans-Italic.ttf
│ │ │ ├── OpenSans-Regular.ttf
│ │ │ └── OpenSans-BoldItalic.ttf
│ │ ├── base.css
│ │ └── keyboard.css
│ ├── types
│ │ ├── guacamole.type.ts
│ │ └── postmessage.type.ts
│ ├── main.ts
│ ├── stores
│ │ └── counter.ts
│ ├── utils
│ │ ├── clipboard.ts
│ │ ├── common.ts
│ │ ├── status.ts
│ │ ├── lunaBus.ts
│ │ ├── config.ts
│ │ └── guacamole_helper.ts
│ ├── router
│ │ └── index.ts
│ ├── api
│ │ └── index.ts
│ ├── components
│ │ ├── CardContainer
│ │ │ └── index.vue
│ │ ├── SessionShare
│ │ │ ├── index.vue
│ │ │ └── widget
│ │ │ │ └── UserItem.vue
│ │ ├── KeyboardOption.vue
│ │ ├── CombinationKey.vue
│ │ ├── ClipBoardText.vue
│ │ ├── OtherOption.vue
│ │ └── Osk.vue
│ ├── views
│ │ ├── MonitorView.vue
│ │ └── ShareView.vue
│ ├── App.vue
│ └── hooks
│ │ └── useColor.ts
├── .gitattributes
├── env.d.ts
├── public
│ └── favicon.ico
├── .prettierrc.json
├── tsconfig.json
├── .editorconfig
├── tsconfig.app.json
├── index.html
├── .gitignore
├── tsconfig.node.json
├── README.md
├── eslint.config.ts
├── vite.config.ts
├── package.json
└── components.d.ts
├── pkg
├── config
│ ├── config_test.yml
│ ├── const.go
│ ├── config_test.go
│ └── config.go
├── session
│ ├── interface.go
│ ├── message.go
│ ├── permisson.go
│ ├── recorder.go
│ ├── display.go
│ ├── session.go
│ └── parser.go
├── tunnel
│ ├── room_cache.go
│ ├── api_response.go
│ ├── ws_error.go
│ ├── stream_input.go
│ ├── cache.go
│ ├── cache_local.go
│ ├── stream_output.go
│ └── monitor.go
├── guacd
│ ├── configuration.go
│ ├── parameters_vnc.go
│ ├── parameters.go
│ ├── instrcution_name.go
│ ├── instruction_test.go
│ ├── information.go
│ ├── qwertz.go
│ ├── keyboard_map.go
│ ├── parameters_rdp.go
│ ├── instruction.go
│ ├── status.go
│ └── tunnel.go
├── middleware
│ ├── session.go
│ └── cookie.go
├── logger
│ ├── gloable.go
│ └── logger.go
└── gateway
│ └── domain.go
├── .golangci.yml
├── docker-compose.yaml.example
├── Dockerfile-ee
├── README_zh-CN.md
├── supervisord.conf
├── README.md
├── config_example.yml
├── .gitignore
├── entrypoint.sh
├── .github
├── release-config.yml
└── workflows
│ ├── jms-generic-action-handler.yml
│ ├── golangci-lint.yml
│ ├── build-ghcr-image.yml
│ ├── build-base-image.yml
│ ├── release-drafter.yml
│ └── jms-build-test.yml.disabled
├── Dockerfile-base
├── .goreleaser.yaml
├── Dockerfile.guacd
├── Dockerfile
├── Makefile
└── go.mod
/.dockerignore:
--------------------------------------------------------------------------------
1 | */data
2 | */node_modules
--------------------------------------------------------------------------------
/ui/src/locales/modules/en.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/ui/src/locales/modules/ja.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/ui/src/locales/modules/zh.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/ui/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/ui/src/locales/modules/zh_Hant.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/ui/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/ui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jumpserver/lion/HEAD/ui/public/favicon.ico
--------------------------------------------------------------------------------
/ui/src/styles/fonts/OpenSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jumpserver/lion/HEAD/ui/src/styles/fonts/OpenSans-Bold.ttf
--------------------------------------------------------------------------------
/ui/src/styles/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jumpserver/lion/HEAD/ui/src/styles/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/ui/src/styles/fonts/OpenSans-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jumpserver/lion/HEAD/ui/src/styles/fonts/OpenSans-Italic.ttf
--------------------------------------------------------------------------------
/ui/src/styles/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jumpserver/lion/HEAD/ui/src/styles/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/ui/src/styles/fonts/OpenSans-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jumpserver/lion/HEAD/ui/src/styles/fonts/OpenSans-BoldItalic.ttf
--------------------------------------------------------------------------------
/pkg/config/config_test.yml:
--------------------------------------------------------------------------------
1 | CORE_HOST: http://10.0.0.5:8080
2 |
3 | BOOTSTRAP_TOKEN: ICAgICAgICBUWCBl
4 |
5 | GUA_HOST: 127.0.0.1
6 |
7 | GUA_PORT: 4822
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | timeout: 5m
3 | modules-download-mode: readonly
4 |
5 |
6 | linters:
7 | enable:
8 | - govet
9 | - staticcheck
10 |
11 |
--------------------------------------------------------------------------------
/ui/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "semi": true,
4 | "singleQuote": true,
5 | "printWidth": 100,
6 | "tabWidth": 2
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/session/interface.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | type ParseEngine interface {
4 | ParseStream(userInChan chan *Message)
5 |
6 | Close()
7 |
8 | CommandRecordChan() chan *ExecutedCommand
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/types/guacamole.type.ts:
--------------------------------------------------------------------------------
1 | export interface GuacamoleDisplay {
2 | getWidth: () => number;
3 | getHeight: () => number;
4 | scale: (scale: number) => void;
5 | getElement: () => HTMLElement;
6 | }
7 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.node.json"
6 | },
7 | {
8 | "path": "./tsconfig.app.json"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/ui/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
2 | charset = utf-8
3 | indent_size = 2
4 | indent_style = space
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 |
8 | end_of_line = lf
9 | max_line_length = 100
10 |
--------------------------------------------------------------------------------
/pkg/config/const.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | const (
4 | GinCtxUserKey = "JMS-CtxUserKey"
5 | )
6 |
7 | const (
8 | GinSessionName = "session-Lion"
9 | GinSessionKey = "SESSION"
10 | )
11 |
12 | const (
13 | ShareTypeRedis = "redis"
14 | ShareTypeLocal = "local"
15 | )
16 |
--------------------------------------------------------------------------------
/ui/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4 | "exclude": ["src/**/__tests__/*"],
5 | "compilerOptions": {
6 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
7 |
8 | "paths": {
9 | "@/*": ["./src/*"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/pkg/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestSetupByYml(t *testing.T) {
8 | var conf = getDefaultConfig()
9 | loadConfigFromFile("config_test.yml", &conf)
10 | t.Log(conf)
11 | }
12 |
13 | func TestSetupByEnv(t *testing.T) {
14 | var conf = getDefaultConfig()
15 | loadConfigFromEnv(&conf)
16 | t.Log(conf)
17 | }
18 |
--------------------------------------------------------------------------------
/ui/src/locales/modules/index.ts:
--------------------------------------------------------------------------------
1 | import en from './en.json';
2 | import zh from './zh.json';
3 | import ja from './ja.json';
4 | import zh_Hant from './zh_Hant.json';
5 |
6 | export const message = {
7 | zh: {
8 | ...zh,
9 | },
10 | zh_hant: {
11 | ...zh_Hant,
12 | },
13 | en: {
14 | ...en,
15 | },
16 | ja: {
17 | ...ja,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/ui/src/main.ts:
--------------------------------------------------------------------------------
1 | import './styles/base.css';
2 |
3 | import { createApp } from 'vue';
4 | import { createPinia } from 'pinia';
5 | import i18n from '@/locales';
6 |
7 | import App from './App.vue';
8 | import router from './router';
9 |
10 | const app = createApp(App);
11 |
12 | app.use(createPinia());
13 | app.use(router);
14 | app.use(i18n);
15 | app.mount('#app');
16 |
--------------------------------------------------------------------------------
/ui/src/stores/counter.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed } from 'vue';
2 | import { defineStore } from 'pinia';
3 |
4 | export const useCounterStore = defineStore('counter', () => {
5 | const count = ref(0);
6 | const doubleCount = computed(() => count.value * 2);
7 | function increment() {
8 | count.value++;
9 | }
10 |
11 | return { count, doubleCount, increment };
12 | });
13 |
--------------------------------------------------------------------------------
/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Lion Terminal
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/ui/src/utils/clipboard.ts:
--------------------------------------------------------------------------------
1 |
2 | export async function readClipboardText(): Promise {
3 | try {
4 | if (navigator.clipboard && navigator.clipboard.readText) {
5 | return await navigator.clipboard.readText()
6 | }
7 | console.log("navigator.clipboard api not found")
8 | return ''
9 | } catch (err) {
10 | console.error('Failed to read clipboard:', err);
11 | return '';
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/docker-compose.yaml.example:
--------------------------------------------------------------------------------
1 | version: "3.0"
2 |
3 | networks:
4 | guacd:
5 | driver: bridge
6 |
7 | services:
8 | guacd:
9 | image: jumpserver/guacd:1.4.0
10 | container_name: guacd
11 | ports:
12 | - "4822:4822"
13 | environment:
14 | GUACD_LOG_LEVEL: debug
15 | networks:
16 | - guacd
17 | restart: always
18 | volumes:
19 | - ./data/:/opt/lion/data/:rw # /opt/lion/-> 本地项目路径, 修改 ./data 目录权限为777
20 |
--------------------------------------------------------------------------------
/Dockerfile-ee:
--------------------------------------------------------------------------------
1 | ARG VERSION=dev
2 |
3 | FROM jumpserver/lion:${VERSION}-ce
4 | ARG TARGETARCH
5 |
6 | ARG DEPENDENCIES=" \
7 | curl \
8 | iputils-ping \
9 | telnet \
10 | vim \
11 | wget"
12 |
13 | RUN set -ex \
14 | && apt-get update \
15 | && apt-get install -y --no-install-recommends ${DEPENDENCIES}
16 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-ssr
14 | coverage
15 | *.local
16 |
17 | /cypress/videos/
18 | /cypress/screenshots/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
30 | *.tsbuildinfo
31 |
--------------------------------------------------------------------------------
/pkg/session/message.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | type Message struct {
4 | Opcode string `json:"opcode"`
5 | Body []string `json:"data"`
6 | Meta MetaMessage `json:"meta"` // receive的信息必须携带Meta
7 | }
8 |
9 | type MetaMessage struct {
10 | UserId string `json:"user_id"`
11 | User string `json:"user"`
12 | Created string `json:"created"`
13 |
14 | TerminalId string `json:"terminal_id"`
15 | Primary bool `json:"primary"`
16 | Writable bool `json:"writable"`
17 | }
18 |
--------------------------------------------------------------------------------
/ui/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node22/tsconfig.json",
3 | "include": [
4 | "vite.config.*",
5 | "vitest.config.*",
6 | "cypress.config.*",
7 | "nightwatch.conf.*",
8 | "playwright.config.*",
9 | "eslint.config.*"
10 | ],
11 | "compilerOptions": {
12 | "noEmit": true,
13 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
14 |
15 | "module": "ESNext",
16 | "moduleResolution": "Bundler",
17 | "types": ["node"]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/README_zh-CN.md:
--------------------------------------------------------------------------------
1 | # Lion
2 |
3 | **简体中文** · [English](./README.md)
4 |
5 | ## 介绍
6 |
7 | 该项目使用 Golang 和 Vue 重构了 JumpServer 的 Guacamole 组件,负责 RDP 和 VNC 的连接。 主要基于 [Apache Guacamole](http://guacamole.apache.org/)
8 | 开发。
9 |
10 | ## 配置
11 |
12 | 启动的配置文件参考[config_example](config_example.yml)
13 |
14 | ## 构建镜像
15 |
16 | ```shell
17 | docker build -t jumpserver/lion .
18 | ```
19 |
20 | ## docker启动
21 |
22 | ```shell
23 | docker run -d --name jms_lion -p 8081:8081 \
24 | -v $(pwd)/data:/opt/lion/data \
25 | -v $(pwd)/config.yml:/opt/lion/config.yml \
26 | jumpserver/lion
27 | ```
--------------------------------------------------------------------------------
/supervisord.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | nodaemon=true
3 | user=root
4 |
5 | [program:guacd]
6 | command=/opt/guacamole/sbin/guacd -b 0.0.0.0 -L %(ENV_GUACD_LOG_LEVEL)s -f
7 | redirect_stderr=true
8 | stdout_logfile=/opt/lion/data/logs/guacd.log
9 | stdout_logfile_maxbytes=50MB
10 | stdout_logfile_backups=10
11 | stdout_capture_maxbytes=1MB
12 | autorestart=true
13 |
14 | [program:lion]
15 | directory=/opt/lion/
16 | command=/opt/lion/lion
17 | stdout_logfile=/dev/stdout
18 | stdout_logfile_maxbytes=0
19 | stderr_logfile=/dev/stderr
20 | stderr_logfile_maxbytes=0
21 | autorestart=true
--------------------------------------------------------------------------------
/pkg/tunnel/room_cache.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | type MetaShareUserMessage struct {
4 | ShareId string `json:"share_id"`
5 |
6 | SessionId string `json:"session_id"`
7 | UserId string `json:"user_id"`
8 | User string `json:"user"`
9 | Created string `json:"created"`
10 | RemoteAddr string `json:"remote_addr"`
11 | Primary bool `json:"primary"`
12 | Writable bool `json:"writable"`
13 | }
14 |
15 | type SessionRoomMessage struct {
16 | Id string `json:"id"`
17 | SessionId string `json:"session_id"`
18 | Event *Event `json:"event"`
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/tunnel/api_response.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var ErrNoAuthUser = errors.New("no auth user")
8 |
9 | type APIResponse struct {
10 | Success bool `json:"success"`
11 | Message string `json:"message"`
12 | Data interface{} `json:"data"`
13 | }
14 |
15 | func SuccessResponse(data interface{}) APIResponse {
16 | return APIResponse{
17 | Success: true,
18 | Data: data,
19 | }
20 | }
21 |
22 | func ErrorResponse(err error) APIResponse {
23 | return APIResponse{
24 | Success: false,
25 | Message: err.Error(),
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lion
2 |
3 | **English** · [简体中文](./README_zh-CN.md)
4 |
5 | ## Introduction
6 |
7 | This project uses Golang and Vue, handling RDP and VNC connections. It is mainly based on [Apache Guacamole](http://guacamole.apache.org/)
8 |
9 | ## Configuration
10 |
11 | Refer to the configuration file [config_example](config_example.yml)
12 |
13 | ## Build the image
14 |
15 | ```shell
16 | docker build -t jumpserver/lion .
17 | ```
18 |
19 | ## Docker start
20 |
21 | ```shell
22 | docker run -d --name jms_lion -p 8081:8081 \
23 | -v $(pwd)/data:/opt/lion/data \
24 | -v $(pwd)/config.yml:/opt/lion/config.yml \
25 | jumpserver/lion
26 | ```
--------------------------------------------------------------------------------
/ui/src/locales/index.ts:
--------------------------------------------------------------------------------
1 | import { useCookies } from 'vue3-cookies';
2 | const { cookies } = useCookies();
3 | import { message } from './modules';
4 | import { createI18n } from 'vue-i18n';
5 | const storeLang = cookies.get('lang');
6 | const cookieLang = cookies.get('django_language');
7 |
8 | const browserLang = navigator.language || (navigator.languages && navigator.languages[0]) || 'en';
9 |
10 | export const LanguageCode = cookieLang || storeLang || browserLang || 'en';
11 | import date from './date';
12 |
13 | const i18n = createI18n({
14 | locale: LanguageCode,
15 | fallbackLocale: 'en',
16 | legacy: false,
17 | allowComposition: true,
18 | silentFallbackWarn: true,
19 | silentTranslationWarn: true,
20 | messages: message,
21 | dateTimeFormats: date,
22 | });
23 |
24 | export default i18n;
25 |
--------------------------------------------------------------------------------
/config_example.yml:
--------------------------------------------------------------------------------
1 | # 项目名称, 会用来向Jumpserver注册, 识别而已, 不能重复
2 | # NAME: {{ Hostname }}
3 |
4 | # Jumpserver项目的url, api请求注册会使用
5 | CORE_HOST: http://127.0.0.1:8080
6 |
7 | # Bootstrap Token, 预共享秘钥, 用来注册使用的service account和terminal
8 | # 请和jumpserver 配置文件中保持一致,注册完成后可以删除
9 | BOOTSTRAP_TOKEN:
10 |
11 | # 启动时绑定的ip, 默认 0.0.0.0
12 | # BIND_HOST: 0.0.0.0
13 |
14 | # 监听的HTTP/WS端口号,默认8081
15 | # HTTPD_PORT: 8081
16 |
17 | # 设置日志级别 [DEBUG, INFO, WARN, ERROR, FATAL, CRITICAL]
18 | # LOG_LEVEL: INFO
19 |
20 | # Guacamole Server ip, 默认127.0.0.1
21 | # GUA_HOST: 127.0.0.1
22 |
23 | # Guacamole Server 端口号,默认4822
24 | # GUA_PORT: 4822
25 |
26 | # 会话共享使用的类型 [local, redis], 默认local
27 | # SHARE_ROOM_TYPE: local
28 |
29 | # Redis配置
30 | # REDIS_HOST: 127.0.0.1
31 | # REDIS_PORT: 6379
32 | # REDIS_PASSWORD:
33 | # REDIS_DB_ROOM:
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 | .idea
17 | data/
18 | docker-compose.yaml
19 | docker-compose.yml
20 | !config_example.yml
21 | .DS_Store
22 | ui/node_modules/
23 | ui/dist/
24 | ui/guacamole/
25 | ui/lion/
26 | config.yml
27 | vendor/
28 |
29 | # local env files
30 | .env.local
31 | .env.*.local
32 |
33 | # Log files
34 | ui/npm-debug.log*
35 | ui/yarn-debug.log*
36 | ui/yarn-error.log*
37 | ui/pnpm-debug.log*
38 |
39 | # Editor directories and files
40 | .vscode
41 | *.suoui
42 | *.ntvs*
43 | *.njsproj
44 | *.sln
45 | *.sw?
46 | build
47 | build/
48 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 |
4 | if [ -n "$CORE_HOST" ]; then
5 | until check ${CORE_HOST}/api/health/; do
6 | echo "wait for jms_core ${CORE_HOST} ready"
7 | sleep 2
8 | done
9 | fi
10 |
11 | if [ ! -d "/opt/lion/data/logs" ]; then
12 | mkdir -p /opt/lion/data/logs
13 | fi
14 |
15 | : ${LOG_LEVEL:='ERROR'}
16 |
17 | case $LOG_LEVEL in
18 | "DEBUG")
19 | level="debug"
20 | ;;
21 | "INFO")
22 | level='info'
23 | ;;
24 | "WARN")
25 | level='warning'
26 | ;;
27 | "ERROR" | "FATAL" | "CRITICAL")
28 | level='error'
29 | ;;
30 | *)
31 | level='error'
32 | ;;
33 | esac
34 | export GUACD_LOG_LEVEL=$level
35 |
36 | echo
37 | date
38 | echo "LION Version $VERSION, more see https://www.jumpserver.org"
39 | echo "Quit the server with CONTROL-C."
40 | echo
41 |
42 | exec "$@"
--------------------------------------------------------------------------------
/pkg/guacd/configuration.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | func NewConfiguration() (conf Configuration) {
4 | conf.Parameters = make(map[string]string)
5 | return conf
6 | }
7 |
8 | type Configuration struct {
9 | ConnectionID string
10 | Protocol string
11 | Parameters map[string]string
12 | }
13 |
14 | func (conf *Configuration) SetParameter(name, value string) {
15 | conf.Parameters[name] = value
16 | }
17 |
18 | func (conf *Configuration) UnSetParameter(name string) {
19 | delete(conf.Parameters, name)
20 | }
21 |
22 | func (conf *Configuration) GetParameter(name string) string {
23 | return conf.Parameters[name]
24 | }
25 |
26 | func (conf *Configuration) Clone() Configuration {
27 | newConf := NewConfiguration()
28 | newConf.ConnectionID = conf.ConnectionID
29 | newConf.Protocol = conf.Protocol
30 | for k, v := range conf.Parameters {
31 | newConf.Parameters[k] = v
32 | }
33 | return newConf
34 | }
35 |
--------------------------------------------------------------------------------
/ui/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from 'vue-router';
2 | import ConnectView from '../views/ConnectView.vue';
3 |
4 | console.log('router init', import.meta.env);
5 |
6 | const router = createRouter({
7 | history: createWebHistory(import.meta.env.BASE_URL),
8 | routes: [
9 | {
10 | path: '/connect/',
11 | name: 'connect',
12 | component: ConnectView,
13 | },
14 | {
15 | path: '/monitor/',
16 | name: 'monitor',
17 | // route level code-splitting
18 | // this generates a separate chunk (About.[hash].js) for this route
19 | // which is lazy-loaded when the route is visited.
20 | component: () => import('../views/MonitorView.vue'),
21 | },
22 | {
23 | path: '/share/:id/',
24 | name: 'share',
25 | component: () => import('../views/ShareView.vue'),
26 | },
27 | ],
28 | });
29 |
30 | export default router;
31 |
--------------------------------------------------------------------------------
/ui/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import { createAlova } from 'alova';
2 | import fetchAdapter from 'alova/fetch';
3 | import { BASE_URL } from '@/utils/common';
4 |
5 | export const alovaInstance = createAlova({
6 | baseURL: BASE_URL,
7 | requestAdapter: fetchAdapter(),
8 | });
9 |
10 | const getSuggestionUsers = (query: string) => {
11 | const params = {
12 | search: query,
13 | };
14 | return alovaInstance.Get('/api/v1/users/users/suggestions/', { params: params });
15 | };
16 |
17 | const createShareURL = (data: any) => {
18 | return alovaInstance.Post(`/lion/api/share/`, data);
19 | };
20 |
21 | const getShareSession = (id: string, data: any) => {
22 | return alovaInstance.Post(`/lion/api/share/${id}/`, data);
23 | };
24 |
25 | const removeShareUser = (data: any) => {
26 | return alovaInstance.Post(`/lion/api/share/remove/`, data);
27 | };
28 |
29 | export { getSuggestionUsers, createShareURL, getShareSession, removeShareUser };
30 |
--------------------------------------------------------------------------------
/.github/release-config.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | categories:
4 | - title: '🌱 新功能 Features'
5 | labels:
6 | - 'feature'
7 | - 'enhancement'
8 | - 'feat'
9 | - '新功能'
10 | - title: '🚀 性能优化 Optimization'
11 | labels:
12 | - 'perf'
13 | - 'opt'
14 | - 'refactor'
15 | - 'Optimization'
16 | - '优化'
17 | - title: '🐛 Bug修复 Bug Fixes'
18 | labels:
19 | - 'fix'
20 | - 'bugfix'
21 | - 'bug'
22 | - title: '🧰 其它 Maintenance'
23 | labels:
24 | - 'chore'
25 | - 'docs'
26 | exclude-labels:
27 | - 'no'
28 | - '无需处理'
29 | - 'wontfix'
30 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
31 | version-resolver:
32 | major:
33 | labels:
34 | - 'major'
35 | minor:
36 | labels:
37 | - 'minor'
38 | patch:
39 | labels:
40 | - 'patch'
41 | default: patch
42 | template: |
43 | ## 版本变化 What’s Changed
44 |
45 | $CHANGES
46 |
--------------------------------------------------------------------------------
/pkg/guacd/parameters_vnc.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | // Network parameters
4 | const (
5 | VNCHostname = "hostname"
6 | VNCPort = "port"
7 | VNCAutoretry = "autoretry"
8 | )
9 |
10 | // Authentication
11 | const (
12 | VNCUsername = "username"
13 | VNCPassword = "password"
14 | )
15 |
16 | // Display Settings
17 | const (
18 | VNCColorDepth = "color-depth"
19 | VNCSwapRedBlue = "swap-red-blue"
20 | VNCCursor = "cursor"
21 | VNCEncoding = "encodings"
22 | VNCReadOnly = "read-only"
23 | )
24 |
25 | // VNC Repeater
26 |
27 | const (
28 | VNCDestHost = "dest-host"
29 | VNCDestPort = "dest-port"
30 | )
31 |
32 | // Reverse VNC connections
33 |
34 | const (
35 | VNCReverseConnect = "reverse-connect"
36 | VNCListenTimeout = "listen-timeout"
37 | )
38 |
39 | // Audio support (via PulseAudio)
40 |
41 | const (
42 | VNCEnableAudio = "enableAudio"
43 | VNCAudioServername = "audio-servername"
44 | )
45 |
46 | // Clipboard encoding
47 |
48 | const (
49 | VNCClipboardEncoding = "clipboard-encoding" // ISO8859-1| UTF-8 | UTF-16| CP1252
50 | )
51 |
--------------------------------------------------------------------------------
/pkg/middleware/session.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "net/http"
5 |
6 | ginSessions "github.com/gin-contrib/sessions"
7 | "github.com/gin-gonic/gin"
8 |
9 | "lion/pkg/config"
10 | "lion/pkg/logger"
11 |
12 | "github.com/jumpserver-dev/sdk-go/service"
13 | )
14 |
15 | func GinSessionAuth(store ginSessions.Store) gin.HandlerFunc {
16 | return ginSessions.Sessions(config.GinSessionName, store)
17 | }
18 |
19 | func SessionAuth(jmsService *service.JMService) gin.HandlerFunc {
20 | return func(ctx *gin.Context) {
21 | ginSession := ginSessions.Default(ctx)
22 | if result := ginSession.Get(config.GinSessionKey); result != nil {
23 | logger.Errorf("Token auth failed %+v", ginSession)
24 | if uid, ok := result.(string); ok {
25 | if user, err := jmsService.GetUserById(uid); err == nil {
26 | ctx.Set(config.GinCtxUserKey, user)
27 | logger.Debugf("Token auth user: %s", user)
28 | return
29 | }
30 | }
31 | }
32 | logger.Errorf("Token auth failed %+v", ginSession)
33 | ctx.Status(http.StatusForbidden)
34 | ctx.Abort()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ui/README.md:
--------------------------------------------------------------------------------
1 | # .
2 |
3 | This template should help get you started developing with Vue 3 in Vite.
4 |
5 | ## Recommended IDE Setup
6 |
7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
8 |
9 | ## Type Support for `.vue` Imports in TS
10 |
11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
12 |
13 | ## Customize configuration
14 |
15 | See [Vite Configuration Reference](https://vite.dev/config/).
16 |
17 | ## Project Setup
18 |
19 | ```sh
20 | npm install
21 | ```
22 |
23 | ### Compile and Hot-Reload for Development
24 |
25 | ```sh
26 | npm run dev
27 | ```
28 |
29 | ### Type-Check, Compile and Minify for Production
30 |
31 | ```sh
32 | npm run build
33 | ```
34 |
35 | ### Lint with [ESLint](https://eslint.org/)
36 |
37 | ```sh
38 | npm run lint
39 | ```
40 |
--------------------------------------------------------------------------------
/ui/eslint.config.ts:
--------------------------------------------------------------------------------
1 | import { globalIgnores } from 'eslint/config'
2 | import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
3 | import pluginVue from 'eslint-plugin-vue'
4 | import pluginOxlint from 'eslint-plugin-oxlint'
5 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
6 |
7 | // To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
8 | // import { configureVueProject } from '@vue/eslint-config-typescript'
9 | // configureVueProject({ scriptLangs: ['ts', 'tsx'] })
10 | // More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
11 |
12 | export default defineConfigWithVueTs(
13 | {
14 | name: 'app/files-to-lint',
15 | files: ['**/*.{ts,mts,tsx,vue}'],
16 | },
17 |
18 | globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
19 |
20 | pluginVue.configs['flat/essential'],
21 | vueTsConfigs.recommended,
22 | ...pluginOxlint.configs['flat/recommended'],
23 | skipFormatting,
24 | {
25 | rules: {
26 | '@typescript-eslint/no-explicit-any': 'off',
27 | },
28 | }
29 | )
30 |
--------------------------------------------------------------------------------
/.github/workflows/jms-generic-action-handler.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | pull_request:
4 | types: [opened, synchronize, closed]
5 | release:
6 | types: [created]
7 |
8 | name: JumpServer repos generic handler
9 |
10 | jobs:
11 | handle_pull_request:
12 | if: github.event_name == 'pull_request'
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: jumpserver/action-generic-handler@master
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
18 | I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
19 |
20 | handle_push:
21 | if: github.event_name == 'push'
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: jumpserver/action-generic-handler@master
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
27 | I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
28 |
29 | handle_release:
30 | if: github.event_name == 'release'
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: jumpserver/action-generic-handler@master
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
36 | I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
37 |
--------------------------------------------------------------------------------
/ui/src/styles/base.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @import './keyboard.css';
3 |
4 | @font-face {
5 | font-family: 'Open Sans';
6 | src: url('./fonts/OpenSans-Regular.ttf');
7 | font-weight: normal;
8 | font-style: normal;
9 | }
10 |
11 | @font-face {
12 | font-family: 'Open Sans';
13 | src: url('./fonts/OpenSans-Bold.ttf');
14 | font-weight: bold;
15 | font-style: normal;
16 | }
17 |
18 | @font-face {
19 | font-family: 'Open Sans';
20 | src: url('./fonts/OpenSans-Light.ttf');
21 | font-weight: 300;
22 | font-style: normal;
23 | }
24 |
25 | @font-face {
26 | font-family: 'Open Sans';
27 | src: url('./fonts/OpenSans-Italic.ttf');
28 | font-weight: 300;
29 | font-style: italic;
30 | }
31 |
32 | body {
33 | height: 100%;
34 | /* -moz-osx-font-smoothing: grayscale; */
35 | -webkit-font-smoothing: auto;
36 | background-color: #000000;
37 | font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
38 | font-size: 13px;
39 | line-height: 1.428;
40 | }
41 |
42 | ::-webkit-scrollbar-track {
43 | box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.3);
44 | background-color: #0a0a0a;
45 | }
46 |
47 | ::-webkit-scrollbar-thumb {
48 | background-color: #494141;
49 | border-radius: 6px;
50 | }
51 |
--------------------------------------------------------------------------------
/ui/src/components/CardContainer/index.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
47 |
--------------------------------------------------------------------------------
/ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url'
2 |
3 | import { defineConfig } from 'vite'
4 | import vue from '@vitejs/plugin-vue'
5 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
6 | import Components from 'unplugin-vue-components/vite'
7 | import vueJsx from '@vitejs/plugin-vue-jsx'
8 | import vueDevTools from 'vite-plugin-vue-devtools'
9 | import tailwindcss from '@tailwindcss/vite'
10 | // https://vite.dev/config/
11 | export default defineConfig({
12 | base: '/lion/',
13 | plugins: [
14 | vue(),
15 | tailwindcss(),
16 | vueJsx(),
17 | Components({ dts: true, resolvers: [NaiveUiResolver()] }),
18 | ],
19 | resolve: {
20 | extensions: ['.js', '.ts', '.tsx', '.vue'],
21 | alias: {
22 | '@': fileURLToPath(new URL('./src', import.meta.url))
23 | },
24 | },
25 | server: {
26 | port: 9529,
27 | proxy: {
28 | '^/lion/ws': {
29 | target: 'http://localhost:8081',
30 | ws: true,
31 | changeOrigin: true,
32 | },
33 | '^/lion/api': {
34 | target: 'http://localhost:8081',
35 | ws: true,
36 | changeOrigin: true,
37 | },
38 | '^/lion/token': {
39 | target: 'http://localhost:8081',
40 | changeOrigin: true,
41 | ws: true,
42 | },
43 | }
44 | },
45 | })
46 |
--------------------------------------------------------------------------------
/Dockerfile-base:
--------------------------------------------------------------------------------
1 | FROM golang:1.24-trixie AS stage-go-build
2 |
3 | FROM node:20-trixie
4 | COPY --from=stage-go-build /usr/local/go/ /usr/local/go/
5 | COPY --from=stage-go-build /go/ /go/
6 | ENV GOPATH=/go
7 | ENV PATH=/go/bin:/usr/local/go/bin:$PATH
8 | ARG TARGETARCH
9 | ARG NPM_REGISTRY="https://registry.npmmirror.com"
10 | ENV NPM_REGISTY=$NPM_REGISTRY
11 |
12 | RUN set -ex \
13 | && npm config set registry ${NPM_REGISTRY} \
14 | && yarn config set registry ${NPM_REGISTRY}
15 |
16 | WORKDIR /opt
17 |
18 | ARG CHECK_VERSION=v1.0.5
19 | RUN set -ex \
20 | && wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
21 | && tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz -C /usr/local/bin/ check \
22 | && chown root:root /usr/local/bin/check \
23 | && chmod 755 /usr/local/bin/check \
24 | && rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
25 |
26 | WORKDIR /opt/lion/ui
27 |
28 | RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked,id=lion \
29 | --mount=type=bind,source=ui/package.json,target=package.json \
30 | --mount=type=bind,source=ui/yarn.lock,target=yarn.lock \
31 | yarn install
32 |
33 | ENV CGO_ENABLED=0
34 | ENV GO111MODULE=on
35 |
36 | WORKDIR /opt/lion
37 |
38 | COPY go.mod go.sum ./
39 |
40 | RUN go mod download -x
41 |
--------------------------------------------------------------------------------
/pkg/middleware/cookie.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 |
8 | "github.com/gin-gonic/gin"
9 |
10 | "lion/pkg/config"
11 | "lion/pkg/logger"
12 |
13 | "github.com/jumpserver-dev/sdk-go/model"
14 | "github.com/jumpserver-dev/sdk-go/service"
15 | )
16 |
17 | func JmsCookieAuth(jmsService *service.JMService) gin.HandlerFunc {
18 | return func(ctx *gin.Context) {
19 | var (
20 | err error
21 | user *model.User
22 | )
23 | reqCookies := ctx.Request.Cookies()
24 | var cookies = make(map[string]string)
25 | for _, cookie := range reqCookies {
26 | cookies[cookie.Name] = cookie.Value
27 | }
28 | user, err = jmsService.CheckUserCookie(cookies)
29 | if err != nil {
30 | logger.Errorf("Check user cookie failed: %+v %s", cookies, err.Error())
31 | loginUrl := fmt.Sprintf("/core/auth/login/?next=%s", url.QueryEscape(ctx.Request.URL.RequestURI()))
32 | ctx.Redirect(http.StatusFound, loginUrl)
33 | ctx.Abort()
34 | return
35 | }
36 | ctx.Set(config.GinCtxUserKey, user)
37 | }
38 | }
39 |
40 | func HTTPMiddleDebugAuth() gin.HandlerFunc {
41 | return func(ctx *gin.Context) {
42 | switch ctx.ClientIP() {
43 | case "127.0.0.1", "localhost":
44 | return
45 | default:
46 | _ = ctx.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid host %s", ctx.ClientIP()))
47 | return
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/session/permisson.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "lion/pkg/config"
5 |
6 | "github.com/jumpserver-dev/sdk-go/model"
7 | )
8 |
9 | type ActionPermission struct {
10 | EnableConnect bool `json:"enable_connect"`
11 |
12 | EnableCopy bool `json:"enable_copy"`
13 | EnablePaste bool `json:"enable_paste"`
14 |
15 | EnableUpload bool `json:"enable_upload"`
16 | EnableDownload bool `json:"enable_download"`
17 | EnableShare bool `json:"enable_share"`
18 | }
19 |
20 | func NewActionPermission(perm *model.Permission, connectType string) *ActionPermission {
21 | action := ActionPermission{
22 | EnableConnect: perm.EnableConnect(),
23 | EnableCopy: perm.EnableCopy(),
24 | EnablePaste: perm.EnablePaste(),
25 | EnableUpload: perm.EnableUpload(),
26 | EnableDownload: perm.EnableDownload(),
27 | EnableShare: perm.EnableShare(),
28 | }
29 | globConfig := config.GlobalConfig
30 | switch connectType {
31 | case TypeRemoteApp:
32 | if globConfig.EnableRemoteAppUpDownLoad {
33 | action.EnableDownload = true
34 | action.EnableUpload = true
35 | }
36 | if globConfig.EnableRemoteAPPCopyPaste {
37 | action.EnablePaste = true
38 | action.EnableCopy = true
39 | }
40 | case TypeRDP, TypeVNC:
41 | }
42 | if globConfig.DisableAllUpDownload {
43 | action.EnableDownload = false
44 | action.EnableUpload = false
45 | }
46 | if globConfig.DisableAllCopyPaste {
47 | action.EnablePaste = false
48 | action.EnableCopy = false
49 | }
50 | return &action
51 | }
52 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
2 | version: 2
3 |
4 | project_name: lion
5 |
6 | before:
7 | hooks:
8 | - go mod tidy
9 | - go generate ./...
10 |
11 | builds:
12 | - id: lion
13 | main: main.go
14 | binary: lion
15 | goos:
16 | - linux
17 | - darwin
18 | - freebsd
19 | - netbsd
20 | goarch:
21 | - amd64
22 | - arm64
23 | - mips64le
24 | - ppc64le
25 | - s390x
26 | - riscv64
27 | - loong64
28 | env:
29 | - CGO_ENABLED=0
30 | ldflags:
31 | - -w -s
32 | - -X 'main.Buildstamp={{ .Date }}'
33 | - -X 'main.Githash={{ .ShortCommit }}'
34 | - -X 'main.Goversion={{ .Env.GOVERSION }}'
35 | - -X 'main.Version={{ .Tag }}'
36 |
37 | archives:
38 | - format: tar.gz
39 | wrap_in_directory: true
40 | files:
41 | - LICENSE
42 | - README.md
43 | - config_example.yml
44 | - entrypoint.sh
45 | - supervisord.conf
46 | - ui/dist/**
47 |
48 | format_overrides:
49 | - goos: windows
50 | format: zip
51 | name_template: "{{ .ProjectName }}-{{ .Tag }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}"
52 |
53 | checksum:
54 | name_template: "checksums.txt"
55 |
56 | release:
57 | draft: true
58 | mode: append
59 | extra_files:
60 | - glob: dist/*.tar.gz
61 | - glob: dist/*.txt
62 | name_template: "Release {{.Tag}}"
63 |
64 | changelog:
65 | sort: asc
66 | filters:
67 | exclude:
68 | - "^docs:"
69 | - "^test:"
--------------------------------------------------------------------------------
/ui/src/views/MonitorView.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - dev
7 |
8 | permissions:
9 | contents: read
10 | # Optional: allow read access to pull request. Use with `only-new-issues` option.
11 | # pull-requests: read
12 |
13 | jobs:
14 | golangci:
15 | name: lint
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Create ui/dist directory
21 | run: |
22 | mkdir -p ui/dist
23 | touch ui/dist/.gitkeep
24 |
25 | - uses: actions/setup-go@v5
26 | with:
27 | go-version: stable
28 |
29 | - uses: golangci/golangci-lint-action@v6
30 | with:
31 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
32 | version: latest
33 |
34 | # Optional: working directory, useful for monorepos
35 | # working-directory: somedir
36 |
37 | # Optional: golangci-lint command line arguments.
38 | # args: --issues-exit-code=0
39 |
40 | # Optional: show only new issues if it's a pull request. The default value is `false`.
41 | # only-new-issues: true
42 |
43 | # Optional: if set to true then the all caching functionality will be complete disabled,
44 | # takes precedence over all other caching options.
45 | # skip-cache: true
46 |
47 | # Optional: if set to true then the action don't cache or restore ~/go/pkg.
48 | # skip-pkg-cache: true
49 |
50 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
51 | # skip-build-cache: true
--------------------------------------------------------------------------------
/ui/src/locales/date.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | en: {
3 | short: {
4 | year: 'numeric',
5 | month: 'short',
6 | day: 'numeric',
7 | },
8 | medium: {
9 | year: 'numeric',
10 | month: '2-digit',
11 | day: '2-digit',
12 | hour: '2-digit',
13 | minute: '2-digit',
14 | second: '2-digit',
15 | hourCycle: 'h23',
16 | hour12: false,
17 | },
18 | long: {
19 | year: 'numeric',
20 | month: 'short',
21 | day: 'numeric',
22 | hour: 'numeric',
23 | minute: 'numeric',
24 | },
25 | },
26 | cn: {
27 | short: {
28 | year: 'numeric',
29 | month: 'short',
30 | day: 'numeric',
31 | },
32 | medium: {
33 | year: 'numeric',
34 | month: '2-digit',
35 | day: '2-digit',
36 | hour: '2-digit',
37 | minute: '2-digit',
38 | second: '2-digit',
39 | hourCycle: 'h23',
40 | hour12: false,
41 | },
42 | long: {
43 | year: 'numeric',
44 | month: 'short',
45 | day: 'numeric',
46 | hour: 'numeric',
47 | minute: 'numeric',
48 | hour12: true,
49 | },
50 | },
51 | ja: {
52 | short: {
53 | year: 'numeric',
54 | month: 'short',
55 | day: 'numeric',
56 | },
57 | medium: {
58 | year: 'numeric',
59 | month: '2-digit',
60 | day: '2-digit',
61 | hour: '2-digit',
62 | minute: '2-digit',
63 | second: '2-digit',
64 | hourCycle: 'h23',
65 | hour12: false,
66 | },
67 | long: {
68 | year: 'numeric',
69 | month: 'short',
70 | day: 'numeric',
71 | hour: 'numeric',
72 | minute: 'numeric',
73 | hour12: true,
74 | },
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/pkg/guacd/parameters.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | // Network parameters
4 |
5 | const (
6 | Hostname = "hostname"
7 | Port = "port"
8 |
9 | Username = "username"
10 | Password = "password"
11 | )
12 |
13 | // Session Recording
14 | const (
15 | RecordingPath = "recording-path"
16 | CreateRecordingPath = "create-recording-path"
17 | RecordingName = "recording-name"
18 | RecordingExcludeOutput = "recording-exclude-output"
19 | RecordingExcludeMouse = "recording-exclude-mouse"
20 | RecordingIncludeKeys = "recording-include-keys"
21 | )
22 |
23 | // SFTP
24 |
25 | const (
26 | EnableSftp = "enable-sftp"
27 | SftpHostname = "sftp-hostname"
28 | SftpPort = "sftp-port"
29 | SftpHostKey = "sftp-host-key"
30 | SftpUsername = "sftp-username"
31 | SftpPassword = "sftp-password"
32 | SftpPrivateKey = "sftp-private-key"
33 | SftpPassphrase = "sftp-passphrase"
34 | SftpDirectory = "sftp-directory"
35 | SftpRootDirectory = "sftp-root-directory"
36 | SftpServerAliveInterval = "sftp-server-alive-interval"
37 | SftpDisableDownload = "sftp-disable-download"
38 | SftpDisableUpload = "sftp-disable-upload"
39 | )
40 |
41 | // Disabling clipboard access
42 |
43 | const (
44 | DisableCopy = "disable-copy"
45 | DisablePaste = "disable-paste"
46 | )
47 |
48 | // Wake-on-LAN Configuration
49 |
50 | const (
51 | WolSendPacket = "wol-send-packet"
52 | WolMacAddr = "wol-mac-addr"
53 | WolBroadcastAddr = "wol-broadcast-addr"
54 | WolWaitTime = "wol-wait-time"
55 | )
56 |
57 | const (
58 | READONLY = "read-only"
59 | )
60 |
61 | const (
62 | BoolFalse = "false"
63 | BoolTrue = "true"
64 | )
65 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lion-terminal",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "run-p type-check \"build-only {@}\" --",
9 | "preview": "vite preview",
10 | "build-only": "vite build",
11 | "type-check": "vue-tsc --build",
12 | "lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
13 | "lint:eslint": "eslint . --fix",
14 | "lint": "run-s lint:*",
15 | "format": "prettier --write src/"
16 | },
17 | "dependencies": {
18 | "@tailwindcss/vite": "^4.1.10",
19 | "@vueuse/core": "^14.0.0",
20 | "alova": "^3.3.3",
21 | "guacamole-common-js": "1.5.0",
22 | "lucide-vue-next": "^0.525.0",
23 | "naive-ui": "^2.42.0",
24 | "pinia": "^3.0.1",
25 | "vue": "^3.5.13",
26 | "vue-i18n": "^11.1.7",
27 | "vue-router": "^4.5.0",
28 | "vue3-cookies": "^1.0.6"
29 | },
30 | "devDependencies": {
31 | "@tsconfig/node22": "^22.0.1",
32 | "@types/node": "^22.14.0",
33 | "@vitejs/plugin-vue": "^5.2.3",
34 | "@vitejs/plugin-vue-jsx": "^4.1.2",
35 | "@vue/eslint-config-prettier": "^10.2.0",
36 | "@vue/eslint-config-typescript": "^14.5.0",
37 | "@vue/tsconfig": "^0.7.0",
38 | "eslint": "^9.22.0",
39 | "eslint-plugin-oxlint": "^0.16.0",
40 | "eslint-plugin-vue": "~10.0.0",
41 | "jiti": "^2.4.2",
42 | "npm-run-all2": "^7.0.2",
43 | "oxlint": "^0.16.0",
44 | "prettier": "3.5.3",
45 | "sass": "^1.89.2",
46 | "tailwindcss": "^4.1.10",
47 | "typescript": "~5.8.0",
48 | "unplugin-auto-import": "^19.3.0",
49 | "unplugin-vue-components": "^28.8.0",
50 | "vite": "^6.2.4",
51 | "vite-plugin-vue-devtools": "^7.7.2",
52 | "vue-eslint-parser": "^10.0.0",
53 | "vue-tsc": "^2.2.8"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Dockerfile.guacd:
--------------------------------------------------------------------------------
1 | FROM jumpserver/lion-base:20250818_063833 AS stage-build
2 | ARG TARGETARCH
3 |
4 | ARG GOPROXY=https://goproxy.io
5 | ENV CGO_ENABLED=0
6 | ENV GO111MODULE=on
7 |
8 | COPY . .
9 |
10 | WORKDIR /opt/lion/ui
11 |
12 | RUN yarn build
13 |
14 | WORKDIR /opt/lion/
15 |
16 | ARG VERSION
17 | ENV VERSION=$VERSION
18 |
19 | RUN export GOFlAGS="-X 'main.Buildstamp=`date -u '+%Y-%m-%d %I:%M:%S%p'`'" \
20 | && export GOFlAGS="${GOFlAGS} -X 'main.Githash=`git rev-parse HEAD`'" \
21 | && export GOFlAGS="${GOFlAGS} -X 'main.Goversion=`go version`'" \
22 | && export GOFlAGS="${GOFlAGS} -X 'main.Version=${VERSION}'" \
23 | && go build -trimpath -x -ldflags "$GOFlAGS" -o lion .
24 |
25 | RUN chmod +x entrypoint.sh
26 |
27 | FROM guacamole/guacd:1.6.0
28 | ARG TARGETARCH
29 | ENV LANG=en_US.UTF-8
30 |
31 | USER root
32 |
33 | ARG DEPENDENCIES=" \
34 | ca-certificates \
35 | supervisor \
36 | curl \
37 | iputils-ping \
38 | vim \
39 | wget"
40 | RUN set -ex \
41 | && apk update \
42 | && apk add ${DEPENDENCIES}
43 |
44 | WORKDIR /opt/lion
45 |
46 | COPY --from=stage-build /usr/local/bin/check /usr/local/bin/check
47 | COPY --from=stage-build /opt/lion/ui/dist ui/dist/
48 | COPY --from=stage-build /opt/lion/lion .
49 | COPY --from=stage-build /opt/lion/config_example.yml .
50 | COPY --from=stage-build /opt/lion/entrypoint.sh .
51 | COPY --from=stage-build /opt/lion/supervisord.conf /etc/supervisor/supervisord.conf
52 |
53 | ARG VERSION
54 | ENV VERSION=$VERSION
55 |
56 | VOLUME /opt/lion/data
57 |
58 | ENTRYPOINT ["./entrypoint.sh"]
59 |
60 | EXPOSE 8081
61 |
62 | STOPSIGNAL SIGQUIT
63 |
64 | CMD [ "supervisord", "-c", "/etc/supervisor/supervisord.conf" ]
65 |
--------------------------------------------------------------------------------
/.github/workflows/build-ghcr-image.yml:
--------------------------------------------------------------------------------
1 | name: build image and push to ghcr.io
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | BRANCH:
7 | description: 'branch'
8 | type: string
9 | default: 'dev'
10 | VERSION:
11 | description: 'version'
12 | type: string
13 | default: 'dev'
14 | PLATFORMS:
15 | description: 'platforms'
16 | type: string
17 | default: 'linux/amd64,linux/arm64'
18 | IMAGE_TAG:
19 | description: "image tag"
20 | type: string
21 | default: 'dev'
22 | required: true
23 | jobs:
24 | build-and-push:
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@v4
30 | with:
31 | ref: ${{ inputs.BRANCH }}
32 |
33 | - name: Set up QEMU
34 | uses: docker/setup-qemu-action@v3
35 | - name: Set up Docker Buildx
36 | uses: docker/setup-buildx-action@v3
37 | - name: Login to GitHub Container Registry
38 | uses: docker/login-action@v3
39 | with:
40 | registry: ghcr.io
41 | username: ${{ github.repository_owner }}
42 | password: ${{ secrets.GITHUB_TOKEN }}
43 |
44 | - name: Extract repository name
45 | id: repo
46 | run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
47 |
48 | - name: Extract image prefix
49 | run: |
50 | echo "IMAGE_PREFIX=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV
51 |
52 | - name: Build and push multi-arch image
53 | uses: docker/build-push-action@v6
54 | with:
55 | context: .
56 | platforms: ${{ inputs.PLATFORMS }}
57 | push: true
58 | build-args: VERSION=${{ inputs.VERSION }}
59 | file: Dockerfile
60 | tags: ${{ env.IMAGE_PREFIX }}/${{ env.REPO }}:${{ inputs.IMAGE_TAG }}
61 |
62 |
--------------------------------------------------------------------------------
/pkg/session/recorder.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "time"
5 |
6 | "lion/pkg/logger"
7 |
8 | "github.com/jumpserver-dev/sdk-go/model"
9 | "github.com/jumpserver-dev/sdk-go/service"
10 | "github.com/jumpserver-dev/sdk-go/storage"
11 | )
12 |
13 | type CommandRecorder struct {
14 | sessionID string
15 | storage storage.CommandStorage
16 |
17 | queue chan *model.Command
18 | closed chan struct{}
19 |
20 | jmsService *service.JMService
21 | }
22 |
23 | func (c *CommandRecorder) Record(command *model.Command) {
24 | c.queue <- command
25 | }
26 |
27 | func (c *CommandRecorder) End() {
28 | select {
29 | case <-c.closed:
30 | return
31 | default:
32 | }
33 | close(c.closed)
34 | }
35 |
36 | func (c *CommandRecorder) record() {
37 | cmdList := make([]*model.Command, 0, 10)
38 | maxRetry := 0
39 | logger.Infof("Session %s: Command recorder start", c.sessionID)
40 | defer logger.Infof("Session %s: Command recorder close", c.sessionID)
41 | tick := time.NewTicker(time.Second * 10)
42 | defer tick.Stop()
43 | for {
44 | select {
45 | case <-c.closed:
46 | if len(cmdList) == 0 {
47 | return
48 | }
49 | case p, ok := <-c.queue:
50 | if !ok {
51 | return
52 | }
53 | cmdList = append(cmdList, p)
54 | if len(cmdList) < 5 {
55 | continue
56 | }
57 | case <-tick.C:
58 | if len(cmdList) == 0 {
59 | continue
60 | }
61 | }
62 | err := c.storage.BulkSave(cmdList)
63 | if err != nil && c.storage.TypeName() != "server" {
64 | logger.Warnf("Session %s: Switch default command storage save.", c.sessionID)
65 | err = c.jmsService.PushSessionCommand(cmdList)
66 | }
67 | if err == nil {
68 | cmdList = cmdList[:0]
69 | maxRetry = 0
70 | continue
71 | }
72 | if err != nil {
73 | logger.Errorf("Session %s: command bulk save err: %s", c.sessionID, err)
74 | }
75 |
76 | if maxRetry > 5 {
77 | cmdList = cmdList[1:]
78 | }
79 | maxRetry++
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/.github/workflows/build-base-image.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push Base Image
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'pr*'
7 | paths:
8 | - 'go.mod'
9 | - 'Dockerfile-base'
10 | - 'ui/package.json'
11 | - 'ui/yarn.lock'
12 |
13 | jobs:
14 | build-and-push:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v4
20 |
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@v3
23 |
24 | - name: Set up Docker Buildx
25 | uses: docker/setup-buildx-action@v3
26 |
27 | - name: Login to DockerHub
28 | uses: docker/login-action@v2
29 | with:
30 | username: ${{ secrets.DOCKERHUB_USERNAME }}
31 | password: ${{ secrets.DOCKERHUB_TOKEN }}
32 |
33 | - name: Extract date
34 | id: vars
35 | run: echo "IMAGE_TAG=$(date +'%Y%m%d_%H%M%S')" >> $GITHUB_ENV
36 |
37 | - name: Extract repository name
38 | id: repo
39 | run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
40 |
41 | - name: Build and push multi-arch image
42 | uses: docker/build-push-action@v6
43 | with:
44 | platforms: linux/amd64,linux/arm64
45 | push: true
46 | file: Dockerfile-base
47 | tags: jumpserver/${{ env.REPO }}-base:${{ env.IMAGE_TAG }}
48 |
49 | - name: Update Dockerfile
50 | run: |
51 | sed -i 's|-base:.* AS stage-build|-base:${{ env.IMAGE_TAG }} AS stage-build|' Dockerfile
52 |
53 | - name: Commit changes
54 | run: |
55 | git config --global user.name 'github-actions[bot]'
56 | git config --global user.email 'github-actions[bot]@users.noreply.github.com'
57 | git add Dockerfile
58 | git commit -m "perf: Update Dockerfile with new base image tag"
59 | git push
60 | env:
61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62 |
--------------------------------------------------------------------------------
/pkg/guacd/instrcution_name.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | // Streaming instructions
4 | const (
5 | InstructionStreamingAck = "ack"
6 | InstructionStreamingArgv = "argv"
7 | InstructionStreamingAudio = "audio"
8 | InstructionStreamingBlob = "blob"
9 | InstructionStreamingClipboard = "clipboard"
10 | InstructionStreamingEnd = "end"
11 | InstructionStreamingFile = "file"
12 | InstructionStreamingImg = "img"
13 | InstructionStreamingNest = "nest"
14 | InstructionStreamingPipe = "pipe"
15 | InstructionStreamingVideo = "video"
16 | )
17 |
18 | // Object instructions
19 | const (
20 | InstructionObjectBody = "body"
21 | InstructionObjectFilesystem = "filesystem"
22 | InstructionObjectGet = "get"
23 | InstructionObjectPut = "put"
24 | InstructionObjectUndefine = "undefine"
25 | )
26 |
27 | // Client handshake instructions
28 | const (
29 | InstructionClientHandshakeAudio = "audio"
30 | InstructionClientHandshakeConnect = "connect"
31 | InstructionClientHandshakeImage = "image"
32 | InstructionClientHandshakeSelect = "select"
33 | InstructionClientHandshakeSize = "size"
34 | InstructionClientHandshakeTimezone = "timezone"
35 | InstructionClientHandshakeVideo = "video"
36 | )
37 |
38 | // Server handshake instructions
39 | const (
40 | InstructionServerHandshakeArgs = "args"
41 | )
42 |
43 | // Client control instructions
44 | const (
45 | InstructionClientDisconnect = "disconnect"
46 | InstructionClientNop = "nop"
47 | InstructionClientSync = "sync"
48 | )
49 |
50 | // Server control instructions
51 | const (
52 | InstructionServerDisconnect = "disconnect"
53 | InstructionServerError = "error"
54 | InstructionServerLog = "log"
55 | InstructionServerMouse = "mouse"
56 | )
57 |
58 | // Client events
59 | const (
60 | InstructionKey = "key"
61 | InstructionMouse = "mouse"
62 | InstructionSize = "size"
63 | )
64 |
65 | const (
66 | InstructionRequired = "required"
67 | )
68 |
--------------------------------------------------------------------------------
/pkg/guacd/instruction_test.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestValidateInstructionString(t *testing.T) {
8 | tests := []string{
9 | "1.a,2.bc,3.def,10.helloworld;",
10 | "4.test,5.test2;",
11 | "0.;",
12 | "3.foo;",
13 | "4.args,13.VERSION_1_3_0,8.hostname,4.port,6.domain,8.username,8.password," +
14 | "5.width,6.height,3.dpi,15.initial-program,11.color-depth," +
15 | "13.disable-audio,15.enable-printing,12.printer-name,12.enable-drive,10.drive-name," +
16 | "10.drive-path,17.create-drive-path,16.disable-download,14.disable-upload," +
17 | "7.console,13.console-audio,13.server-layout,8.security,11.ignore-cert," +
18 | "12.disable-auth,10.remote-app,14.remote-app-dir,15.remote-app-args,15.static-channels," +
19 | "11.client-name,16.enable-wallpaper,14.enable-theming,21.enable-font-smoothing,23.enable-full-window-drag," +
20 | "26.enable-desktop-composition,22.enable-menu-animations,22.disable-bitmap-caching,25.disable-offscreen-caching,21.disable-glyph-caching,16.preconnection-id,18.preconnection-blob,8.timezone,11.enable-sftp,13.sftp-hostname,13.sftp-host-key,9.sftp-port,13.sftp-username,13.sftp-password,16.sftp-private-key,15.sftp-passphrase,14.sftp-directory,19.sftp-root-directory,26.sftp-server-alive-interval,21.sftp-disable-download,19.sftp-disable-upload,14.recording-path,14.recording-name,24.recording-exclude-output,23.recording-exclude-mouse,22.recording-include-keys,21.create-recording-path,13.resize-method,18.enable-audio-input,9.read-only,16.gateway-hostname,12.gateway-port,14.gateway-domain,16.gateway-username,16.gateway-password,17.load-balance-info," +
21 | "12.disable-copy,13.disable-paste,15.wol-send-packet,12.wol-mac-addr,18.wol-broadcast-addr,13.wol-wait-time;",
22 | "5.audio,1.1,31.audio/L16;",
23 | "5e.audio,1.1,31.audio/L16;",
24 | ";",
25 | }
26 |
27 | for i := range tests {
28 | ins, err := ParseInstructionString(tests[i])
29 | if err != nil {
30 | t.Log(err)
31 | continue
32 | }
33 | t.Log(ins)
34 |
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/tunnel/ws_error.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "strconv"
5 |
6 | "lion/pkg/guacd"
7 | )
8 |
9 | type JMSGuacamoleError struct {
10 | code int
11 | msg string
12 | }
13 |
14 | func (g JMSGuacamoleError) String() string {
15 | ins := g.Instruction()
16 | return ins.String()
17 | }
18 |
19 | func (g JMSGuacamoleError) Instruction() guacd.Instruction {
20 | return guacd.NewInstruction(
21 | guacd.InstructionServerError,
22 | g.msg,
23 | strconv.Itoa(g.code))
24 | }
25 |
26 | func NewJMSGuacamoleError(code int, msg string) JMSGuacamoleError {
27 | return JMSGuacamoleError{
28 | code: code,
29 | msg: msg,
30 | }
31 | }
32 |
33 | const (
34 | InstructionJmsEvent = "jms_event"
35 | )
36 |
37 | func NewJmsEventInstruction(event string, jsonData string) guacd.Instruction {
38 | return guacd.NewInstruction(InstructionJmsEvent, event, jsonData)
39 | }
40 |
41 | // todo: 构造一种通用的错误框架,方便前后端处理异常
42 |
43 | func NewJMSIdleTimeOutError(min int) JMSGuacamoleError {
44 | return NewJMSGuacamoleError(1003, strconv.Itoa(min))
45 | }
46 |
47 | func NewJMSMaxSessionTimeError(hour int) JMSGuacamoleError {
48 | return NewJMSGuacamoleError(1010, strconv.Itoa(hour))
49 | }
50 |
51 | var (
52 | ErrNoSession = NewJMSGuacamoleError(1000, "Not Found Session")
53 |
54 | ErrAuthUser = NewJMSGuacamoleError(1001, "Not auth user")
55 |
56 | ErrBadParams = NewJMSGuacamoleError(1002, "Not session params")
57 |
58 | //ErrIdleTimeOut = NewJMSGuacamoleError(1003, "Terminated by idle timeout")
59 |
60 | ErrPermissionExpired = NewJMSGuacamoleError(1004, "Terminated by permission expired")
61 |
62 | //ErrTerminatedByAdmin = NewJMSGuacamoleError(1005, "Terminated by Admin")
63 |
64 | ErrAPIFailed = NewJMSGuacamoleError(1006, "API failed")
65 |
66 | ErrGatewayFailed = NewJMSGuacamoleError(1007, "Gateway not available")
67 |
68 | ErrGuacamoleServer = NewJMSGuacamoleError(1008, "Connect guacamole server failed")
69 |
70 | ErrPermission = NewJMSGuacamoleError(256, "No permission")
71 |
72 | ErrDisconnect = NewJMSGuacamoleError(1009, "Disconnect by client")
73 | )
74 |
--------------------------------------------------------------------------------
/pkg/logger/gloable.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "log"
7 | "os"
8 | "path/filepath"
9 |
10 | "gopkg.in/natefinch/lumberjack.v2"
11 |
12 | "lion/pkg/config"
13 | )
14 |
15 | var globalLogger = &Logger{newLogger: log.New(os.Stdout, "", log.Lmsgprefix)}
16 |
17 | const logTimeFormat = "2006-01-02 15:04:05"
18 |
19 | func SetupLogger(conf *config.Config) {
20 | fileName := filepath.Join(conf.LogDirPath, "lion.log")
21 | loggerWriter := &lumberjack.Logger{
22 | Filename: fileName,
23 | MaxSize: 5,
24 | MaxAge: 7,
25 | LocalTime: true,
26 | Compress: true,
27 | }
28 | writer := io.MultiWriter(loggerWriter, os.Stdout)
29 | l := log.New(writer, "", log.Lmsgprefix)
30 | globalLogger = &Logger{newLogger: l, level: ParseLevel(conf.LogLevel)}
31 | }
32 |
33 | func Debug(v ...interface{}) {
34 | globalLogger.Output(LevelDebug, fmt.Sprint(v...))
35 | }
36 |
37 | func Debugf(format string, v ...interface{}) {
38 | globalLogger.Output(LevelDebug, fmt.Sprintf(format, v...))
39 | }
40 |
41 | func Info(v ...interface{}) {
42 | globalLogger.Output(LevelInfo, fmt.Sprint(v...))
43 | }
44 |
45 | func Infof(format string, v ...interface{}) {
46 | globalLogger.Output(LevelInfo, fmt.Sprintf(format, v...))
47 | }
48 |
49 | func Warn(v ...interface{}) {
50 | globalLogger.Output(LevelWarn, fmt.Sprint(v...))
51 | }
52 |
53 | func Warnf(format string, v ...interface{}) {
54 | globalLogger.Output(LevelWarn, fmt.Sprintf(format, v...))
55 | }
56 |
57 | func Error(v ...interface{}) {
58 | globalLogger.Output(LevelError, fmt.Sprint(v...))
59 | }
60 |
61 | func Errorf(format string, v ...interface{}) {
62 | globalLogger.Output(LevelError, fmt.Sprintf(format, v...))
63 | }
64 |
65 | func Fatal(v ...interface{}) {
66 | globalLogger.Output(LevelFatal, fmt.Sprint(v...))
67 | }
68 |
69 | func Fatalf(format string, v ...interface{}) {
70 | globalLogger.Output(LevelFatal, fmt.Sprintf(format, v...))
71 | }
72 |
73 | func Panic(v ...interface{}) {
74 | globalLogger.Output(LevelPanic, fmt.Sprint(v...))
75 | }
76 |
77 | func Panicf(format string, v ...interface{}) {
78 | globalLogger.Output(LevelPanic, fmt.Sprintf(format, v...))
79 | }
80 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | # Sequence of patterns matched against refs/tags
4 | tags:
5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
6 |
7 | name: Create Release And Upload assets
8 |
9 | jobs:
10 | create-release:
11 | name: Create Release
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | go_version: [ 'stable' ]
16 | node_version: [ '20' ]
17 | outputs:
18 | upload_url: ${{ steps.create_release.outputs.upload_url }}
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: actions/cache@v4
22 | with:
23 | path: |
24 | ~/.npm
25 | ~/.cache
26 | ~/go/pkg/mod
27 | key: ${{ runner.os }}-build-${{ github.sha }}
28 | restore-keys: ${{ runner.os }}-build-
29 |
30 | - name: Get version
31 | id: get_version
32 | run: |
33 | TAG=$(basename ${GITHUB_REF})
34 | echo "TAG=$TAG" >> $GITHUB_OUTPUT
35 |
36 | - name: Create Release
37 | id: create_release
38 | uses: release-drafter/release-drafter@v6
39 | env:
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | with:
42 | config-name: release-config.yml
43 | version: ${{ steps.get_version.outputs.TAG }}
44 | tag: ${{ steps.get_version.outputs.TAG }}
45 |
46 | - uses: actions/setup-node@v4
47 | with:
48 | node-version: ${{ matrix.node_version }}
49 |
50 | - uses: actions/setup-go@v5
51 | with:
52 | go-version: ${{ matrix.go_version }}
53 | cache: false
54 |
55 | - name: Make Build
56 | id: make_build
57 | run: |
58 | make all -s && ls build
59 | env:
60 | VERSION: ${{ steps.get_version.outputs.TAG }}
61 |
62 | - name: Release Upload Assets
63 | uses: softprops/action-gh-release@v2
64 | if: startsWith(github.ref, 'refs/tags/')
65 | with:
66 | draft: true
67 | files: |
68 | build/*.gz
69 | env:
70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM jumpserver/lion-base:20251125_031411 AS stage-build
2 | ARG TARGETARCH
3 |
4 | ARG GOPROXY=https://goproxy.io
5 | ENV CGO_ENABLED=0
6 | ENV GO111MODULE=on
7 |
8 | COPY . .
9 |
10 | WORKDIR /opt/lion/ui
11 |
12 | RUN yarn build
13 |
14 | WORKDIR /opt/lion/
15 |
16 | ARG VERSION
17 | ENV VERSION=$VERSION
18 |
19 | RUN export GOFlAGS="-X 'main.Buildstamp=`date -u '+%Y-%m-%d %I:%M:%S%p'`'" \
20 | && export GOFlAGS="${GOFlAGS} -X 'main.Githash=`git rev-parse HEAD`'" \
21 | && export GOFlAGS="${GOFlAGS} -X 'main.Goversion=`go version`'" \
22 | && export GOFlAGS="${GOFlAGS} -X 'main.Version=${VERSION}'" \
23 | && go build -trimpath -x -ldflags "$GOFlAGS" -o lion .
24 |
25 | RUN chmod +x entrypoint.sh
26 |
27 | FROM jumpserver/guacd:1.5.5-bullseye
28 | ARG TARGETARCH
29 | ENV LANG=en_US.UTF-8
30 | USER root
31 | ARG DEPENDENCIES=" \
32 | ca-certificates \
33 | supervisor"
34 |
35 | ARG PREFIX_DIR=/opt/guacamole
36 | ENV LD_LIBRARY_PATH=${PREFIX_DIR}/lib
37 |
38 | ARG APT_MIRROR=http://deb.debian.org
39 |
40 | RUN set -ex \
41 | && sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
42 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
43 | && apt-get update \
44 | && apt-get install -y --no-install-recommends ${DEPENDENCIES} \
45 | && apt-get install -y --no-install-recommends $(cat "${PREFIX_DIR}"/DEPENDENCIES) \
46 | && apt-get clean all \
47 | && rm -rf /var/lib/apt/lists/* \
48 | && mkdir -p /lib32 /libx32
49 |
50 | WORKDIR /opt/lion
51 |
52 | COPY --from=stage-build /usr/local/bin/check /usr/local/bin/check
53 | COPY --from=stage-build /opt/lion/ui/dist ui/dist/
54 | COPY --from=stage-build /opt/lion/lion .
55 | COPY --from=stage-build /opt/lion/config_example.yml .
56 | COPY --from=stage-build /opt/lion/entrypoint.sh .
57 | COPY --from=stage-build /opt/lion/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
58 |
59 | ARG VERSION
60 | ENV VERSION=$VERSION
61 |
62 | VOLUME /opt/lion/data
63 |
64 | ENTRYPOINT ["./entrypoint.sh"]
65 |
66 | EXPOSE 8081
67 |
68 | STOPSIGNAL SIGQUIT
69 |
70 | CMD [ "supervisord", "-c", "/etc/supervisor/supervisord.conf" ]
71 |
--------------------------------------------------------------------------------
/.github/workflows/jms-build-test.yml.disabled:
--------------------------------------------------------------------------------
1 | name: "Run Build Test"
2 | on:
3 | push:
4 | paths:
5 | - 'Dockerfile'
6 | - 'Dockerfile*'
7 | - 'Dockerfile-*'
8 | - 'go.mod'
9 | - 'go.sum'
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | component: [lion]
17 | version: [v4]
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: docker/setup-buildx-action@v3
21 |
22 | - name: Prepare Build
23 | run: |
24 | sed -i 's@registry.npmmirror.com@registry.yarnpkg.com@g' ui/yarn.lock
25 | sed -i 's@^FROM registry.fit2cloud.com/jumpserver@FROM ghcr.io/jumpserver@g' Dockerfile
26 | sed -i 's@^FROM registry.fit2cloud.com/jumpserver@FROM ghcr.io/jumpserver@g' Dockerfile-ee
27 |
28 | - name: Login to GitHub Container Registry
29 | uses: docker/login-action@v3
30 | with:
31 | registry: ghcr.io
32 | username: ${{ github.repository_owner }}
33 | password: ${{ secrets.GITHUB_TOKEN }}
34 |
35 | - name: Build CE Image
36 | uses: docker/build-push-action@v5
37 | with:
38 | context: .
39 | push: true
40 | file: Dockerfile
41 | tags: ghcr.io/jumpserver/${{ matrix.component }}:${{ matrix.version }}-ce
42 | platforms: linux/amd64
43 | build-args: |
44 | VERSION=${{ matrix.version }}
45 | GOPROXY=direct
46 | APT_MIRROR=http://deb.debian.org
47 | NPM_REGISTRY=https://registry.yarnpkg.com
48 | outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
49 | cache-from: type=gha
50 | cache-to: type=gha,mode=max
51 |
52 | - name: Build EE Image
53 | uses: docker/build-push-action@v5
54 | with:
55 | context: .
56 | push: false
57 | file: Dockerfile-ee
58 | tags: ghcr.io/jumpserver/${{ matrix.component }}:${{ matrix.version }}
59 | platforms: linux/amd64
60 | build-args: |
61 | VERSION=${{ matrix.version }}
62 | APT_MIRROR=http://deb.debian.org
63 | outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
64 | cache-from: type=gha
65 | cache-to: type=gha,mode=max
--------------------------------------------------------------------------------
/pkg/guacd/information.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | const (
4 | defaultOptimalScreenWidth = 1024
5 | defaultOptimalScreenHeight = 768
6 | defaultOptimalResolution = 96
7 |
8 | defaultTimezone = "Asia/Shanghai"
9 | )
10 |
11 | func NewClientInformation() ClientInformation {
12 | return ClientInformation{
13 | OptimalScreenWidth: defaultOptimalScreenWidth,
14 | OptimalScreenHeight: defaultOptimalScreenHeight,
15 | OptimalResolution: defaultOptimalResolution,
16 | Timezone: defaultTimezone,
17 | AudioMimetypes: []string{"audio/L8", "audio/L16"},
18 | ImageMimetypes: []string{"image/jpeg", "image/png", "image/webp"},
19 | VideoMimetypes: []string{},
20 | }
21 | }
22 |
23 | type ClientInformation struct {
24 | /**
25 | * The optimal screen width requested by the client, in pixels.
26 | */
27 | OptimalScreenWidth int
28 |
29 | /**
30 | * The optimal screen height requested by the client, in pixels.
31 | */
32 | OptimalScreenHeight int
33 |
34 | /**
35 | * The resolution of the optimal dimensions given, in DPI.
36 | */
37 | OptimalResolution int
38 |
39 | /**
40 | * The list of audio mimetypes reported by the client to be supported.
41 | */
42 | AudioMimetypes []string
43 |
44 | /**
45 | * The list of video mimetypes reported by the client to be supported.
46 | */
47 | VideoMimetypes []string
48 |
49 | /**
50 | * The list of image mimetypes reported by the client to be supported.
51 | */
52 | ImageMimetypes []string
53 |
54 | /**
55 | * The timezone reported by the client.
56 | */
57 | Timezone string
58 |
59 | /**
60 | * qwerty keyboard layout
61 | */
62 | KeyboardLayout string
63 | }
64 |
65 | func (info *ClientInformation) ExtraConfig() map[string]string {
66 | ret := make(map[string]string)
67 | if layout, ok := RDPServerLayouts[info.KeyboardLayout]; ok {
68 | ret[RDPServerLayout] = layout
69 | }
70 | return ret
71 | }
72 |
73 | func (info *ClientInformation) Clone() ClientInformation {
74 | return ClientInformation{
75 | OptimalScreenWidth: info.OptimalScreenWidth,
76 | OptimalScreenHeight: info.OptimalScreenHeight,
77 | OptimalResolution: info.OptimalResolution,
78 | ImageMimetypes: []string{"image/jpeg", "image/png", "image/webp"},
79 | Timezone: info.Timezone,
80 | KeyboardLayout: info.KeyboardLayout,
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/ui/src/components/SessionShare/index.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
50 |
51 | {{ t('OnlineUser') }}
52 |
53 | {{ props.users?.length || 0 }}
54 |
55 |
56 |
57 |
58 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/ui/src/components/KeyboardOption.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
75 |
--------------------------------------------------------------------------------
/ui/src/components/CombinationKey.vue:
--------------------------------------------------------------------------------
1 |
78 |
79 |
80 |
81 |
82 |
83 |
89 |
90 |
91 |
92 | {{ item.label }}
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/pkg/guacd/qwertz.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | var RDPServerLayouts = map[string]string{
4 | "de-de-qwertz": "de-de-qwertz",
5 | "de-ch-qwertz": "de-ch-qwertz",
6 | "en-gb-qwerty": "en-gb-qwerty",
7 | "en-us-qwerty": "en-us-qwerty",
8 | "es-es-qwerty": "es-es-qwerty",
9 | "es-latam-qwerty": "es-latam-qwerty",
10 | "failsafe": "failsafe",
11 | "fr-be-azerty": "fr-be-azerty",
12 | "fr-fr-azerty": "fr-fr-azerty",
13 | "fr-ca-qwerty": "fr-ca-qwerty",
14 | "fr-ch-qwertz": "fr-ch-qwertz",
15 | "hu-hu-qwertz": "hu-hu-qwertz",
16 | "it-it-qwerty": "it-it-qwerty",
17 | "ja-jp-qwerty": "ja-jp-qwerty",
18 | "no-no-qwerty": "no-no-qwerty",
19 | "pl-pl-qwerty": "pl-pl-qwerty",
20 | "pt-br-qwerty": "pt-br-qwerty",
21 | "pt-pt-qwerty": "pt-pt-qwerty",
22 | "ro-ro-qwerty": "ro-ro-qwerty",
23 | "sv-se-qwerty": "sv-se-qwerty",
24 | "da-dk-qwerty": "da-dk-qwerty",
25 | "tr-tr-qwerty": "tr-tr-qwerty",
26 | }
27 |
28 | /*
29 | https://github.com/apache/guacamole-client/blob/fe6677bf4ebaa8662418013ab1af8c7060224ef5/guacamole/src/main/frontend/src/translations/zh.json
30 |
31 | "FIELD_OPTION_SERVER_LAYOUT_DE_CH_QWERTZ" : "Swiss German (Qwertz)",
32 | "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "German (Qwertz)",
33 | "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "",
34 | "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "UK English (Qwerty)",
35 | "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US English (Qwerty)",
36 | "FIELD_OPTION_SERVER_LAYOUT_ES_ES_QWERTY" : "Spanish (Qwerty)",
37 | "FIELD_OPTION_SERVER_LAYOUT_ES_LATAM_QWERTY" : "Latin American (Qwerty)",
38 | "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode",
39 | "FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Belgian French (Azerty)",
40 | "FIELD_OPTION_SERVER_LAYOUT_FR_CA_QWERTY" : "Canadian French (Qwerty)",
41 | "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Swiss French (Qwertz)",
42 | "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)",
43 | "FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Hungarian (Qwertz)",
44 | "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)",
45 | "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japanese (Qwerty)",
46 | "FIELD_OPTION_SERVER_LAYOUT_NO_NO_QWERTY" : "Norwegian (Qwerty)",
47 | "FIELD_OPTION_SERVER_LAYOUT_PL_PL_QWERTY" : "Polish (Qwerty)",
48 | "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portuguese Brazilian (Qwerty)",
49 | "FIELD_OPTION_SERVER_LAYOUT_PT_PT_QWERTY" : "Portuguese (Qwerty)",
50 | "FIELD_OPTION_SERVER_LAYOUT_RO_RO_QWERTY" : "Romanian (Qwerty)",
51 | "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)",
52 | "FIELD_OPTION_SERVER_LAYOUT_DA_DK_QWERTY" : "Danish (Qwerty)",
53 | "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Turkish-Q (Qwerty)",
54 | */
55 |
--------------------------------------------------------------------------------
/pkg/guacd/keyboard_map.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | var keyidentifierKeysym = map[int]string{
4 | 0xFF3D: "AllCandidates",
5 | 0xFF30: "Alphanumeric",
6 | 0xFFE9: "Alt",
7 | 0xFE03: "Alt",
8 | 0xFD0E: "Attn",
9 | 0xFF54: "ArrowDown",
10 | 0xFF51: "ArrowLeft",
11 | 0xFF53: "ArrowRight",
12 | 0xFF52: "ArrowUp",
13 | 0xFF08: "Backspace",
14 | 0xFFE5: "CapsLock",
15 | 0xFF69: "Cancel",
16 | 0xFF0B: "Clear",
17 | 0xFF21: "Convert",
18 | 0xFD15: "Copy",
19 | 0xFD1C: "Crsel",
20 | 0xFF37: "CodeInput",
21 | 0xFF20: "Compose",
22 | 0xFFE3: "Control",
23 | 0xFFE4: "Control",
24 | 0xFF67: "ContextMenu",
25 | 0xFFFF: "Delete",
26 | 0xFF57: "End",
27 | 0xFF0D: "Enter\r",
28 | 0xFD06: "EraseEof",
29 | 0xFF1B: "Escape",
30 | 0xFF62: "Execute",
31 | 0xFD1D: "Exsel",
32 | 0xFFBE: "F1",
33 | 0xFFBF: "F2",
34 | 0xFFC0: "F3",
35 | 0xFFC1: "F4",
36 | 0xFFC2: "F5",
37 | 0xFFC3: "F6",
38 | 0xFFC4: "F7",
39 | 0xFFC5: "F8",
40 | 0xFFC6: "F9",
41 | 0xFFC7: "F10",
42 | 0xFFC8: "F11",
43 | 0xFFC9: "F12",
44 | 0xFFCA: "F13",
45 | 0xFFCB: "F14",
46 | 0xFFCC: "F15",
47 | 0xFFCD: "F16",
48 | 0xFFCE: "F17",
49 | 0xFFCF: "F18",
50 | 0xFFD0: "F19",
51 | 0xFFD1: "F20",
52 | 0xFFD2: "F21",
53 | 0xFFD3: "F22",
54 | 0xFFD4: "F23",
55 | 0xFFD5: "F24",
56 | 0xFF68: "Find",
57 | 0xFE0C: "GroupFirst",
58 | 0xFE0E: "GroupLast",
59 | 0xFE08: "GroupNext",
60 | 0xFE0A: "GroupPrevious",
61 | 0xFF31: "HangulMode",
62 | 0xFF29: "Hankaku",
63 | 0xFF34: "HanjaMode",
64 | 0xFF6A: "Help",
65 | 0xFF25: "Hiragana",
66 | 0xFF27: "HiraganaKatakana",
67 | 0xFF50: "Home",
68 | 0xFFED: "Hyper",
69 | 0xFFEE: "Hyper",
70 | 0xFF63: "Insert",
71 | 0xFF24: "JapaneseRomaji",
72 | 0xFF38: "JunjaMode",
73 | 0xFF2D: "KanaMode",
74 | 0xFF26: "Katakana",
75 | 0xFFE7: "Meta",
76 | 0xFFE8: "Meta",
77 | 0xFF7E: "ModeChange",
78 | 0xFF7F: "NumLock",
79 | 0xFF56: "PageDown",
80 | 0xFF55: "PageUp",
81 | 0xFF13: "Pause",
82 | 0xFD16: "Play",
83 | 0xFF3E: "PreviousCandidate",
84 | 0xFF61: "PrintScreen",
85 | 0xFF66: "Redo",
86 | 0xFF14: "Scroll",
87 | 0xFF60: "Select",
88 | 0xFFAC: "Separator",
89 | 0xFFE1: "Shift",
90 | 0xFFE2: "Shift",
91 | 0xFF3C: "SingleCandidate",
92 | 0xFFEC: "Super",
93 | 0xFF09: "Tab",
94 | 0xFF65: "Undo",
95 | 0xFFEB: "Win",
96 | 0xFF28: "Zenkaku",
97 | 0xFF2A: "ZenkakuHankaku",
98 | }
99 |
100 | const (
101 | KeyPress = "1"
102 | KeyRelease = "0"
103 | )
104 |
105 | const (
106 | MouseLeft = "1"
107 | MouseMiddle = "2"
108 | MouseRight = "4"
109 | MouseUp = "8"
110 | MouseDown = "16"
111 | )
112 |
113 | const (
114 | KeyCodeUnknown = "Unknown"
115 | )
116 |
117 | func KeysymToCharacter(keysym int) string {
118 | return keyidentifierKeysym[keysym]
119 | }
120 |
--------------------------------------------------------------------------------
/pkg/tunnel/stream_input.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "encoding/base64"
5 | "io"
6 | "sync"
7 |
8 | "lion/pkg/guacd"
9 | "lion/pkg/logger"
10 | )
11 |
12 | type InputStreamInterceptingFilter struct {
13 | tunnel *Connection
14 | streams map[string]*InputStreamResource
15 | sync.Mutex
16 | }
17 |
18 | func (filter *InputStreamInterceptingFilter) Filter(unfilteredInstruction *guacd.Instruction) *guacd.Instruction {
19 |
20 | if unfilteredInstruction.Opcode == guacd.InstructionStreamingAck {
21 | filter.handleAck(unfilteredInstruction)
22 | }
23 | return unfilteredInstruction
24 | }
25 |
26 | func (filter *InputStreamInterceptingFilter) handleAck(unfilteredInstruction *guacd.Instruction) {
27 | filter.Lock()
28 | defer filter.Unlock()
29 | // Verify all required arguments are present
30 | args := unfilteredInstruction.Args
31 | if len(args) < 3 {
32 | return
33 | }
34 | index := args[0]
35 | if stream, ok := filter.streams[index]; ok {
36 | status := args[2]
37 | if status != "0" {
38 | return
39 | }
40 |
41 | // Send next blob
42 | filter.readNextBlob(stream)
43 |
44 | //stream.reader.Read()
45 | }
46 | }
47 |
48 | func (filter *InputStreamInterceptingFilter) readNextBlob(stream *InputStreamResource) {
49 | buf := make([]byte, 6048)
50 | nr, err := stream.reader.Read(buf)
51 | if nr > 0 {
52 | filter.sendBlob(stream.streamIndex, buf[:nr])
53 | }
54 | if err != nil {
55 | if err != io.EOF {
56 | stream.err = err
57 | }
58 | filter.closeInterceptedStream(stream.streamIndex)
59 | return
60 | }
61 |
62 | }
63 |
64 | func (filter *InputStreamInterceptingFilter) sendBlob(index string, p []byte) {
65 | err := filter.tunnel.WriteTunnelMessage(guacd.NewInstruction(
66 | guacd.InstructionStreamingBlob, index, base64.StdEncoding.EncodeToString(p)))
67 | if err != nil {
68 | logger.Error(err)
69 | }
70 | }
71 |
72 | func (filter *InputStreamInterceptingFilter) closeInterceptedStream(index string) {
73 | if outStream, ok := filter.streams[index]; ok {
74 | close(outStream.done)
75 | }
76 | delete(filter.streams, index)
77 | }
78 |
79 | func (filter *InputStreamInterceptingFilter) addInputStream(stream *InputStreamResource) {
80 | filter.Lock()
81 | defer filter.Unlock()
82 | filter.streams[stream.streamIndex] = stream
83 | filter.readNextBlob(stream)
84 | }
85 |
86 | // 上传文件的对象
87 | type InputStreamResource struct {
88 | streamIndex string
89 | //mediaType string // application/octet-stream
90 | reader io.ReadCloser
91 | done chan struct{}
92 |
93 | err error
94 | }
95 |
96 | func (r *InputStreamResource) Wait() {
97 | <-r.done
98 | }
99 |
100 | func (r *InputStreamResource) WaitErr() error {
101 | return r.err
102 | }
103 |
--------------------------------------------------------------------------------
/ui/src/utils/common.ts:
--------------------------------------------------------------------------------
1 | export function sanitizeFilename(filename: string): string {
2 | return filename.replace(/[\\\/]+/g, '_');
3 | }
4 |
5 | export const FileType = {
6 | NORMAL: 'NORMAL',
7 | DIRECTORY: 'DIRECTORY',
8 | };
9 |
10 | export function isDirectory(guacFile: { type: string }): boolean {
11 | return guacFile.type === FileType.DIRECTORY;
12 | }
13 |
14 | let streamOrigin;
15 | // Work-around for IE missing window.location.origin
16 | if (!window.location.origin) {
17 | streamOrigin =
18 | window.location.protocol +
19 | '//' +
20 | window.location.hostname +
21 | (window.location.port ? ':' + window.location.port : '');
22 | } else {
23 | streamOrigin = window.location.origin;
24 | }
25 | const scheme = document.location.protocol === 'https:' ? 'wss' : 'ws';
26 | const port = document.location.port ? ':' + document.location.port : '';
27 | const BASE_WS_URL = scheme + '://' + document.location.hostname + port;
28 | const BASE_URL = document.location.protocol + '//' + document.location.hostname + port;
29 | export { BASE_WS_URL, BASE_URL };
30 |
31 | export const OriginSite = streamOrigin;
32 |
33 | export const BaseAPIURL = streamOrigin + '/lion/api';
34 |
35 | const sessionBaseAPI = '/api';
36 | const wsURL = '/lion/ws/connect/';
37 | const monitorWsURL = '/lion/ws/monitor/';
38 |
39 | export function getCurrentConnectParams() {
40 | const urlParams = getURLParams();
41 | const data: any = {};
42 | urlParams.forEach(function (value, key, parent) {
43 | data[key] = value;
44 | });
45 | const result: any = {};
46 | result['data'] = data;
47 | result['ws'] = wsURL;
48 | result['api'] = sessionBaseAPI;
49 | return result;
50 | }
51 |
52 | export function getMonitorConnectParams() {
53 | const urlParams = getURLParams();
54 | const data: any = {};
55 | urlParams.forEach(function (value, key, parent) {
56 | data[key] = value;
57 | });
58 | const result: any = {};
59 | result['data'] = data;
60 | result['ws'] = monitorWsURL;
61 | return result;
62 | }
63 |
64 | export function getURLParams() {
65 | return new URLSearchParams(window.location.search.slice(1));
66 | }
67 |
68 | export function localStorageGet(key: string): string | object | null {
69 | let data = localStorage.getItem(key);
70 | if (!data) {
71 | return data;
72 | }
73 | try {
74 | data = JSON.parse(data);
75 | return data;
76 | } catch (e) {
77 | //
78 | }
79 | return data;
80 | }
81 |
82 | export function getCookie(name: string): string | undefined {
83 | const match = document.cookie.match(new RegExp(name + '=([^;]+)'));
84 | return match ? match[1] : undefined;
85 | }
86 |
87 | export function CopyTextToClipboard(text: string) {
88 | const transfer = document.createElement('textarea');
89 | document.body.appendChild(transfer);
90 | transfer.value = text;
91 | transfer.focus();
92 | transfer.select();
93 | document.execCommand('copy');
94 | document.body.removeChild(transfer);
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/guacd/parameters_rdp.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | // Network parameters
4 | const (
5 | RDPHostname = "hostname"
6 | RDPPort = "port" // default 3389
7 | )
8 |
9 | // Authentication and security
10 |
11 | const (
12 | RDPUsername = "username"
13 | RDPPassword = "password"
14 | RDPDomain = "domain"
15 | RDPSecurity = "security" // any | nla | nla-ext | tls | vmconnect | rdp
16 | RDPIgnoreCert = "ignore-cert"
17 | RDPDisableAuth = "disable-auth"
18 | )
19 |
20 | // Session settings
21 |
22 | const (
23 | RDPClientName = "client-name"
24 | RDPConsole = "console"
25 | RDPInitialProgram = "initial-program"
26 | RDPServerLayout = "server-layout"
27 | RDPTimezone = "timezone"
28 | )
29 |
30 | // Display settings
31 |
32 | const (
33 | RDPColorDepth = "color-depth"
34 | RDPWidth = "width"
35 | RDPHeight = "height"
36 | RDPDpi = "dpi"
37 | RDPResizeMethod = "resize-method" // display-update| reconnect
38 | )
39 |
40 | // Device redirection
41 | // https://tools.ietf.org/html/rfc4856
42 | const (
43 | RDPDisableAudio = "disable-audio"
44 | RDPEnableAudioInput = "enable-audio-input"
45 | RDPPrinterName = "printer-name"
46 | RDPEnableDrive = "enable-drive"
47 | RDPDisableDownload = "disable-download"
48 | RDPDisableUpload = "disable-upload"
49 | RDPDriveName = "drive-name"
50 | RDPDrivePath = "drive-path"
51 | RDPCreateDrivePath = "create-drive-path"
52 | RDPConsoleAudio = "console-audio"
53 | RDPStaticChannels = "static-channels"
54 | )
55 |
56 | // Preconnection PDU (Hyper-V / VMConnect)
57 |
58 | const (
59 | RDPPreConnectionId = "preconnection-id"
60 | RDPPreConnectionBlob = "preconnection-blob"
61 | )
62 |
63 | // Remote desktop gateway
64 |
65 | const (
66 | RDPGatewayHostname = "gateway-hostname"
67 | RDPGatewayPort = "gateway-port"
68 | RDPGatewayUsername = "gateway-username"
69 | RDPGatewayPassword = "gateway-password"
70 | RDPGatewayDomain = "gateway-domain"
71 | )
72 |
73 | // Load balancing and RDP connection brokers
74 |
75 | const (
76 | RDPLoadBalanceInfo = "load-balance-info"
77 | )
78 |
79 | // Performance flags
80 |
81 | const (
82 | RDPEnableWallpaper = "enable-wallpaper"
83 | RDPEnableTheming = "enable-theming"
84 | RDPEnableFontSmoothing = "enable-font-smoothing"
85 | RDPEnableFullWindowDrag = "enable-full-window-drag"
86 | RDPEnableDesktopComposition = "enable-desktop-composition"
87 | RDPEnableMenuAnimations = "enable-menu-animations"
88 | RDPDisableBitmapCaching = "disable-bitmap-caching"
89 | RDPDisableOffscreenCaching = "disable-offscreen-caching"
90 | RDPDisableGlyphCaching = "disable-glyph-caching"
91 | )
92 |
93 | // RemoteApp
94 |
95 | const (
96 | RDPRemoteApp = "remote-app"
97 | RDPRemoteAppDir = "remote-app-dir"
98 | RDPRemoteAppArgs = "remote-app-args"
99 | )
100 |
--------------------------------------------------------------------------------
/pkg/guacd/instruction.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | type Instruction struct {
11 | Opcode string
12 | Args []string
13 | ProtocolForm string
14 | }
15 |
16 | func NewInstruction(opcode string, args ...string) (ret Instruction) {
17 | ret.Opcode = opcode
18 | ret.Args = args
19 | return ret
20 | }
21 |
22 | // 构造 `OPCODE,ARG1,ARG2,ARG3,...;` 的格式
23 | func (opt *Instruction) String() string {
24 | if len(opt.ProtocolForm) > 0 {
25 | return opt.ProtocolForm
26 | }
27 | opt.ProtocolForm = fmt.Sprintf("%d.%s", len(opt.Opcode), opt.Opcode)
28 | for _, value := range opt.Args {
29 | opt.ProtocolForm += fmt.Sprintf(",%d.%s", len([]rune(value)), value)
30 | }
31 | opt.ProtocolForm += semicolonDelimiter
32 | return opt.ProtocolForm
33 | }
34 |
35 | const (
36 | semicolonDelimiter = ";"
37 | )
38 |
39 | const (
40 | ByteDotDelimiter = '.'
41 | ByteCommaDelimiter = ','
42 | ByteSemicolonDelimiter = ';'
43 | )
44 |
45 | var (
46 | ErrInstructionMissSemicolon = errors.New("instruction without semicolon")
47 | ErrInstructionMissDot = errors.New("instruction without dot")
48 | ErrInstructionBadDigit = errors.New("instruction with bad digit")
49 | ErrInstructionBadContent = errors.New("instruction with bad Content")
50 | )
51 |
52 | // raw 是以 `;` 为结束符的原生字符串
53 | func ParseInstructionString(raw string) (ret Instruction, err error) {
54 | if !strings.HasSuffix(raw, semicolonDelimiter) {
55 | return Instruction{}, fmt.Errorf("%w: %s", ErrInstructionMissSemicolon, raw)
56 | }
57 | raw = trimSuffixSemicolonDelimiter(raw)
58 | rawRune := []rune(raw)
59 | args := make([]string, 0, 1024)
60 | var i = 0
61 | for len(rawRune) > 0 {
62 | switch rawRune[i] {
63 | case ByteCommaDelimiter, ByteSemicolonDelimiter:
64 | // 重置数据
65 | rawRune = rawRune[i+1:]
66 | i = 0
67 | continue
68 | case ByteDotDelimiter:
69 | // 解析 LENGTH.VALUE 格式的数据
70 | dotIndex := rawRune[:i]
71 | argContentLen, err := strconv.Atoi(string(dotIndex))
72 | if err != nil {
73 | return Instruction{}, fmt.Errorf("%w: %s",
74 | ErrInstructionBadDigit, err.Error())
75 | }
76 | if argContentLen > len(rawRune[i+1:]) {
77 | return Instruction{}, fmt.Errorf("%w: %s",
78 | ErrInstructionBadContent, raw)
79 | }
80 | argContent := string(rawRune[i+1 : argContentLen+i+1])
81 | args = append(args, argContent)
82 | rawRune = rawRune[argContentLen+i+1:]
83 | i = 0
84 | continue
85 | default:
86 | i++
87 | }
88 | if i >= len(rawRune) {
89 | return Instruction{}, fmt.Errorf("%w: %s", ErrInstructionBadContent, raw)
90 | }
91 | }
92 | if len(args) < 1 {
93 | return Instruction{}, fmt.Errorf("%w: no content", ErrInstructionBadContent)
94 | }
95 | return NewInstruction(args[0], args[1:]...), nil
96 | }
97 |
98 | func trimSuffixSemicolonDelimiter(content string) string {
99 | return strings.TrimSuffix(content, semicolonDelimiter)
100 | }
101 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NAME=lion
2 | BUILDDIR=build
3 |
4 | VERSION ?= Unknown
5 | BuildTime := $(shell date -u '+%Y-%m-%d %I:%M:%S%p')
6 | COMMIT := $(shell git rev-parse HEAD)
7 | GOVERSION := $(shell go version)
8 |
9 | GOOS := $(shell go env GOOS)
10 | GOARCH := $(shell go env GOARCH)
11 |
12 | LDFLAGS=-w -s
13 |
14 | GOLDFLAGS=-X 'main.Version=$(VERSION)'
15 | GOLDFLAGS+=-X 'main.Buildstamp=$(BuildTime)'
16 | GOLDFLAGS+=-X 'main.Githash=$(COMMIT)'
17 | GOLDFLAGS+=-X 'main.Goversion=$(GOVERSION)'
18 |
19 | GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags "$(GOLDFLAGS) ${LDFLAGS}"
20 |
21 | UIDIR=ui
22 |
23 | define make_artifact_full
24 | GOOS=$(1) GOARCH=$(2) $(GOBUILD) -o $(BUILDDIR)/$(NAME)-$(1)-$(2)
25 | mkdir -p $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2)/$(UIDIR)/dist/
26 | cp $(BUILDDIR)/$(NAME)-$(1)-$(2) $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2)/$(NAME)
27 | cp README.md $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2)/README.md
28 | cp LICENSE $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2)/LICENSE
29 | cp config_example.yml $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2)/config_example.yml
30 | cp entrypoint.sh $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2)/entrypoint.sh
31 | cp supervisord.conf $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2)/supervisord.conf
32 | cp -r $(UIDIR)/dist/* $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2)/$(UIDIR)/dist/
33 |
34 | cd $(BUILDDIR) && tar -czvf $(NAME)-$(VERSION)-$(1)-$(2).tar.gz $(NAME)-$(VERSION)-$(1)-$(2)
35 | rm -rf $(BUILDDIR)/$(NAME)-$(VERSION)-$(1)-$(2) $(BUILDDIR)/$(NAME)-$(1)-$(2)
36 | endef
37 |
38 | build:
39 | GOARCH=$(GOARCH) GOOS=$(GOOS) $(GOBUILD) -o $(BUILDDIR)/$(NAME) .
40 |
41 | all: lion-ui
42 | $(call make_artifact_full,darwin,amd64)
43 | $(call make_artifact_full,darwin,arm64)
44 | $(call make_artifact_full,linux,amd64)
45 | $(call make_artifact_full,linux,arm64)
46 | $(call make_artifact_full,linux,mips64le)
47 | $(call make_artifact_full,linux,ppc64le)
48 | $(call make_artifact_full,linux,s390x)
49 | $(call make_artifact_full,linux,riscv64)
50 | $(call make_artifact_full,linux,loong64)
51 |
52 | local: lion-ui
53 | $(call make_artifact_full,$(shell go env GOOS),$(shell go env GOARCH))
54 |
55 | darwin-amd64: lion-ui
56 | $(call make_artifact_full,darwin,amd64)
57 |
58 | darwin-arm64: lion-ui
59 | $(call make_artifact_full,darwin,arm64)
60 |
61 | linux-amd64: lion-ui
62 | $(call make_artifact_full,linux,amd64)
63 |
64 | linux-arm64: lion-ui
65 | $(call make_artifact_full,linux,arm64)
66 |
67 | linux-loong64: lion-ui
68 | $(call make_artifact_full,linux,loong64)
69 |
70 | linux-ppc64le: lion-ui
71 | $(call make_artifact_full,linux,ppc64le)
72 |
73 | linux-mips64le: lion-ui
74 | $(call make_artifact_full,linux,mips64le)
75 |
76 | linux-s390x: lion-ui
77 | $(call make_artifact_full,linux,s390x)
78 |
79 | linux-riscv64: lion-ui
80 | $(call make_artifact_full,linux,riscv64)
81 |
82 | .PHONY: docker
83 | docker:
84 | docker buildx build --build-arg VERSION=$(VERSION) -t jumpserver/lion:$(VERSION) .
85 |
86 | lion-ui:
87 | @echo "build ui"
88 | @cd $(UIDIR) && yarn install && yarn build
89 |
90 | clean:
91 | rm -rf $(BUILDDIR)
92 | -rm -rf $(UIDIR)/dist
93 |
--------------------------------------------------------------------------------
/ui/src/App.vue:
--------------------------------------------------------------------------------
1 |
67 |
68 |
69 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/ui/src/utils/status.ts:
--------------------------------------------------------------------------------
1 | export const ErrorStatusCodes: any = {
2 | 256: 'GuaErrUnSupport',
3 | 514: 'GuaErrUpStreamTimeout',
4 | 521: 'GuaErrSessionConflict',
5 | 769: 'GuaErrClientUnauthorized',
6 | 1000: 'JMSErrNoSession',
7 | 1001: 'JMSErrAuthUser',
8 | 1002: 'JMSErrBadParams',
9 | 1003: 'JMSErrIdleTimeOut',
10 | 1004: 'JMSErrPermissionExpired',
11 | 1005: 'JMSErrTerminatedByAdmin',
12 | 1006: 'JMSErrAPIFailed',
13 | 1007: 'JMSErrGatewayFailed',
14 | 1008: 'JMSErrGuacamoleServer',
15 | 1009: 'JMSErrDisconnected',
16 | 1010: 'JMSErrMaxSession',
17 | 1011: 'JMSErrRemoveShareUser',
18 | };
19 |
20 | export function ConvertAPIError(errMsg: string | any): string {
21 | if (typeof errMsg !== 'string') {
22 | return errMsg;
23 | }
24 | const errArray = errMsg.split(':');
25 | if (errArray.length >= 1) {
26 | return APIErrorType[errArray[0]] || errMsg;
27 | }
28 | return errMsg;
29 | }
30 |
31 | export const APIErrorType: any = {
32 | 'connect API core err': 'JMSErrAPIFailed',
33 | 'connect Panda API core err': 'JMSErrAPIFailed',
34 | 'unsupported type': 'JMSErrBadParams',
35 | 'unsupported protocol': 'JMSErrBadParams',
36 | 'permission deny': 'JMSErrPermission',
37 | };
38 |
39 | export function ConvertGuacamoleError(errMsg: string | any): string {
40 | if (typeof errMsg !== 'string') {
41 | return errMsg;
42 | }
43 | return GuacamoleErrMsg[errMsg] || errMsg;
44 | }
45 |
46 | export const GuacamoleErrMsg: any = {
47 | 'Disconnected.': 'GuacamoleErrDisconnected',
48 | 'Credentials expired.': 'GuacamoleErrCredentialsExpired',
49 | 'Security negotiation failed (wrong security type?)': 'GuacamoleErrSecurityNegotiationFailed',
50 | 'Access denied by server (account locked/disabled?)': 'GuacamoleErrAccessDenied',
51 | 'Authentication failure (invalid credentials?)': 'GuacamoleErrAuthenticationFailure',
52 | 'SSL/TLS connection failed (untrusted/self-signed certificate?)':
53 | 'GuacamoleErrSSLTLSConnectionFailed',
54 | 'DNS lookup failed (incorrect hostname?)': 'GuacamoleErrDNSLookupFailed',
55 | 'Server refused connection (wrong security type?)':
56 | 'GuacamoleErrServerRefusedConnectionBySecurityType',
57 | 'Connection failed (server unreachable?)': 'GuacamoleErrConnectionFailed',
58 | 'Upstream error.': 'GuacamoleErrUpstreamError',
59 | 'Forcibly disconnected.': 'GuacamoleErrForciblyDisconnected',
60 | 'Logged off.': 'GuacamoleErrLoggedOff',
61 | 'Idle session time limit exceeded.': 'GuacamoleErrIdleSessionTimeLimitExceeded',
62 | 'Active session time limit exceeded.': 'GuacamoleErrActiveSessionTimeLimitExceeded',
63 | 'Disconnected by other connection.': 'GuacamoleErrDisconnectedByOtherConnection',
64 | 'Server refused connection.': 'GuacamoleErrServerRefusedConnection',
65 | 'Insufficient privileges.': 'GuacamoleErrInsufficientPrivileges',
66 | 'Manually disconnected.': 'GuacamoleErrManuallyDisconnected',
67 | 'Manually logged off.': 'GuacamoleErrManuallyLoggedOff',
68 |
69 | 'Unsupported credential type requested.': 'GuacamoleErrUnsupportedCredentialTypeRequested',
70 | 'Unable to connect to VNC server.': 'GuacamoleErrUnableToConnectToVNCServer',
71 | };
72 |
--------------------------------------------------------------------------------
/ui/components.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // @ts-nocheck
3 | // Generated by unplugin-vue-components
4 | // Read more: https://github.com/vuejs/core/pull/3399
5 | // biome-ignore lint: disable
6 | export {}
7 |
8 | /* prettier-ignore */
9 | declare module 'vue' {
10 | export interface GlobalComponents {
11 | CardContainer: typeof import('./src/components/CardContainer/index.vue')['default']
12 | ClipBoardText: typeof import('./src/components/ClipBoardText.vue')['default']
13 | CombinationKey: typeof import('./src/components/CombinationKey.vue')['default']
14 | CreateLink: typeof import('./src/components/SessionShare/widget/CreateLink.vue')['default']
15 | FileManager: typeof import('./src/components/FileManager.vue')['default']
16 | KeyboardOption: typeof import('./src/components/KeyboardOption.vue')['default']
17 | NButton: typeof import('naive-ui')['NButton']
18 | NButtonGroup: typeof import('naive-ui')['NButtonGroup']
19 | NCard: typeof import('naive-ui')['NCard']
20 | NCollapse: typeof import('naive-ui')['NCollapse']
21 | NCollapseItem: typeof import('naive-ui')['NCollapseItem']
22 | NConfigProvider: typeof import('naive-ui')['NConfigProvider']
23 | NDescriptions: typeof import('naive-ui')['NDescriptions']
24 | NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
25 | NDialogProvider: typeof import('naive-ui')['NDialogProvider']
26 | NDivider: typeof import('naive-ui')['NDivider']
27 | NDrawer: typeof import('naive-ui')['NDrawer']
28 | NDrawerContent: typeof import('naive-ui')['NDrawerContent']
29 | NDropdown: typeof import('naive-ui')['NDropdown']
30 | NEmpty: typeof import('naive-ui')['NEmpty']
31 | NFlex: typeof import('naive-ui')['NFlex']
32 | NFormItem: typeof import('naive-ui')['NFormItem']
33 | NGi: typeof import('naive-ui')['NGi']
34 | NGradientText: typeof import('naive-ui')['NGradientText']
35 | NGrid: typeof import('naive-ui')['NGrid']
36 | NInput: typeof import('naive-ui')['NInput']
37 | NMessageProvider: typeof import('naive-ui')['NMessageProvider']
38 | NModal: typeof import('naive-ui')['NModal']
39 | NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
40 | NPopconfirm: typeof import('naive-ui')['NPopconfirm']
41 | NSelect: typeof import('naive-ui')['NSelect']
42 | NSpin: typeof import('naive-ui')['NSpin']
43 | NSwitch: typeof import('naive-ui')['NSwitch']
44 | NTabPane: typeof import('naive-ui')['NTabPane']
45 | NTabs: typeof import('naive-ui')['NTabs']
46 | NTag: typeof import('naive-ui')['NTag']
47 | NText: typeof import('naive-ui')['NText']
48 | NUploadFileList: typeof import('naive-ui')['NUploadFileList']
49 | Osk: typeof import('./src/components/Osk.vue')['default']
50 | OtherOption: typeof import('./src/components/OtherOption.vue')['default']
51 | RouterLink: typeof import('vue-router')['RouterLink']
52 | RouterView: typeof import('vue-router')['RouterView']
53 | SessionShare: typeof import('./src/components/SessionShare/index.vue')['default']
54 | UserItem: typeof import('./src/components/SessionShare/widget/UserItem.vue')['default']
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/ui/src/utils/lunaBus.ts:
--------------------------------------------------------------------------------
1 | import type { Emitter } from 'mitt';
2 |
3 | import mitt from 'mitt';
4 |
5 | import type { LunaMessage, LunaMessageEvents } from '@/types/postmessage.type';
6 |
7 | import { LUNA_MESSAGE_TYPE } from '@/types/postmessage.type';
8 |
9 | // 获取所有事件类型
10 | export type LunaEventType = keyof LunaMessageEvents;
11 |
12 | // 创建事件-数据映射类型
13 | type EventPayloadMap = {
14 | [K in LunaEventType]: LunaMessageEvents[K]['data'] extends undefined
15 | ? void
16 | : LunaMessageEvents[K]['data'];
17 | };
18 |
19 | const allEventTypes = Object.keys(LUNA_MESSAGE_TYPE) as LunaEventType[];
20 |
21 | class LunaCommunicator {
22 | private mitt: Emitter;
23 | private lunaId: string = '';
24 | private targetOrigin: string = '*';
25 | private protocol: string = '';
26 |
27 | constructor() {
28 | this.mitt = mitt();
29 | this.setupMessageListener();
30 | }
31 |
32 | private setupMessageListener() {
33 | window.addEventListener('message', (event: MessageEvent) => {
34 | const message: LunaMessage = event.data;
35 | switch (message.name) {
36 | case LUNA_MESSAGE_TYPE.PING:
37 | this.lunaId = message.id;
38 | this.targetOrigin = event.origin;
39 | this.protocol = message.protocol;
40 | this.sendLuna(LUNA_MESSAGE_TYPE.PONG, '');
41 | console.log(
42 | `LunaCommunicator initialized with ID: ${this.lunaId}, Origin: ${this.targetOrigin}, Protocol: ${this.protocol}`,
43 | );
44 | break;
45 | default:
46 | // 处理其他类型的消息
47 | if (allEventTypes.includes(message.name as LunaEventType)) {
48 | const eventType = message.name as keyof T;
49 | const data = message as T[keyof T];
50 | this.mitt.emit(eventType, data);
51 | } else {
52 | console.warn(`Unhandled message type: ${message.name}`, message);
53 | }
54 | }
55 | });
56 | }
57 |
58 | // 发送消息到目标窗口
59 | public sendLuna(name: K, data: T[K]) {
60 | if (!this.lunaId || !this.targetOrigin) {
61 | console.warn('Target window not set');
62 | }
63 |
64 | window.parent.postMessage({ name, id: this.lunaId, data }, this.targetOrigin);
65 | }
66 |
67 | // 监听事件
68 | public onLuna(type: K, handler: (data: T[K]) => void) {
69 | this.mitt.on(type, handler);
70 | }
71 |
72 | // 移除监听器
73 | public offLuna(type: K, handler?: (data: T[K]) => void) {
74 | this.mitt.off(type, handler);
75 | }
76 |
77 | // 监听一次性事件
78 | public once(type: K, handler: (data: T[K]) => void) {
79 | const onceHandler = (data: T[K]) => {
80 | handler(data);
81 | this.offLuna(type, onceHandler);
82 | };
83 | this.onLuna(type, onceHandler);
84 | }
85 |
86 | // 销毁实例
87 | public destroy() {
88 | this.mitt.all.clear();
89 | }
90 |
91 | // 获取所有事件类型
92 | public getEventTypes(): Array {
93 | return Object.keys(this.mitt.all) as Array;
94 | }
95 | }
96 |
97 | export const lunaCommunicator = new LunaCommunicator();
98 |
--------------------------------------------------------------------------------
/pkg/tunnel/cache.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/jumpserver-dev/sdk-go/common"
7 |
8 | "lion/pkg/guacd"
9 | "lion/pkg/logger"
10 | )
11 |
12 | type Tunneler interface {
13 | UUID() string
14 | WriteAndFlush(p []byte) (int, error)
15 | ReadInstruction() (guacd.Instruction, error)
16 | Close() error
17 | }
18 |
19 | type GuaTunnelCache interface {
20 | Add(*Connection)
21 | Delete(*Connection)
22 | Get(string) *Connection
23 | RangeActiveSessionIds() []string
24 | RangeActiveUserIds() map[string]struct{}
25 | GetBySessionId(sid string) *Connection
26 | GetMonitorTunnelerBySessionId(sid string) Tunneler
27 | RemoveMonitorTunneler(sid string, monitorTunnel Tunneler)
28 |
29 | GetSessionEventChan(sid string) *EventChan
30 | BroadcastSessionEvent(sid string, event *Event)
31 | RecycleSessionEventChannel(sid string, eventChan *EventChan)
32 |
33 | GetActiveConnections() []*Connection
34 | }
35 |
36 | type SessionEvent interface {
37 | GetSessionEventChannel(sid string) *EventChan
38 | RecycleSessionEventChannel(sid string, eventChan *EventChan)
39 | BroadcastSessionEvent(sid string, event *Event)
40 | }
41 |
42 | var (
43 | _ GuaTunnelCache = (*GuaTunnelLocalCache)(nil)
44 | _ GuaTunnelCache = (*GuaTunnelRedisCache)(nil)
45 | )
46 |
47 | type GuaTunnelCacheManager struct {
48 | GuaTunnelCache
49 | }
50 |
51 | type Room struct {
52 | sid string
53 | eventChanMaps map[string]*EventChan
54 | lock sync.Mutex
55 | }
56 |
57 | func (r *Room) GetEventChannel(sid string) *EventChan {
58 | r.lock.Lock()
59 | defer r.lock.Unlock()
60 | eventChan := NewEventChan(sid)
61 | r.eventChanMaps[eventChan.id] = eventChan
62 | return eventChan
63 | }
64 |
65 | func (r *Room) RecycleEventChannel(eventChan *EventChan) {
66 | r.lock.Lock()
67 | defer r.lock.Unlock()
68 | delete(r.eventChanMaps, eventChan.id)
69 | eventChan.Close()
70 | }
71 |
72 | func (r *Room) BroadcastEvent(event *Event) {
73 | r.lock.Lock()
74 | defer r.lock.Unlock()
75 | for _, eventChan := range r.eventChanMaps {
76 | eventChan.SendEvent(event)
77 | }
78 | }
79 |
80 | type EventChan struct {
81 | id string
82 | sid string
83 | eventCh chan *Event
84 | }
85 |
86 | func (e *EventChan) GetEventChannel() chan *Event {
87 | return e.eventCh
88 | }
89 |
90 | func (e *EventChan) SendEvent(event *Event) {
91 | select {
92 | case e.eventCh <- event:
93 | default:
94 | logger.Errorf("EventChan %s for session %s is full", e.id, e.sid)
95 | }
96 | }
97 |
98 | func (e *EventChan) Close() {
99 | close(e.eventCh)
100 | }
101 |
102 | func NewEventChan(sid string) *EventChan {
103 | return &EventChan{
104 | id: common.UUID(),
105 | sid: sid,
106 | eventCh: make(chan *Event, 5),
107 | }
108 | }
109 |
110 | type Event struct {
111 | Type string
112 | Data []byte
113 | }
114 |
115 | const (
116 | ShareJoin = "share_join"
117 | ShareExit = "share_exit"
118 | ShareUsers = "share_users"
119 |
120 | ShareRemoveUser = "share_remove_user"
121 | ShareSessionPause = "share_session_pause"
122 | ShareSessionResume = "share_session_resume"
123 |
124 | PermExpiredEvent = "perm_expired"
125 | PermValidEvent = "perm_valid"
126 | )
127 |
--------------------------------------------------------------------------------
/ui/src/components/SessionShare/widget/UserItem.vue:
--------------------------------------------------------------------------------
1 |
60 |
61 |
62 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {{ username }}
77 |
78 |
85 | {{ primary ? t('PrimaryUser') : t('ShareUser') }}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
100 |
101 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {{ t('RemoveUser') }}
120 |
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/pkg/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "path/filepath"
7 | "runtime"
8 | "strings"
9 | "time"
10 | )
11 |
12 | type Level int8
13 |
14 | const (
15 | LevelDebug Level = iota
16 | LevelInfo
17 | LevelWarn
18 | LevelError
19 | LevelFatal
20 | LevelPanic
21 | )
22 |
23 | func (l Level) String() string {
24 | switch l {
25 | case LevelDebug:
26 | return "DEBUG"
27 | case LevelInfo:
28 | return "INFO"
29 | case LevelWarn:
30 | return "WARN"
31 | case LevelError:
32 | return "ERROR"
33 | case LevelFatal:
34 | return "FATAL"
35 | case LevelPanic:
36 | return "PANIC"
37 | }
38 | return ""
39 | }
40 |
41 | func ParseLevel(l string) Level {
42 | switch l {
43 | case "DEBUG":
44 | return LevelDebug
45 | case "INFO":
46 | return LevelInfo
47 | case "WARN":
48 | return LevelWarn
49 | case "ERROR":
50 | return LevelError
51 | case "FATAL":
52 | return LevelFatal
53 | case "PANIC":
54 | return LevelPanic
55 | }
56 | return LevelInfo
57 | }
58 |
59 | type Logger struct {
60 | newLogger *log.Logger
61 | level Level
62 | }
63 |
64 | func (l *Logger) Output(level Level, message string) {
65 | if l.level > level {
66 | return
67 | }
68 | pc, fileName, _, ok := runtime.Caller(2)
69 | if !ok {
70 | fileName = "???"
71 | }
72 | name := runtime.FuncForPC(pc).Name()
73 | name = strings.Split(filepath.Base(name), ".")[0]
74 | message = fmt.Sprintf("%s %s %s [%s] %s",
75 | time.Now().Format(logTimeFormat), name,
76 | filepath.Base(fileName), level, message)
77 | switch level {
78 | case LevelDebug:
79 | l.newLogger.Println(message)
80 | case LevelInfo:
81 | l.newLogger.Println(message)
82 | case LevelWarn:
83 | l.newLogger.Println(message)
84 | case LevelError:
85 | l.newLogger.Println(message)
86 | case LevelFatal:
87 | l.newLogger.Fatalln(message)
88 | case LevelPanic:
89 | l.newLogger.Panicln(message)
90 | }
91 | }
92 |
93 | func (l *Logger) Debug(v ...interface{}) {
94 | l.Output(LevelDebug, fmt.Sprint(v...))
95 | }
96 |
97 | func (l *Logger) Debugf(format string, v ...interface{}) {
98 | l.Output(LevelDebug, fmt.Sprintf(format, v...))
99 | }
100 |
101 | func (l *Logger) Info(v ...interface{}) {
102 | l.Output(LevelInfo, fmt.Sprint(v...))
103 | }
104 |
105 | func (l *Logger) Infof(format string, v ...interface{}) {
106 | l.Output(LevelInfo, fmt.Sprintf(format, v...))
107 | }
108 |
109 | func (l *Logger) Warn(v ...interface{}) {
110 | l.Output(LevelWarn, fmt.Sprint(v...))
111 | }
112 |
113 | func (l *Logger) Warnf(format string, v ...interface{}) {
114 | l.Output(LevelWarn, fmt.Sprintf(format, v...))
115 | }
116 |
117 | func (l *Logger) Error(v ...interface{}) {
118 | l.Output(LevelError, fmt.Sprint(v...))
119 | }
120 |
121 | func (l *Logger) Errorf(format string, v ...interface{}) {
122 | l.Output(LevelError, fmt.Sprintf(format, v...))
123 | }
124 |
125 | func (l *Logger) Fatal(v ...interface{}) {
126 | l.Output(LevelFatal, fmt.Sprint(v...))
127 | }
128 |
129 | func (l *Logger) Fatalf(format string, v ...interface{}) {
130 | l.Output(LevelFatal, fmt.Sprintf(format, v...))
131 | }
132 |
133 | func (l *Logger) Panic(v ...interface{}) {
134 | l.Output(LevelPanic, fmt.Sprint(v...))
135 | }
136 |
137 | func (l *Logger) Panicf(format string, v ...interface{}) {
138 | l.Output(LevelPanic, fmt.Sprintf(format, v...))
139 | }
140 |
--------------------------------------------------------------------------------
/pkg/tunnel/cache_local.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "sync"
5 |
6 | "lion/pkg/guacd"
7 | "lion/pkg/logger"
8 | )
9 |
10 | func NewLocalTunnelLocalCache() *GuaTunnelLocalCache {
11 | return &GuaTunnelLocalCache{
12 | Tunnels: make(map[string]*Connection),
13 | Rooms: make(map[string]*Room),
14 | }
15 | }
16 |
17 | type GuaTunnelLocalCache struct {
18 | sync.Mutex
19 | Tunnels map[string]*Connection
20 | roomLock sync.Mutex
21 | Rooms map[string]*Room
22 | }
23 |
24 | func (g *GuaTunnelLocalCache) Add(t *Connection) {
25 | g.Lock()
26 | defer g.Unlock()
27 | g.Tunnels[t.guacdTunnel.UUID()] = t
28 | }
29 |
30 | func (g *GuaTunnelLocalCache) Delete(t *Connection) {
31 | g.Lock()
32 | defer g.Unlock()
33 | delete(g.Tunnels, t.guacdTunnel.UUID())
34 | }
35 |
36 | func (g *GuaTunnelLocalCache) Get(tid string) *Connection {
37 | g.Lock()
38 | defer g.Unlock()
39 | return g.Tunnels[tid]
40 | }
41 |
42 | func (g *GuaTunnelLocalCache) RangeActiveSessionIds() []string {
43 | g.Lock()
44 | ret := make([]string, 0, len(g.Tunnels))
45 | for i := range g.Tunnels {
46 | ret = append(ret, g.Tunnels[i].Sess.ID)
47 | }
48 | g.Unlock()
49 | return ret
50 | }
51 |
52 | func (g *GuaTunnelLocalCache) RangeActiveUserIds() map[string]struct{} {
53 | g.Lock()
54 | ret := make(map[string]struct{})
55 | for i := range g.Tunnels {
56 | currentUser := g.Tunnels[i].Sess.User
57 | ret[currentUser.ID] = struct{}{}
58 | }
59 | g.Unlock()
60 | return ret
61 | }
62 |
63 | func (g *GuaTunnelLocalCache) GetBySessionId(sid string) *Connection {
64 | g.Lock()
65 | defer g.Unlock()
66 | for i := range g.Tunnels {
67 | if sid == g.Tunnels[i].Sess.ID {
68 | return g.Tunnels[i]
69 | }
70 | }
71 | return nil
72 | }
73 |
74 | func (g *GuaTunnelLocalCache) GetMonitorTunnelerBySessionId(sid string) Tunneler {
75 | if conn := g.GetBySessionId(sid); conn != nil {
76 | if guacdTunnel, err := conn.CloneMonitorTunnel(); err == nil {
77 | return guacdTunnel
78 | } else {
79 | logger.Error(err)
80 | }
81 | }
82 | return nil
83 | }
84 |
85 | func (g *GuaTunnelLocalCache) RemoveMonitorTunneler(sid string, monitorTunnel Tunneler) {
86 | if conn := g.GetBySessionId(sid); conn != nil {
87 | if tunnel, ok := monitorTunnel.(*guacd.Tunnel); ok {
88 | conn.unTraceMonitorTunnel(tunnel)
89 | }
90 | }
91 | }
92 |
93 | func (g *GuaTunnelLocalCache) GetSessionEventChan(sid string) *EventChan {
94 | g.roomLock.Lock()
95 | defer g.roomLock.Unlock()
96 | room := g.Rooms[sid]
97 | if room == nil {
98 | g.Rooms[sid] = &Room{
99 | sid: sid,
100 | eventChanMaps: make(map[string]*EventChan),
101 | }
102 | }
103 | return g.Rooms[sid].GetEventChannel(sid)
104 | }
105 |
106 | func (g *GuaTunnelLocalCache) BroadcastSessionEvent(sid string, event *Event) {
107 | g.roomLock.Lock()
108 | defer g.roomLock.Unlock()
109 | if room, ok := g.Rooms[sid]; ok {
110 | room.BroadcastEvent(event)
111 | }
112 | }
113 |
114 | func (g *GuaTunnelLocalCache) RecycleSessionEventChannel(sid string, eventChan *EventChan) {
115 | g.roomLock.Lock()
116 | defer g.roomLock.Unlock()
117 | if room, ok := g.Rooms[sid]; ok {
118 | room.RecycleEventChannel(eventChan)
119 | if len(room.eventChanMaps) == 0 {
120 | delete(g.Rooms, sid)
121 | }
122 | }
123 | }
124 |
125 | func (g *GuaTunnelLocalCache) GetActiveConnections() []*Connection {
126 | g.Lock()
127 | defer g.Unlock()
128 | ret := make([]*Connection, 0, len(g.Tunnels))
129 | for i := range g.Tunnels {
130 | ret = append(ret, g.Tunnels[i])
131 | }
132 | return ret
133 | }
134 |
--------------------------------------------------------------------------------
/pkg/session/display.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "github.com/spf13/viper"
5 |
6 | "lion/pkg/guacd"
7 | )
8 |
9 | type valueType string
10 |
11 | const (
12 | Boolean valueType = "boolean"
13 | String valueType = "string"
14 | Integer valueType = "integer"
15 | )
16 |
17 | type DisplayParameter struct {
18 | Key string
19 | DefaultValue string
20 | valueType valueType
21 | }
22 |
23 | var (
24 | colorDepth = DisplayParameter{Key: guacd.RDPColorDepth, DefaultValue: "24", valueType: Integer}
25 | dpi = DisplayParameter{Key: guacd.RDPDpi, DefaultValue: "", valueType: Integer}
26 | disableAudio = DisplayParameter{Key: guacd.RDPDisableAudio, DefaultValue: "", valueType: Boolean}
27 | enableWallpaper = DisplayParameter{Key: guacd.RDPEnableWallpaper, DefaultValue: "", valueType: Boolean}
28 | enableTheming = DisplayParameter{Key: guacd.RDPEnableTheming, DefaultValue: "", valueType: Boolean}
29 | enableFontSmoothing = DisplayParameter{Key: guacd.RDPEnableFontSmoothing, DefaultValue: "", valueType: Boolean}
30 | enableFullWindowDrag = DisplayParameter{Key: guacd.RDPEnableFullWindowDrag, DefaultValue: "", valueType: Boolean}
31 | enableDesktopComposition = DisplayParameter{Key: guacd.RDPEnableDesktopComposition, DefaultValue: "", valueType: Boolean}
32 | enableMenuAnimations = DisplayParameter{Key: guacd.RDPEnableMenuAnimations, DefaultValue: "", valueType: Boolean}
33 | disableBitmapCaching = DisplayParameter{Key: guacd.RDPDisableBitmapCaching, DefaultValue: "", valueType: Boolean}
34 | disableOffscreenCaching = DisplayParameter{Key: guacd.RDPDisableOffscreenCaching, DefaultValue: "", valueType: Boolean}
35 | vncCursorRender = DisplayParameter{Key: guacd.VNCCursor, DefaultValue: "", valueType: String}
36 | enableConsoleAudio = DisplayParameter{Key: guacd.RDPConsoleAudio, DefaultValue: "", valueType: Boolean}
37 | enableAudioInput = DisplayParameter{Key: guacd.RDPEnableAudioInput, DefaultValue: "", valueType: Boolean}
38 | )
39 |
40 | type Display struct {
41 | data map[string]DisplayParameter
42 | }
43 |
44 | func (d Display) GetDisplayParams() map[string]string {
45 | res := make(map[string]string)
46 | for envKey, displayParam := range d.data {
47 | res[displayParam.Key] = displayParam.DefaultValue
48 | if value := viper.GetString(envKey); value != "" {
49 | switch displayParam.valueType {
50 | case Boolean:
51 | booleanValue := viper.GetBool(envKey)
52 | res[displayParam.Key] = ConvertBoolToString(booleanValue)
53 | default:
54 | res[displayParam.Key] = value
55 | }
56 | }
57 | }
58 | return res
59 | }
60 |
61 | var RDPDisplay = Display{data: map[string]DisplayParameter{
62 | "JUMPSERVER_COLOR_DEPTH": colorDepth,
63 | "JUMPSERVER_DPI": dpi,
64 | "JUMPSERVER_DISABLE_AUDIO": disableAudio,
65 | "JUMPSERVER_ENABLE_WALLPAPER": enableWallpaper,
66 | "JUMPSERVER_ENABLE_THEMING": enableTheming,
67 | "JUMPSERVER_ENABLE_FONT_SMOOTHING": enableFontSmoothing,
68 | "JUMPSERVER_ENABLE_FULL_WINDOW_DRAG": enableFullWindowDrag,
69 | "JUMPSERVER_ENABLE_DESKTOP_COMPOSITION": enableDesktopComposition,
70 | "JUMPSERVER_ENABLE_MENU_ANIMATIONS": enableMenuAnimations,
71 | "JUMPSERVER_DISABLE_BITMAP_CACHING": disableBitmapCaching,
72 | "JUMPSERVER_DISABLE_OFFSCREEN_CACHING": disableOffscreenCaching,
73 | "JUMPSERVER_ENABLE_CONSOLE_AUDIO": enableConsoleAudio,
74 | "JUMPSERVER_ENABLE_AUDIO_INPUT": enableAudioInput,
75 | }}
76 |
77 | var VNCDisplay = Display{data: map[string]DisplayParameter{
78 | "JUMPSERVER_COLOR_DEPTH": colorDepth,
79 | "JUMPSERVER_VNC_CURSOR_RENDER": vncCursorRender,
80 | }}
81 |
82 | var RDPBuiltIn = map[string]string{
83 | guacd.RDPDisableGlyphCaching: BoolTrue,
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/gateway/domain.go:
--------------------------------------------------------------------------------
1 | package gateway
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net"
7 | "strconv"
8 | "sync"
9 | "time"
10 |
11 | gossh "golang.org/x/crypto/ssh"
12 |
13 | "lion/pkg/logger"
14 |
15 | "github.com/jumpserver-dev/sdk-go/common"
16 | "github.com/jumpserver-dev/sdk-go/model"
17 | )
18 |
19 | var ErrNoAvailable = errors.New("no available domain")
20 |
21 | const (
22 | miniTimeout = 15 * time.Second
23 | )
24 |
25 | type DomainGateway struct {
26 | DstAddr string // 10.0.0.1:3389
27 |
28 | sshClient *gossh.Client
29 | SelectedGateway *model.Gateway
30 |
31 | ln net.Listener
32 |
33 | once sync.Once
34 | }
35 |
36 | func (d *DomainGateway) run() {
37 | defer d.closeOnce()
38 | for {
39 | con, err := d.ln.Accept()
40 | if err != nil {
41 | break
42 | }
43 | logger.Infof("Accept new conn by gateway %s ", d.SelectedGateway.Name)
44 | go d.handlerConn(con)
45 | }
46 | logger.Infof("Stop proxy by gateway %s", d.SelectedGateway.Name)
47 | }
48 |
49 | func (d *DomainGateway) handlerConn(srcCon net.Conn) {
50 | defer srcCon.Close()
51 | dstCon, err := d.sshClient.Dial("tcp", d.DstAddr)
52 | if err != nil {
53 | logger.Errorf("Failed gateway dial %s: %s ",
54 | d.DstAddr, err.Error())
55 | return
56 | }
57 | defer dstCon.Close()
58 | go func() {
59 | _, _ = io.Copy(dstCon, srcCon)
60 | _ = dstCon.Close()
61 | }()
62 | _, _ = io.Copy(srcCon, dstCon)
63 | logger.Infof("Gateway end proxy %s", d.DstAddr)
64 | }
65 |
66 | func (d *DomainGateway) Start() (err error) {
67 | if !d.getAvailableGateway() {
68 | return ErrNoAvailable
69 | }
70 | localIP := common.CurrentLocalIP()
71 | d.ln, err = net.Listen("tcp", net.JoinHostPort(localIP, "0"))
72 | if err != nil {
73 | _ = d.sshClient.Close()
74 | return err
75 | }
76 | go d.run()
77 | return nil
78 | }
79 |
80 | func (d *DomainGateway) GetListenAddr() *net.TCPAddr {
81 | return d.ln.Addr().(*net.TCPAddr)
82 | }
83 |
84 | func (d *DomainGateway) getAvailableGateway() bool {
85 | if d.SelectedGateway != nil {
86 | sshClient, err := d.createGatewaySSHClient(d.SelectedGateway)
87 | if err != nil {
88 | logger.Errorf("Dial select gateway %s err: %s ", d.SelectedGateway.Name, err)
89 | return false
90 | }
91 | logger.Debugf("Dial select gateway %s success", d.SelectedGateway.Name)
92 | d.sshClient = sshClient
93 | return true
94 | }
95 | return false
96 | }
97 | func (d *DomainGateway) createGatewaySSHClient(gateway *model.Gateway) (*gossh.Client, error) {
98 | auths := make([]gossh.AuthMethod, 0, 3)
99 | loginAccount := gateway.Account
100 | if loginAccount.IsSSHKey() {
101 | if signer, err1 := gossh.ParsePrivateKey([]byte(loginAccount.Secret)); err1 == nil {
102 | auths = append(auths, gossh.PublicKeys(signer))
103 | } else {
104 | logger.Errorf("Domain gateway Parse private key error: %s", err1)
105 | }
106 | } else {
107 | auths = append(auths, gossh.Password(loginAccount.Secret))
108 | auths = append(auths, gossh.KeyboardInteractive(func(user, instruction string,
109 | questions []string, echos []bool) (answers []string, err error) {
110 | return []string{loginAccount.Secret}, nil
111 | }))
112 | }
113 | sshConfig := gossh.ClientConfig{
114 | User: loginAccount.Username,
115 | Auth: auths,
116 | HostKeyCallback: gossh.InsecureIgnoreHostKey(),
117 | Timeout: miniTimeout,
118 | }
119 | port := gateway.Protocols.GetProtocolPort("ssh")
120 | addr := net.JoinHostPort(gateway.Address, strconv.Itoa(port))
121 | return gossh.Dial("tcp", addr, &sshConfig)
122 | }
123 | func (d *DomainGateway) Stop() {
124 | d.closeOnce()
125 | }
126 |
127 | func (d *DomainGateway) closeOnce() {
128 | d.once.Do(func() {
129 | _ = d.ln.Close()
130 | _ = d.sshClient.Close()
131 | })
132 | }
133 |
--------------------------------------------------------------------------------
/ui/src/utils/config.ts:
--------------------------------------------------------------------------------
1 | import { useCookies } from 'vue3-cookies';
2 |
3 | const PORT = document.location.port ? `:${document.location.port}` : '';
4 | const SCHEME = document.location.protocol === 'https:' ? 'wss' : 'ws';
5 |
6 | export const BASE_WS_URL = SCHEME + '://' + document.location.hostname + PORT;
7 | export const BASE_URL = document.location.protocol + '//' + document.location.hostname + PORT;
8 |
9 | const { cookies } = useCookies();
10 |
11 | const storeLang = cookies.get('lang');
12 | const cookieLang = cookies.get('django_language');
13 |
14 | const browserLang = navigator.language || (navigator.languages && navigator.languages[0]) || 'en';
15 |
16 | export const LanguageCode = cookieLang || storeLang || browserLang || 'en';
17 | export const ThemeCode = localStorage.getItem('themeType') || 'default';
18 |
19 | export const MaxTimeout = 30 * 1000;
20 |
21 | export const MAX_TRANSFER_SIZE = 1024 * 1024 * 500;
22 |
23 | export const defaultTheme = {
24 | background: '#121414',
25 | foreground: '#ffffff',
26 | black: '#2e3436',
27 | red: '#cc0000',
28 | green: '#4e9a06',
29 | yellow: '#c4a000',
30 | blue: '#3465a4',
31 | magenta: '#75507b',
32 | cyan: '#06989a',
33 | white: '#d3d7cf',
34 | brightBlack: '#555753',
35 | brightRed: '#ef2929',
36 | brightGreen: '#8ae234',
37 | brightYellow: '#fce94f',
38 | brightBlue: '#729fcf',
39 | brightMagenta: '#ad7fa8',
40 | brightCyan: '#34e2e2',
41 | brightWhite: '#eeeeec',
42 | };
43 |
44 | // 图片类型的
45 | export const FILE_SUFFIX_IMAGE = [
46 | 'jpg',
47 | 'jpeg',
48 | 'png',
49 | 'gif',
50 | 'bmp',
51 | 'webp',
52 | 'ico',
53 | 'svg',
54 | 'heic',
55 | 'heif',
56 | ];
57 | // 音频类型的
58 | export const FILE_SUFFIX_AUDIO = [
59 | 'mp3',
60 | 'wav',
61 | 'ogg',
62 | 'm4a',
63 | 'aac',
64 | 'flac',
65 | 'm4b',
66 | 'm4p',
67 | 'm4b',
68 | 'm4p',
69 | 'm4b',
70 | 'm4p',
71 | ];
72 | // 视频类型的
73 | export const FILE_SUFFIX_VIDEO = [
74 | 'mp4',
75 | 'avi',
76 | 'mov',
77 | 'wmv',
78 | 'flv',
79 | 'mpeg',
80 | 'mpg',
81 | 'm4v',
82 | 'mkv',
83 | 'webm',
84 | 'vob',
85 | 'm2ts',
86 | 'mts',
87 | 'ts',
88 | 'm2t',
89 | 'm2ts',
90 | 'mts',
91 | 'ts',
92 | 'm2t',
93 | 'm2ts',
94 | ];
95 | // 压缩包类型的
96 | export const FILE_SUFFIX_COMPRESSION = [
97 | 'zip',
98 | 'rar',
99 | '7z',
100 | 'tar',
101 | 'gz',
102 | 'bz2',
103 | 'iso',
104 | 'dmg',
105 | 'pkg',
106 | 'deb',
107 | 'rpm',
108 | 'msi',
109 | 'exe',
110 | 'app',
111 | 'dmg',
112 | 'pkg',
113 | 'deb',
114 | 'rpm',
115 | 'msi',
116 | 'exe',
117 | 'app',
118 | ];
119 | // 文档类型的
120 | export const FILE_SUFFIX_DOCUMENT = [
121 | 'doc',
122 | 'docx',
123 | 'xls',
124 | 'xlsx',
125 | 'ppt',
126 | 'pptx',
127 | 'pdf',
128 | 'txt',
129 | 'md',
130 | 'csv',
131 | 'json',
132 | 'xml',
133 | 'yaml',
134 | 'yml',
135 | 'toml',
136 | 'ini',
137 | 'conf',
138 | 'cfg',
139 | 'config',
140 | 'log',
141 | 'yml',
142 | 'toml',
143 | 'ini',
144 | 'conf',
145 | 'cfg',
146 | 'config',
147 | 'log',
148 | 'lock',
149 | 'sock',
150 | ];
151 | // 代码类型的
152 | export const FILE_SUFFIX_CODE = [
153 | 'js',
154 | 'ts',
155 | 'py',
156 | 'java',
157 | 'c',
158 | 'cpp',
159 | 'h',
160 | 'hpp',
161 | 'css',
162 | 'html',
163 | 'php',
164 | 'ruby',
165 | 'go',
166 | 'rust',
167 | 'swift',
168 | 'kotlin',
169 | 'dart',
170 | 'scala',
171 | 'haskell',
172 | 'erlang',
173 | 'elixir',
174 | 'ocaml',
175 | 'erlang',
176 | 'elixir',
177 | 'ocaml',
178 | 'erlang',
179 | 'elixir',
180 | 'ocaml',
181 | 'erlang',
182 | 'elixir',
183 | 'ocaml',
184 | ];
185 | // 安装包类型的
186 | export const FILE_SUFFIX_INSTALL = [
187 | 'deb',
188 | 'rpm',
189 | 'msi',
190 | 'exe',
191 | 'app',
192 | 'dmg',
193 | 'pkg',
194 | 'deb',
195 | 'rpm',
196 | 'msi',
197 | 'exe',
198 | 'app',
199 | ];
200 | // 数据库类型
201 | export const FILE_SUFFIX_DATABASE = [
202 | 'mysql',
203 | 'oracle',
204 | 'postgresql',
205 | 'sqlserver',
206 | 'mongodb',
207 | 'redis',
208 | 'memcached',
209 | 'sqlite',
210 | 'mariadb',
211 | ];
212 |
--------------------------------------------------------------------------------
/ui/src/components/ClipBoardText.vue:
--------------------------------------------------------------------------------
1 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
85 |
86 |
87 |
96 |
97 |
98 |
99 |
100 |
117 |
118 |
119 |
120 |
127 |
135 |
136 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/ui/src/components/OtherOption.vue:
--------------------------------------------------------------------------------
1 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
120 | {{ props.fitPercentage }}%
123 |
124 |
125 |
126 |
127 |
128 |
146 |
147 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module lion
2 |
3 | go 1.24.6
4 |
5 | require (
6 | github.com/gin-contrib/sessions v1.0.1
7 | github.com/gin-gonic/gin v1.9.1
8 | github.com/go-redis/redis/v8 v8.11.5
9 | github.com/gorilla/websocket v1.5.3
10 | github.com/jumpserver-dev/sdk-go v0.0.0-20251119062736-f025e5399a93
11 | github.com/spf13/viper v1.21.0
12 | golang.org/x/crypto v0.45.0
13 | gopkg.in/natefinch/lumberjack.v2 v2.2.1
14 | )
15 |
16 | require (
17 | github.com/Azure/azure-pipeline-go v0.2.3 // indirect
18 | github.com/Azure/azure-storage-blob-go v0.15.0 // indirect
19 | github.com/LeeEirc/httpsig v1.2.1 // indirect
20 | github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect
21 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
22 | github.com/aws/aws-sdk-go v1.44.306 // indirect
23 | github.com/bytedance/sonic v1.11.9 // indirect
24 | github.com/bytedance/sonic/loader v0.1.1 // indirect
25 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
26 | github.com/cloudwego/base64x v0.1.4 // indirect
27 | github.com/cloudwego/iasm v0.2.0 // indirect
28 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
29 | github.com/dlclark/regexp2 v1.11.4 // indirect
30 | github.com/elastic/elastic-transport-go/v8 v8.6.0 // indirect
31 | github.com/elastic/go-elasticsearch/v6 v6.8.10 // indirect
32 | github.com/elastic/go-elasticsearch/v8 v8.14.0 // indirect
33 | github.com/fsnotify/fsnotify v1.9.0 // indirect
34 | github.com/gabriel-vasile/mimetype v1.4.4 // indirect
35 | github.com/gin-contrib/sse v0.1.0 // indirect
36 | github.com/go-logr/logr v1.4.1 // indirect
37 | github.com/go-logr/stdr v1.2.2 // indirect
38 | github.com/go-ole/go-ole v1.2.6 // indirect
39 | github.com/go-playground/locales v0.14.1 // indirect
40 | github.com/go-playground/universal-translator v0.18.1 // indirect
41 | github.com/go-playground/validator/v10 v10.22.0 // indirect
42 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
43 | github.com/goccy/go-json v0.10.3 // indirect
44 | github.com/google/uuid v1.6.0 // indirect
45 | github.com/gorilla/context v1.1.2 // indirect
46 | github.com/gorilla/securecookie v1.1.2 // indirect
47 | github.com/gorilla/sessions v1.3.0 // indirect
48 | github.com/huaweicloud/huaweicloud-sdk-go-obs v3.25.4+incompatible // indirect
49 | github.com/influxdata/influxdb-client-go/v2 v2.14.0 // indirect
50 | github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
51 | github.com/jmespath/go-jmespath v0.4.0 // indirect
52 | github.com/json-iterator/go v1.1.12 // indirect
53 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect
54 | github.com/leodido/go-urn v1.4.0 // indirect
55 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
56 | github.com/mattn/go-ieproxy v0.0.1 // indirect
57 | github.com/mattn/go-isatty v0.0.20 // indirect
58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
59 | github.com/modern-go/reflect2 v1.0.2 // indirect
60 | github.com/oapi-codegen/runtime v1.1.1 // indirect
61 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect
62 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
63 | github.com/sagikazarmark/locafero v0.11.0 // indirect
64 | github.com/shirou/gopsutil/v3 v3.24.5 // indirect
65 | github.com/shoenig/go-m1cpu v0.1.6 // indirect
66 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
67 | github.com/spf13/afero v1.15.0 // indirect
68 | github.com/spf13/cast v1.10.0 // indirect
69 | github.com/spf13/pflag v1.0.10 // indirect
70 | github.com/subosito/gotenv v1.6.0 // indirect
71 | github.com/tklauser/go-sysconf v0.3.12 // indirect
72 | github.com/tklauser/numcpus v0.6.1 // indirect
73 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
74 | github.com/ugorji/go/codec v1.2.12 // indirect
75 | github.com/yusufpapurcu/wmi v1.2.4 // indirect
76 | go.opentelemetry.io/otel v1.24.0 // indirect
77 | go.opentelemetry.io/otel/metric v1.24.0 // indirect
78 | go.opentelemetry.io/otel/trace v1.24.0 // indirect
79 | go.yaml.in/yaml/v3 v3.0.4 // indirect
80 | golang.org/x/arch v0.8.0 // indirect
81 | golang.org/x/net v0.47.0 // indirect
82 | golang.org/x/sys v0.38.0 // indirect
83 | golang.org/x/text v0.31.0 // indirect
84 | golang.org/x/time v0.12.0 // indirect
85 | google.golang.org/protobuf v1.34.2 // indirect
86 | gopkg.in/yaml.v3 v3.0.1 // indirect
87 | )
88 |
--------------------------------------------------------------------------------
/ui/src/utils/guacamole_helper.ts:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | import Guacamole from 'guacamole-common-js';
3 |
4 | const supportImages: any[] = [];
5 | const pendingTests: any[] = [];
6 | const testImages: any = {
7 | /**
8 | * Test JPEG image, encoded as base64.
9 | */
10 | 'image/jpeg':
11 | '/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH' +
12 | 'BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQME' +
13 | 'BAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU' +
14 | 'FBQUFBQUFBQUFBQUFBT/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAA' +
15 | 'AAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAA' +
16 | 'AAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AVMH/2Q==',
17 |
18 | /**
19 | * Test PNG image, encoded as base64.
20 | */
21 | 'image/png':
22 | 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvI' +
23 | 'AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==',
24 |
25 | /**
26 | * Test WebP image, encoded as base64.
27 | */
28 | 'image/webp': 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==',
29 | }; // 测试单个图片格式
30 | async function testImageFormat(mimeType: string, base64Data: any): Promise {
31 | return new Promise((resolve) => {
32 | const image = new Image();
33 |
34 | image.onload = () => {
35 | // Image format is supported if successfully decoded with correct dimensions
36 | const isSupported = image.width === 1 && image.height === 1;
37 | resolve(isSupported);
38 | };
39 |
40 | image.onerror = () => {
41 | console.debug(`Format ${mimeType} not supported`);
42 | resolve(false);
43 | };
44 |
45 | // Set source to trigger loading
46 | image.src = `data:${mimeType};base64,${base64Data}`;
47 | });
48 | }
49 |
50 | // Use Object.entries for better iteration over key-value pairs
51 | Object.entries(testImages).forEach(([mimeType, base64Data]) => {
52 | const imageTest = new Promise((resolve) => {
53 | const image = new Image();
54 |
55 | // Set up handlers before setting src to avoid race conditions
56 | image.onload = () => {
57 | // Image format is supported if successfully decoded with correct dimensions
58 | if (image.width === 1 && image.height === 1) {
59 | supportImages.push(mimeType);
60 | }
61 | resolve();
62 | };
63 |
64 | // Handle errors separately for better debugging
65 | image.onerror = () => {
66 | console.debug(`Format ${mimeType} not supported`);
67 | resolve(); // Still resolve to continue testing other formats
68 | };
69 |
70 | // Set source to trigger loading
71 | image.src = `data:${mimeType};base64,${base64Data}`;
72 | });
73 |
74 | pendingTests.push(imageTest);
75 | });
76 |
77 | export async function getSupportedImages(): Promise {
78 | // 清空之前的结果
79 | supportImages.length = 0;
80 |
81 | // 并行测试所有图片格式
82 | const testPromises = Object.entries(testImages).map(async ([mimeType, base64Data]) => {
83 | const isSupported = await testImageFormat(mimeType, base64Data);
84 | if (isSupported) {
85 | supportImages.push(mimeType);
86 | }
87 | return { mimeType, isSupported };
88 | });
89 |
90 | // 等待所有测试完成
91 | await Promise.all(testPromises);
92 |
93 | return [...supportImages]; // 返回副本
94 | }
95 | export async function getSupportedGuacVideos(): Promise {
96 | return Guacamole.VideoPlayer.getSupportedTypes();
97 | }
98 |
99 | export async function getSupportedGuacAudios(): Promise {
100 | return Guacamole.AudioPlayer.getSupportedTypes();
101 | }
102 |
103 | export async function getSupportedGuacMimeTypes(): Promise {
104 | const supportImages = await getSupportedImages();
105 | const supportVideos = await getSupportedGuacVideos();
106 | const supportAudios = await getSupportedGuacAudios();
107 | let connectString = '';
108 | supportImages.forEach((mimeType) => {
109 | connectString += '&GUAC_IMAGE=' + encodeURIComponent(mimeType);
110 | });
111 | supportVideos.forEach((mimeType) => {
112 | connectString += '&GUAC_AUDIO=' + encodeURIComponent(mimeType);
113 | });
114 | supportAudios.forEach((mimeType) => {
115 | connectString += '&GUAC_VIDEO=' + encodeURIComponent(mimeType);
116 | });
117 | return connectString;
118 | }
119 |
--------------------------------------------------------------------------------
/pkg/session/session.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "lion/pkg/guacd"
5 |
6 | "github.com/jumpserver-dev/sdk-go/common"
7 | "github.com/jumpserver-dev/sdk-go/model"
8 | )
9 |
10 | type TunnelSession struct {
11 | ID string `json:"id"`
12 | Protocol string `json:"protocol"`
13 | Created common.UTCTime `json:"-"`
14 | Asset *model.Asset `json:"asset"`
15 | Account *model.Account `json:"-"`
16 | User *model.User `json:"user"`
17 | Platform *model.Platform `json:"platform"`
18 | RemoteApp *model.Applet `json:"remote_app"`
19 | Permission *model.Permission `json:"permission"`
20 | Gateway *model.Gateway `json:"-"`
21 | TerminalConfig *model.TerminalConfig `json:"-"`
22 | ExpireInfo model.ExpireInfo `json:"expire_info"`
23 | ActionPerm *ActionPermission `json:"action_permission"`
24 | DisplayAccount *model.Account `json:"system_user"`
25 |
26 | AppletOpts *model.AppletOption `json:"-"`
27 | AuthInfo *model.ConnectToken `json:"-"`
28 |
29 | VirtualAppOpts *model.VirtualAppContainer `json:"-"`
30 |
31 | ConnectedCallback func() error `json:"-"`
32 | ConnectedFailedCallback func(err error) error `json:"-"`
33 | DisConnectedCallback func() error `json:"-"`
34 |
35 | ReleaseAppletAccount func() error `json:"-"`
36 |
37 | ModelSession *model.Session `json:"-"`
38 | }
39 |
40 | const (
41 | vnc = "vnc"
42 | rdp = "rdp"
43 | )
44 |
45 | func (s TunnelSession) GuaConfiguration() guacd.Configuration {
46 | if s.AppletOpts != nil {
47 | return s.configurationRemoteAppRDP()
48 | }
49 | if s.VirtualAppOpts != nil {
50 | return s.configurationVirtualApp()
51 | }
52 | switch s.Protocol {
53 | case vnc:
54 | return s.configurationVNC()
55 | default:
56 | return s.configurationRDP()
57 | }
58 | }
59 |
60 | func (s TunnelSession) configurationVNC() guacd.Configuration {
61 | conf := VNCConfiguration{
62 | SessionId: s.ID,
63 | Created: s.Created,
64 | User: s.User,
65 | Asset: s.Asset,
66 | Account: s.Account,
67 | Platform: s.Platform,
68 | TerminalConfig: s.TerminalConfig,
69 | ActionsPerm: s.ActionPerm,
70 | }
71 | return conf.GetGuacdConfiguration()
72 | }
73 |
74 | func (s TunnelSession) configurationRDP() guacd.Configuration {
75 | if s.AppletOpts != nil {
76 | return s.configurationRemoteAppRDP()
77 | }
78 | rdpConf := RDPConfiguration{
79 | SessionId: s.ID,
80 | Created: s.Created,
81 | User: s.User,
82 | Asset: s.Asset,
83 | Account: s.Account,
84 | Platform: s.Platform,
85 | TerminalConfig: s.TerminalConfig,
86 | ActionsPerm: s.ActionPerm,
87 | }
88 | return rdpConf.GetGuacdConfiguration()
89 | }
90 |
91 | func (s TunnelSession) configurationRemoteAppRDP() guacd.Configuration {
92 | appletOpt := s.AppletOpts
93 | rdpConf := RDPConfiguration{
94 | SessionId: s.ID,
95 | Created: s.Created,
96 | User: s.User,
97 | Asset: &appletOpt.Host,
98 | Account: &appletOpt.Account,
99 | Platform: s.Platform,
100 | TerminalConfig: s.TerminalConfig,
101 | ActionsPerm: s.ActionPerm,
102 | }
103 | conf := rdpConf.GetGuacdConfiguration()
104 | remoteAPP := appletOpt.RemoteAppOption
105 | // 设置 remote app 参数
106 | {
107 | conf.SetParameter(guacd.RDPRemoteApp, remoteAPP.Name)
108 | conf.SetParameter(guacd.RDPRemoteAppDir, "")
109 | conf.SetParameter(guacd.RDPRemoteAppArgs, remoteAPP.CmdLine)
110 | }
111 | return conf
112 | }
113 |
114 | func (s TunnelSession) configurationVirtualApp() guacd.Configuration {
115 | vncConf := VirtualAppConfiguration{
116 | SessionId: s.ID,
117 | Created: s.Created,
118 | User: s.User,
119 | VirtualAppOpt: s.VirtualAppOpts,
120 | TerminalConfig: s.TerminalConfig,
121 | ActionsPerm: s.ActionPerm,
122 | }
123 | return vncConf.GetGuacdConfiguration()
124 | }
125 |
126 | const (
127 | SecurityAny = "any"
128 | SecurityNla = "nla"
129 | SecurityNlaExt = "nla-ext"
130 | SecurityTls = "tls"
131 | SecurityVmConnect = "vmconnect"
132 | SecurityRdp = "rdp"
133 | )
134 |
135 | func ValidateSecurityValue(security string) bool {
136 | switch security {
137 | case SecurityAny,
138 | SecurityNla,
139 | SecurityNlaExt,
140 | SecurityTls,
141 | SecurityVmConnect,
142 | SecurityRdp:
143 | return true
144 | }
145 | return false
146 | }
147 |
--------------------------------------------------------------------------------
/ui/src/types/postmessage.type.ts:
--------------------------------------------------------------------------------
1 | export interface LunaMessageEvents {
2 | [LUNA_MESSAGE_TYPE.PING]: {
3 | data: LunaMessage;
4 | };
5 | [LUNA_MESSAGE_TYPE.PONG]: {
6 | data: string;
7 | };
8 | [LUNA_MESSAGE_TYPE.CMD]: {
9 | data: LunaMessage;
10 | };
11 | [LUNA_MESSAGE_TYPE.FOCUS]: {
12 | data: LunaMessage;
13 | };
14 | [LUNA_MESSAGE_TYPE.OPEN]: {
15 | data: LunaMessage;
16 | };
17 | [LUNA_MESSAGE_TYPE.FILE]: {
18 | data: LunaMessage;
19 | };
20 | [LUNA_MESSAGE_TYPE.CREATE_FILE_CONNECT_TOKEN]: {
21 | data: LunaMessage;
22 | };
23 | [LUNA_MESSAGE_TYPE.SESSION_INFO]: {
24 | data: LunaMessage;
25 | };
26 | [LUNA_MESSAGE_TYPE.SHARE_USER]: {
27 | data: LunaMessage;
28 | };
29 | [LUNA_MESSAGE_TYPE.SHARE_USER_REMOVE]: {
30 | data: LunaMessage;
31 | };
32 | [LUNA_MESSAGE_TYPE.SHARE_USER_ADD]: {
33 | data: LunaMessage;
34 | };
35 | [LUNA_MESSAGE_TYPE.TERMINAL_THEME_CHANGE]: {
36 | data: LunaMessage;
37 | };
38 |
39 | [LUNA_MESSAGE_TYPE.SHARE_CODE_REQUEST]: {
40 | data: ShareUserRequest;
41 | };
42 | [LUNA_MESSAGE_TYPE.SHARE_CODE_RESPONSE]: {
43 | data: string;
44 | };
45 | [LUNA_MESSAGE_TYPE.CLOSE]: {
46 | data: string;
47 | };
48 | [LUNA_MESSAGE_TYPE.CONNECT]: {
49 | data: LunaMessage;
50 | };
51 | [LUNA_MESSAGE_TYPE.TERMINAL_ERROR]: {
52 | data: LunaMessage;
53 | };
54 | [LUNA_MESSAGE_TYPE.MESSAGE_NOTIFY]: {
55 | data: LunaMessage;
56 | };
57 | [LUNA_MESSAGE_TYPE.KEYEVENT]: {
58 | data: string;
59 | };
60 | [LUNA_MESSAGE_TYPE.TERMINAL_CONTENT]: {
61 | data: LunaMessage;
62 | };
63 | [LUNA_MESSAGE_TYPE.TERMINAL_CONTENT_RESPONSE]: {
64 | data: TerminalContentRepsonse;
65 | };
66 | [LUNA_MESSAGE_TYPE.CLICK]: {
67 | data: string;
68 | };
69 | [LUNA_MESSAGE_TYPE.FILE_MANAGE_EXPIRED]: {
70 | data: string;
71 | };
72 | [LUNA_MESSAGE_TYPE.CHANGE_MAIN_THEME]: {
73 | data: LunaMessage;
74 | };
75 | [LUNA_MESSAGE_TYPE.MOUSE_EVENT]: {
76 | data: string;
77 | };
78 | [LUNA_MESSAGE_TYPE.KEYBOARDEVENT]: {
79 | data: string;
80 | };
81 | [LUNA_MESSAGE_TYPE.INPUT_ACTIVE]: {
82 | data: string;
83 | };
84 | }
85 |
86 | export interface LunaMessage {
87 | id: string;
88 | name: string;
89 | origin: string;
90 | protocol: string;
91 | data: string | object | null;
92 | theme?: string;
93 | user_meta?: string;
94 | }
95 |
96 | export interface ShareUserRequest {
97 | name: string;
98 | data: {
99 | sessionId: string;
100 | requestData: {
101 | expired_time: number;
102 | action_permission: string;
103 | action_perm: string;
104 | users: string[];
105 | };
106 | };
107 | }
108 |
109 | export interface ShareUserResponse {
110 | shareId: string;
111 | code: string;
112 | terminalId: string;
113 | }
114 |
115 | export interface TerminalSessionInfo {
116 | session: TerminalSession;
117 | permission: TerminalPermission;
118 | backspaceAsCtrlH: boolean;
119 | ctrlCAsCtrlZ: boolean;
120 | themeName: string;
121 | }
122 |
123 | export interface TerminalSession {
124 | id: string;
125 | user: string;
126 |
127 | userId: string;
128 | }
129 |
130 | export interface TerminalPermission {
131 | actions: string[];
132 | }
133 |
134 | export interface TerminalContentRepsonse {
135 | terminalId: string;
136 | content: string;
137 | sessionId: string;
138 | }
139 |
140 | export enum LUNA_MESSAGE_TYPE {
141 | PING = 'PING',
142 | PONG = 'PONG',
143 | CMD = 'CMD',
144 | FOCUS = 'FOCUS',
145 | OPEN = 'OPEN',
146 | FILE = 'FILE',
147 | CREATE_FILE_CONNECT_TOKEN = 'CREATE_FILE_CONNECT_TOKEN',
148 |
149 | SESSION_INFO = 'SESSION_INFO',
150 |
151 | SHARE_USER = 'SHARE_USER',
152 | SHARE_USER_REMOVE = 'SHARE_USER_REMOVE',
153 | SHARE_USER_ADD = 'SHARE_USER_ADD',
154 | SHARE_USER_LEAVE = 'SHARE_USER_LEAVE',
155 |
156 | TERMINAL_THEME_CHANGE = 'TERMINAL_THEME_CHANGE',
157 |
158 | SHARE_CODE_REQUEST = 'SHARE_CODE_REQUEST',
159 | SHARE_CODE_RESPONSE = 'SHARE_CODE_RESPONSE',
160 |
161 | CLOSE = 'CLOSE',
162 | CONNECT = 'CONNECT',
163 | TERMINAL_ERROR = 'TERMINAL_ERROR',
164 | MESSAGE_NOTIFY = 'MESSAGE_NOTIFY',
165 | KEYEVENT = 'KEYEVENT',
166 |
167 | TERMINAL_CONTENT = 'TERMINAL_CONTENT_REQUEST',
168 | TERMINAL_CONTENT_RESPONSE = 'TERMINAL_CONTENT_RESPONSE',
169 | CLICK = 'CLICK',
170 | CHANGE_MAIN_THEME = 'CHANGE_MAIN_THEME',
171 | FILE_MANAGE_EXPIRED = 'FILE_MANAGE_EXPIRED',
172 |
173 | MOUSE_EVENT = 'MOUSEEVENT',
174 |
175 | KEYBOARDEVENT = 'KEYBOARDEVENT',
176 |
177 | INPUT_ACTIVE = 'INPUT_ACTIVE',
178 | }
179 |
--------------------------------------------------------------------------------
/pkg/guacd/status.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type GuacamoleStatus struct {
8 | HttpCode int
9 | WsCode int
10 | GuaCode int
11 | }
12 |
13 | func (g GuacamoleStatus) String() string {
14 | return fmt.Sprintf("%d - %d - %d", g.HttpCode,
15 | g.WsCode, g.GuaCode)
16 | }
17 |
18 | func (g GuacamoleStatus) Error() string {
19 | return g.String()
20 | }
21 |
22 | var (
23 | /**
24 | * The operation succeeded.
25 | */
26 | StatusSuccess GuacamoleStatus = GuacamoleStatus{200, 1000, 0x0000}
27 |
28 | /**
29 | * The requested operation is unsupported.
30 | */
31 | StatusUnsupported GuacamoleStatus = GuacamoleStatus{501, 1011, 0x0100}
32 |
33 | /**
34 | * The operation could not be performed due to an internal failure.
35 | */
36 | StatusServerError GuacamoleStatus = GuacamoleStatus{500, 1011, 0x0200}
37 |
38 | /**
39 | * The operation could not be performed as the server is busy.
40 | */
41 | StatusServerBusy GuacamoleStatus = GuacamoleStatus{503, 1008, 0x0201}
42 |
43 | /**
44 | * The operation could not be performed because the upstream server is not
45 | * responding.
46 | */
47 | StatusUpstreamTimeout GuacamoleStatus = GuacamoleStatus{504, 1011, 0x0202}
48 |
49 | /**
50 | * The operation was unsuccessful due to an error or otherwise unexpected
51 | * condition of the upstream server.
52 | */
53 | StatusUpstreamError GuacamoleStatus = GuacamoleStatus{502, 1011, 0x0203}
54 |
55 | /**
56 | * The operation could not be performed as the requested resource does not
57 | * exist.
58 | */
59 | StatusResourceNotFound GuacamoleStatus = GuacamoleStatus{404, 1002, 0x0204}
60 |
61 | /**
62 | * The operation could not be performed as the requested resource is already
63 | * in use.
64 | */
65 | StatusResourceConflict GuacamoleStatus = GuacamoleStatus{409, 1008, 0x0205}
66 |
67 | /**
68 | * The operation could not be performed as the requested resource is now
69 | * closed.
70 | */
71 | StatusResourceClosed GuacamoleStatus = GuacamoleStatus{404, 1002, 0x0206}
72 |
73 | /**
74 | * The operation could not be performed because the upstream server does
75 | * not appear to exist.
76 | */
77 | StatusUpstreamNotFound GuacamoleStatus = GuacamoleStatus{502, 1011, 0x0207}
78 |
79 | /**
80 | * The operation could not be performed because the upstream server is not
81 | * available to service the request.
82 | */
83 | StatusUpstreamUnavailable GuacamoleStatus = GuacamoleStatus{502, 1011, 0x0208}
84 |
85 | /**
86 | * The session within the upstream server has ended because it conflicted
87 | * with another session.
88 | */
89 | StatusSessionConflict GuacamoleStatus = GuacamoleStatus{409, 1008, 0x0209}
90 |
91 | /**
92 | * The session within the upstream server has ended because it appeared to
93 | * be inactive.
94 | */
95 | StatusSessionTimeout GuacamoleStatus = GuacamoleStatus{408, 1002, 0x020A}
96 |
97 | /**
98 | * The session within the upstream server has been forcibly terminated.
99 | */
100 | StatusSessionClosed GuacamoleStatus = GuacamoleStatus{404, 1002, 0x020B}
101 |
102 | /**
103 | * The operation could not be performed because bad parameters were given.
104 | */
105 | StatusClientBadRequest GuacamoleStatus = GuacamoleStatus{400, 1002, 0x0300}
106 |
107 | /**
108 | * Permission was denied to perform the operation, as the user is not yet
109 | * authorized (not yet logged in, for example). As HTTP 401 has implications
110 | * for HTTP-specific authorization schemes, this status continues to map to
111 | * HTTP 403 ("Forbidden"). To do otherwise would risk unintended effects.
112 | */
113 | StatusClientUnauthorized GuacamoleStatus = GuacamoleStatus{403, 1008, 0x0301}
114 |
115 | /**
116 | * Permission was denied to perform the operation, and this operation will
117 | * not be granted even if the user is authorized.
118 | */
119 | StatusClientForbidden GuacamoleStatus = GuacamoleStatus{403, 1008, 0x0303}
120 |
121 | /**
122 | * The client took too long to respond.
123 | */
124 | StatusClientTimeout GuacamoleStatus = GuacamoleStatus{408, 1002, 0x0308}
125 |
126 | /**
127 | * The client sent too much data.
128 | */
129 | StatusClientOverRun GuacamoleStatus = GuacamoleStatus{413, 1009, 0x030D}
130 |
131 | /**
132 | * The client sent data of an unsupported or unexpected type.
133 | */
134 | StatusClientBadType GuacamoleStatus = GuacamoleStatus{415, 1003, 0x030F}
135 |
136 | /**
137 | * The operation failed because the current client is already using too
138 | * many resources.
139 | */
140 | StatusClientTooMany GuacamoleStatus = GuacamoleStatus{429, 1008, 0x031D}
141 | )
142 |
--------------------------------------------------------------------------------
/ui/src/hooks/useColor.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue';
2 |
3 | interface HSL {
4 | h: number;
5 | s: number;
6 | l: number;
7 | }
8 |
9 | const mainThemeColorMap = new Map(
10 | Object.entries({
11 | default: '#483D3D',
12 | deepBlue: '#1A212C',
13 | darkGary: '#303237',
14 | }),
15 | );
16 |
17 | const currentMainColoc = ref('#303237');
18 |
19 | export const useColor = () => {
20 | const setCurrentMainColor = (color: string) => {
21 | const themeColor = mainThemeColorMap.get(color);
22 |
23 | if (themeColor) {
24 | currentMainColoc.value = themeColor;
25 | } else {
26 | currentMainColoc.value = '#483D3D';
27 | }
28 | };
29 |
30 | /**
31 | * 将十六进制颜色转换为HSL颜色
32 | * @param hex 十六进制颜色
33 | * @returns HSL颜色
34 | */
35 | const hexToHSL = (hex: string): HSL => {
36 | let hexValue = hex.replace(/^#/, '');
37 |
38 | if (hexValue.length === 3) {
39 | hexValue = hexValue
40 | .split('')
41 | .map((char) => char + char)
42 | .join('');
43 | }
44 |
45 | // 解析RGB值
46 | const r = Number.parseInt(hexValue.substring(0, 2), 16) / 255;
47 | const g = Number.parseInt(hexValue.substring(2, 4), 16) / 255;
48 | const b = Number.parseInt(hexValue.substring(4, 6), 16) / 255;
49 |
50 | // 计算HSL值
51 | const max = Math.max(r, g, b);
52 | const min = Math.min(r, g, b);
53 | let h = 0;
54 | let s = 0;
55 | const l = (max + min) / 2;
56 |
57 | if (max !== min) {
58 | const d = max - min;
59 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
60 |
61 | switch (max) {
62 | case r:
63 | h = (g - b) / d + (g < b ? 6 : 0);
64 | break;
65 | case g:
66 | h = (b - r) / d + 2;
67 | break;
68 | case b:
69 | h = (r - g) / d + 4;
70 | break;
71 | }
72 |
73 | h /= 6;
74 | }
75 |
76 | // 转换为标准HSL格式
77 | return {
78 | h: Math.round(h * 360),
79 | s: Math.round(s * 100),
80 | l: Math.round(l * 100),
81 | };
82 | };
83 |
84 | /**
85 | * 将HSL颜色转换为十六进制颜色
86 | * @param h 色相
87 | * @param s 饱和度
88 | * @param l 亮度
89 | * @returns 十六进制颜色
90 | */
91 | const hslToHex = (h: number, s: number, l: number) => {
92 | h /= 360;
93 | s /= 100;
94 | l /= 100;
95 |
96 | let r, g, b;
97 |
98 | if (s === 0) {
99 | // 如果饱和度为0,则为灰色
100 | r = g = b = l;
101 | } else {
102 | const hue2rgb = (p: number, q: number, t: number): number => {
103 | if (t < 0) t += 1;
104 | if (t > 1) t -= 1;
105 | if (t < 1 / 6) return p + (q - p) * 6 * t;
106 | if (t < 1 / 2) return q;
107 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
108 | return p;
109 | };
110 |
111 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
112 | const p = 2 * l - q;
113 |
114 | r = hue2rgb(p, q, h + 1 / 3);
115 | g = hue2rgb(p, q, h);
116 | b = hue2rgb(p, q, h - 1 / 3);
117 | }
118 |
119 | // 转换为十六进制
120 | const toHex = (x: number): string => {
121 | const hex = Math.round(x * 255).toString(16);
122 | return hex.length === 1 ? `0${hex}` : hex;
123 | };
124 |
125 | return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
126 | };
127 |
128 | /**
129 | * 将颜色转换为rgba格式
130 | * @param alphaValue 透明度值
131 | * @param color 颜色
132 | * @returns rgba格式颜色
133 | */
134 | const alpha = (alphaValue: number, color?: string) => {
135 | // 如果没有提供颜色,使用当前主题颜色
136 | const actualColor = color || currentMainColoc.value;
137 | // 确保透明度值在0-1之间
138 | const alpha = Math.max(0, Math.min(1, alphaValue));
139 |
140 | // 移除#号并处理缩写形式
141 | let hex = actualColor.replace(/^#/, '');
142 |
143 | if (hex.length === 3) {
144 | hex = hex
145 | .split('')
146 | .map((char) => char + char)
147 | .join('');
148 | }
149 |
150 | // 解析RGB值
151 | const r = Number.parseInt(hex.substring(0, 2), 16);
152 | const g = Number.parseInt(hex.substring(2, 4), 16);
153 | const b = Number.parseInt(hex.substring(4, 6), 16);
154 |
155 | // 返回rgba格式
156 | return `rgba(${r}, ${g}, ${b}, ${alpha})`;
157 | };
158 |
159 | /**
160 | * 将颜色变亮
161 | * @param amount
162 | * @param color
163 | * @param alphaValue
164 | * @returns
165 | */
166 | const lighten = (amount: number, color?: string, alphaValue?: number) => {
167 | const actualColor = color || currentMainColoc.value;
168 | const hsl = hexToHSL(actualColor);
169 | const hexColor = hslToHex(hsl.h, hsl.s, Math.min(100, hsl.l + amount));
170 |
171 | if (alphaValue !== undefined) {
172 | return alpha(alphaValue, hexColor);
173 | }
174 |
175 | return hexColor;
176 | };
177 |
178 | /**
179 | * 将颜色变暗
180 | * @param amount
181 | * @param color
182 | * @param alphaValue
183 | * @returns
184 | */
185 | const darken = (amount: number, color?: string, alphaValue?: number) => {
186 | const actualColor = color || currentMainColoc.value;
187 | const hsl = hexToHSL(actualColor);
188 | const hexColor = hslToHex(hsl.h, hsl.s, Math.max(0, hsl.l - amount));
189 |
190 | // 如果提供了透明度参数,应用透明度
191 | if (alphaValue !== undefined) {
192 | return alpha(alphaValue, hexColor);
193 | }
194 |
195 | return hexColor;
196 | };
197 |
198 | return {
199 | darken,
200 | lighten,
201 | alpha,
202 | setCurrentMainColor,
203 | currentMainColor: currentMainColoc,
204 | };
205 | };
206 |
--------------------------------------------------------------------------------
/pkg/tunnel/stream_output.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "context"
5 | "encoding/base64"
6 | "fmt"
7 | "net/http"
8 | "strconv"
9 | "sync"
10 |
11 | "lion/pkg/guacd"
12 | "lion/pkg/logger"
13 | "lion/pkg/proxy"
14 |
15 | "github.com/jumpserver-dev/sdk-go/model"
16 | )
17 |
18 | type OutputStreamInterceptingFilter struct {
19 | sync.Mutex
20 | tunnel *Connection
21 | streams map[string]OutStreamResource
22 | acknowledgeBlobs bool
23 | }
24 |
25 | func (filter *OutputStreamInterceptingFilter) Filter(unfilteredInstruction *guacd.Instruction) *guacd.Instruction {
26 |
27 | switch unfilteredInstruction.Opcode {
28 | case guacd.InstructionStreamingBlob:
29 | // Intercept "blob" instructions for in-progress streams
30 | //if (instruction.getOpcode().equals("blob"))
31 | //return handleBlob(instruction);
32 | return filter.handleBlob(unfilteredInstruction)
33 | case guacd.InstructionStreamingEnd:
34 | //// Intercept "end" instructions for in-progress streams
35 | //if (instruction.getOpcode().equals("end")) {
36 | // handleEnd(instruction);
37 | // return instruction;
38 | //}
39 | filter.handleEnd(unfilteredInstruction)
40 | return unfilteredInstruction
41 | case guacd.InstructionClientSync:
42 | // Monitor "sync" instructions to ensure the client does not starve
43 | // from lack of graphical updates
44 | //if (instruction.getOpcode().equals("sync")) {
45 | // handleSync(instruction);
46 | // return instruction;
47 | //}
48 | filter.handleSync(unfilteredInstruction)
49 | return unfilteredInstruction
50 | }
51 |
52 | // Pass instruction through untouched
53 | //return instruction
54 | return unfilteredInstruction
55 | }
56 |
57 | func (filter *OutputStreamInterceptingFilter) handleBlob(unfilteredInstruction *guacd.Instruction) *guacd.Instruction {
58 | // Verify all required arguments are present
59 | args := unfilteredInstruction.Args
60 | if len(args) < 2 {
61 | return unfilteredInstruction
62 | }
63 | index := args[0]
64 | if stream, ok := filter.streams[index]; ok {
65 | // Decode blob
66 | data := args[1]
67 | blob, err := base64.StdEncoding.DecodeString(data)
68 | if err != nil {
69 | logger.Errorf("Base64 decode blob err: %+v", err)
70 | return nil
71 | }
72 |
73 | _, err = stream.writer.Write(blob)
74 | if err != nil {
75 | stream.err = err
76 | logger.Errorf("OutputStream filter stream %s write err: %+v", stream.streamIndex, err)
77 | if err = filter.sendAck(index, "FAIL", guacd.StatusServerError); err != nil {
78 | logger.Errorf("OutputStream filter sendAck err: %+v", err)
79 | }
80 | return nil
81 | }
82 | if err1 := stream.recorder.RecordWrite(stream.ftpLog, blob); err1 != nil {
83 | logger.Errorf("OutputStream filter stream %s record write err: %+v", stream.streamIndex, err1)
84 | }
85 | if !filter.acknowledgeBlobs {
86 | filter.acknowledgeBlobs = true
87 | ins := guacd.NewInstruction(guacd.InstructionStreamingBlob, index, "")
88 | return &ins
89 | }
90 |
91 | err = filter.sendAck(index, "Ok", guacd.StatusSuccess)
92 | if err != nil {
93 | logger.Errorf("OutputStream filter sendAck err: %+v", err)
94 | }
95 | return nil
96 | }
97 | return unfilteredInstruction
98 | }
99 |
100 | func (filter *OutputStreamInterceptingFilter) handleSync(unfilteredInstruction *guacd.Instruction) {
101 | filter.acknowledgeBlobs = false
102 | }
103 |
104 | func (filter *OutputStreamInterceptingFilter) handleEnd(unfilteredInstruction *guacd.Instruction) {
105 | // Verify all required arguments are present
106 | //List args = instruction.getArgs();
107 | //if (args.size() < 1)
108 | //return;
109 |
110 | // Terminate stream
111 | //closeInterceptedStream(args.get(0));
112 | args := unfilteredInstruction.Args
113 | if len(args) < 1 {
114 | return
115 | }
116 | filter.closeInterceptedStream(args[0])
117 | }
118 |
119 | func (filter *OutputStreamInterceptingFilter) sendAck(index, msg string, status guacd.GuacamoleStatus) error {
120 |
121 | // Error "ack" instructions implicitly close the stream
122 | //if (status != GuacamoleStatus.SUCCESS)
123 | // closeInterceptedStream(index);
124 | //
125 | //sendInstruction(new GuacamoleInstruction("ack", index, message,
126 | // Integer.toString(status.getGuacamoleStatusCode())));
127 | if status.HttpCode != guacd.StatusSuccess.HttpCode {
128 | filter.closeInterceptedStream(index)
129 | }
130 | return filter.tunnel.WriteTunnelMessage(guacd.NewInstruction(
131 | guacd.InstructionStreamingAck, index, msg,
132 | strconv.Itoa(status.GuaCode)))
133 |
134 | }
135 |
136 | func (filter *OutputStreamInterceptingFilter) closeInterceptedStream(index string) {
137 | filter.Lock()
138 | defer filter.Unlock()
139 | if outStream, ok := filter.streams[index]; ok {
140 | close(outStream.done)
141 | }
142 | delete(filter.streams, index)
143 | }
144 |
145 | func (filter *OutputStreamInterceptingFilter) addOutStream(out OutStreamResource) {
146 | filter.Lock()
147 | defer filter.Unlock()
148 | err := filter.sendAck(out.streamIndex, "OK", guacd.StatusSuccess)
149 | if err != nil {
150 | logger.Errorf("OutputStream filter sendAck index %s err: %+v", out.streamIndex, err)
151 | out.err = err
152 | close(out.done)
153 | return
154 | }
155 | filter.streams[out.streamIndex] = out
156 | }
157 |
158 | // 下载文件的对象
159 |
160 | type OutStreamResource struct {
161 | streamIndex string
162 | mediaType string // application/octet-stream
163 | writer http.ResponseWriter
164 | done chan struct{}
165 | err error
166 | ctx context.Context
167 |
168 | ftpLog *model.FTPLog
169 | recorder *proxy.FTPFileRecorder
170 | }
171 |
172 | func (r *OutStreamResource) Wait() error {
173 | select {
174 | case <-r.done:
175 | case <-r.ctx.Done():
176 | return fmt.Errorf("closed request %s", r.streamIndex)
177 | }
178 | return r.err
179 | }
180 |
--------------------------------------------------------------------------------
/pkg/guacd/tunnel.go:
--------------------------------------------------------------------------------
1 | package guacd
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "net"
9 | "strconv"
10 | "strings"
11 | "time"
12 | )
13 |
14 | const (
15 | defaultSocketTimeOut = time.Second * 15
16 |
17 | Version = "VERSION_1_3_0"
18 | )
19 |
20 | func NewTunnel(address string, config Configuration, info ClientInformation) (tunnel *Tunnel, err error) {
21 | var conn net.Conn
22 | conn, err = net.DialTimeout("tcp", address, defaultSocketTimeOut)
23 | if err != nil {
24 | return nil, err
25 | }
26 | defer func() {
27 | // 如果err 则直接关闭 连接
28 | if err != nil {
29 | _ = conn.Close()
30 | log.Printf("关闭连接防止conn未关闭,%s\n", err.Error())
31 | }
32 | }()
33 | tunnel = &Tunnel{}
34 | tunnel.conn = conn
35 | tunnel.rw = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
36 | tunnel.Config = config
37 |
38 | selectArg := config.ConnectionID
39 | if selectArg == "" {
40 | selectArg = config.Protocol
41 | }
42 |
43 | // Send requested protocol or connection ID
44 | if err = tunnel.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil {
45 | return nil, err
46 | }
47 | var connectArgs Instruction
48 |
49 | // Wait for server args
50 | connectArgs, err = tunnel.expect("args")
51 | if err != nil {
52 | return nil, err
53 | }
54 | // Build args list off provided names and config
55 | connectArgsValues := make([]string, len(connectArgs.Args))
56 | for i := range connectArgs.Args {
57 | argName := connectArgs.Args[i]
58 | if strings.HasPrefix(argName, "VERSION") {
59 | connectArgsValues[i] = Version
60 | continue
61 | }
62 | connectArgsValues[i] = config.GetParameter(argName)
63 | }
64 |
65 | // send size
66 | width := info.OptimalScreenWidth
67 | height := info.OptimalScreenHeight
68 | dpi := info.OptimalResolution
69 | if err = tunnel.WriteInstructionAndFlush(NewInstruction(
70 | "size",
71 | strconv.Itoa(width),
72 | strconv.Itoa(height),
73 | strconv.Itoa(dpi))); err != nil {
74 | return nil, err
75 | }
76 |
77 | // Send supported audio formats
78 | supportedAudios := info.AudioMimetypes
79 |
80 | if err = tunnel.WriteInstructionAndFlush(NewInstruction(
81 | "audio", supportedAudios...)); err != nil {
82 | return nil, err
83 | }
84 |
85 | // Send supported video formats
86 | supportedVideos := info.VideoMimetypes
87 |
88 | if err = tunnel.WriteInstructionAndFlush(NewInstruction(
89 | "video", supportedVideos...)); err != nil {
90 | return nil, err
91 | }
92 |
93 | // Send supported image formats
94 | supportedImages := info.ImageMimetypes
95 | if err = tunnel.WriteInstructionAndFlush(NewInstruction(
96 | "image", supportedImages...)); err != nil {
97 | return nil, err
98 | }
99 |
100 | // Send client timezone, if supported and available
101 | clientTimezone := info.Timezone
102 |
103 | if err = tunnel.WriteInstructionAndFlush(NewInstruction(
104 | "timezone", clientTimezone)); err != nil {
105 | return nil, err
106 | }
107 |
108 | // Send args
109 | if err = tunnel.WriteInstructionAndFlush(NewInstruction(
110 | "connect", connectArgsValues...)); err != nil {
111 | return nil, err
112 | }
113 |
114 | // Wait for ready, store ID
115 | ready, err := tunnel.expect("ready")
116 | if err != nil {
117 | return nil, err
118 | }
119 |
120 | if len(ready.Args) == 0 {
121 | err = errors.New("no connection id received")
122 | return nil, err
123 | }
124 |
125 | tunnel.uuid = ready.Args[0]
126 | tunnel.IsOpen = true
127 | tunnel.Config = config
128 | return tunnel, nil
129 | }
130 |
131 | type Tunnel struct {
132 | rw *bufio.ReadWriter
133 | conn net.Conn
134 |
135 | uuid string
136 | Config Configuration
137 | IsOpen bool
138 | }
139 |
140 | func (t *Tunnel) UUID() string {
141 | return t.uuid
142 | }
143 |
144 | func (t *Tunnel) WriteInstructionAndFlush(instruction Instruction) (err error) {
145 | _, err = t.WriteAndFlush([]byte(instruction.String()))
146 | return
147 | }
148 |
149 | func (t *Tunnel) WriteInstruction(instruction Instruction) (err error) {
150 | _, err = t.Write([]byte(instruction.String()))
151 | return
152 | }
153 |
154 | func (t *Tunnel) WriteAndFlush(p []byte) (int, error) {
155 | nw, err := t.rw.Write(p)
156 | if err != nil {
157 | return nw, err
158 | }
159 | err = t.rw.Flush()
160 | if err != nil {
161 | return nw, err
162 | }
163 | return nw, nil
164 | }
165 |
166 | func (t *Tunnel) Write(p []byte) (int, error) {
167 | return t.rw.Write(p)
168 | }
169 |
170 | func (t *Tunnel) Flush() error {
171 | return t.rw.Flush()
172 | }
173 |
174 | func (t *Tunnel) ReadInstruction() (instruction Instruction, err error) {
175 | var ret string
176 | for {
177 | if err = t.conn.SetReadDeadline(time.Now().Add(defaultSocketTimeOut)); err != nil {
178 | return Instruction{}, err
179 | }
180 | msg, err := t.rw.ReadString(ByteSemicolonDelimiter)
181 | if err != nil {
182 | return Instruction{}, err
183 | }
184 | ret += msg
185 | if retInstruction, err := ParseInstructionString(ret); err == nil {
186 | return retInstruction, nil
187 | } else {
188 | log.Printf("%s %v\n", ret, err.Error())
189 | }
190 | }
191 | }
192 |
193 | func (t *Tunnel) Read() ([]byte, error) {
194 | var (
195 | ins Instruction
196 | err error
197 | )
198 | if ins, err = t.ReadInstruction(); err != nil {
199 | return nil, err
200 | }
201 | return []byte(ins.String()), nil
202 | }
203 |
204 | func (t *Tunnel) expect(opcode string) (instruction Instruction, err error) {
205 | instruction, err = t.ReadInstruction()
206 | if err != nil {
207 | return instruction, err
208 | }
209 |
210 | if opcode != instruction.Opcode {
211 | msg := fmt.Sprintf(`expected "%s" instruction but instead received "%s"`, opcode, instruction.Opcode)
212 | return instruction, errors.New(msg)
213 | }
214 | return instruction, nil
215 | }
216 |
217 | func (t *Tunnel) Close() error {
218 | t.IsOpen = false
219 | return t.conn.Close()
220 | }
221 |
--------------------------------------------------------------------------------
/ui/src/styles/keyboard.css:
--------------------------------------------------------------------------------
1 | .guac-keyboard {
2 | display: inline-block;
3 | width: 100%;
4 |
5 | margin: 0;
6 | padding: 0;
7 | cursor: default;
8 |
9 | text-align: left;
10 | vertical-align: middle;
11 | }
12 |
13 | .guac-keyboard,
14 | .guac-keyboard * {
15 | overflow: hidden;
16 | white-space: nowrap;
17 | }
18 |
19 | .guac-keyboard .guac-keyboard-key-container {
20 | display: inline-block;
21 | margin: 0.05em;
22 | position: relative;
23 | }
24 |
25 | .guac-keyboard .guac-keyboard-key {
26 | position: absolute;
27 | left: 0;
28 | right: 0;
29 | top: 0;
30 | bottom: 0;
31 |
32 | background: #444;
33 |
34 | border: 0.125em solid #666;
35 | -moz-border-radius: 0.25em;
36 | -webkit-border-radius: 0.25em;
37 | -khtml-border-radius: 0.25em;
38 | border-radius: 0.25em;
39 |
40 | color: white;
41 | font-size: 40%;
42 | font-weight: lighter;
43 | text-align: center;
44 | white-space: pre;
45 |
46 | text-shadow:
47 | 1px 1px 0 rgba(0, 0, 0, 0.25),
48 | 1px -1px 0 rgba(0, 0, 0, 0.25),
49 | -1px 1px 0 rgba(0, 0, 0, 0.25),
50 | -1px -1px 0 rgba(0, 0, 0, 0.25);
51 | }
52 |
53 | .guac-keyboard .guac-keyboard-key:hover {
54 | cursor: pointer;
55 | }
56 |
57 | .guac-keyboard .guac-keyboard-key.highlight {
58 | background: #666;
59 | border-color: #666;
60 | }
61 |
62 | /* Align some keys to the left */
63 | .guac-keyboard .guac-keyboard-key-caps,
64 | .guac-keyboard .guac-keyboard-key-enter,
65 | .guac-keyboard .guac-keyboard-key-tab,
66 | .guac-keyboard .guac-keyboard-key-lalt,
67 | .guac-keyboard .guac-keyboard-key-ralt,
68 | .guac-keyboard .guac-keyboard-key-alt-gr,
69 | .guac-keyboard .guac-keyboard-key-lctrl,
70 | .guac-keyboard .guac-keyboard-key-rctrl,
71 | .guac-keyboard .guac-keyboard-key-lshift,
72 | .guac-keyboard .guac-keyboard-key-rshift {
73 | text-align: left;
74 | padding-left: 0.75em;
75 | }
76 |
77 | /* Active shift */
78 | .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-rshift,
79 | .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-lshift,
80 |
81 | /* Active ctrl */
82 | .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-rctrl,
83 | .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-lctrl,
84 |
85 | /* Active alt */
86 | .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-ralt,
87 | .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-lalt,
88 |
89 | /* Active alt-gr */
90 | .guac-keyboard.guac-keyboard-modifier-alt-gr .guac-keyboard-key-alt-gr,
91 |
92 | /* Active caps */
93 | .guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key-caps,
94 |
95 | /* Active super */
96 | .guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-super,
97 |
98 | /* Active latin */
99 | .guac-keyboard.guac-keyboard-modifier-lat .guac-keyboard-key-latin {
100 | background: #882;
101 | border-color: #dd4;
102 | }
103 |
104 | .guac-keyboard .guac-keyboard-key.guac-keyboard-pressed {
105 | background: #822;
106 | border-color: #d44;
107 | }
108 |
109 | .guac-keyboard .guac-keyboard-group {
110 | line-height: 0;
111 | }
112 |
113 | .guac-keyboard .guac-keyboard-group.guac-keyboard-alpha,
114 | .guac-keyboard .guac-keyboard-group.guac-keyboard-movement,
115 | .guac-keyboard .guac-keyboard-group.guac-keyboard-function,
116 | .guac-keyboard .guac-keyboard-group.guac-keyboard-virtual {
117 | display: inline-block;
118 | text-align: center;
119 | vertical-align: top;
120 | }
121 |
122 | .guac-keyboard .guac-keyboard-group.guac-keyboard-main,
123 | .guac-keyboard .guac-keyboard-group.guac-keyboard-top {
124 | /* IE10 */
125 | display: -ms-flexbox;
126 | -ms-flex-align: stretch;
127 | -ms-flex-direction: row;
128 |
129 | /* Ancient Mozilla */
130 | display: -moz-box;
131 | -moz-box-align: stretch;
132 | -moz-box-orient: horizontal;
133 |
134 | /* Ancient WebKit */
135 | display: -webkit-box;
136 | -webkit-box-align: stretch;
137 | -webkit-box-orient: horizontal;
138 |
139 | /* Old WebKit */
140 | display: -webkit-flex;
141 | -webkit-align-items: stretch;
142 | -webkit-flex-direction: row;
143 |
144 | /* W3C */
145 | display: flex;
146 | align-items: stretch;
147 | flex-direction: row;
148 | }
149 |
150 | .guac-keyboard .guac-keyboard-group.guac-keyboard-movement,
151 | .guac-keyboard .guac-keyboard-group.guac-keyboard-virtual {
152 | -ms-flex: 1 1 auto;
153 | -moz-box-flex: 1;
154 | -webkit-box-flex: 1;
155 | -webkit-flex: 1 1 auto;
156 | flex: 1 1 auto;
157 | }
158 |
159 | .guac-keyboard .guac-keyboard-gap {
160 | display: inline-block;
161 | }
162 |
163 | /* Hide keycaps requiring modifiers which are NOT currently active. */
164 | .guac-keyboard:not(.guac-keyboard-modifier-caps)
165 | .guac-keyboard-cap.guac-keyboard-requires-caps,
166 |
167 | .guac-keyboard:not(.guac-keyboard-modifier-shift)
168 | .guac-keyboard-cap.guac-keyboard-requires-shift,
169 |
170 | .guac-keyboard:not(.guac-keyboard-modifier-alt-gr)
171 | .guac-keyboard-cap.guac-keyboard-requires-alt-gr,
172 |
173 | .guac-keyboard:not(.guac-keyboard-modifier-lat)
174 | .guac-keyboard-cap.guac-keyboard-requires-lat,
175 |
176 | /* Hide keycaps NOT requiring modifiers which ARE currently active, where that
177 | modifier is used to determine which cap is displayed for the current key. */
178 | .guac-keyboard.guac-keyboard-modifier-shift
179 | .guac-keyboard-key.guac-keyboard-uses-shift
180 | .guac-keyboard-cap:not(.guac-keyboard-requires-shift),
181 |
182 | .guac-keyboard.guac-keyboard-modifier-caps
183 | .guac-keyboard-key.guac-keyboard-uses-caps
184 | .guac-keyboard-cap:not(.guac-keyboard-requires-caps),
185 |
186 | .guac-keyboard.guac-keyboard-modifier-alt-gr
187 | .guac-keyboard-key.guac-keyboard-uses-alt-gr
188 | .guac-keyboard-cap:not(.guac-keyboard-requires-alt-gr),
189 |
190 | .guac-keyboard.guac-keyboard-modifier-lat
191 | .guac-keyboard-key.guac-keyboard-uses-lat
192 | .guac-keyboard-cap:not(.guac-keyboard-requires-lat) {
193 | display: none;
194 | }
195 |
196 | /* Fade out keys which do not use AltGr if AltGr is active */
197 | .guac-keyboard.guac-keyboard-modifier-alt-gr
198 | .guac-keyboard-key:not(.guac-keyboard-uses-alt-gr):not(.guac-keyboard-key-alt-gr) {
199 | opacity: 0.5;
200 | }
201 |
--------------------------------------------------------------------------------
/pkg/tunnel/monitor.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "strings"
7 | "sync"
8 | "sync/atomic"
9 |
10 | "github.com/gorilla/websocket"
11 |
12 | "lion/pkg/guacd"
13 | "lion/pkg/logger"
14 |
15 | "github.com/jumpserver-dev/sdk-go/model"
16 | )
17 |
18 | type MonitorCon struct {
19 | Id string
20 | guacdTunnel Tunneler
21 |
22 | ws *websocket.Conn
23 |
24 | wsLock sync.Mutex
25 | guacdLock sync.Mutex
26 |
27 | Service *GuacamoleTunnelServer
28 | User *model.User
29 | Meta *MetaShareUserMessage
30 |
31 | lockedStatus atomic.Bool
32 | }
33 |
34 | func (m *MonitorCon) SendWsMessage(msg guacd.Instruction) error {
35 | return m.writeWsMessage([]byte(msg.String()))
36 | }
37 |
38 | func (m *MonitorCon) writeWsMessage(p []byte) error {
39 | m.wsLock.Lock()
40 | defer m.wsLock.Unlock()
41 | return m.ws.WriteMessage(websocket.TextMessage, p)
42 | }
43 |
44 | func (m *MonitorCon) WriteTunnelMessage(msg guacd.Instruction) (err error) {
45 | _, err = m.writeTunnelMessage([]byte(msg.String()))
46 | return err
47 | }
48 |
49 | func (m *MonitorCon) writeTunnelMessage(p []byte) (int, error) {
50 | m.guacdLock.Lock()
51 | defer m.guacdLock.Unlock()
52 | return m.guacdTunnel.WriteAndFlush(p)
53 | }
54 | func (m *MonitorCon) readTunnelInstruction() (*guacd.Instruction, error) {
55 | instruction, err := m.guacdTunnel.ReadInstruction()
56 | if err != nil {
57 | return nil, err
58 | }
59 | return &instruction, nil
60 | }
61 |
62 | func (m *MonitorCon) Run(ctx context.Context) (err error) {
63 | retChan := m.Service.Cache.GetSessionEventChan(m.Id)
64 | if m.Meta != nil {
65 | var jsonBuilder strings.Builder
66 | _ = json.NewEncoder(&jsonBuilder).Encode(m.Meta)
67 | metaJsonStr := jsonBuilder.String()
68 | currentUserInst := NewJmsEventInstruction("current_user", metaJsonStr)
69 | _ = m.SendWsMessage(currentUserInst)
70 | eventData := []byte(metaJsonStr)
71 | m.Service.Cache.BroadcastSessionEvent(m.Id, &Event{Type: ShareJoin, Data: eventData})
72 | defer func() {
73 | m.Service.Cache.BroadcastSessionEvent(m.Id, &Event{Type: ShareExit, Data: eventData})
74 | }()
75 | }
76 |
77 | exit := make(chan error, 2)
78 | go func(t *MonitorCon) {
79 | for {
80 | instruction, err1 := t.readTunnelInstruction()
81 | if err1 != nil {
82 | _ = t.writeWsMessage([]byte(ErrDisconnect.String()))
83 | logger.Infof("Monitor[%s] guacd tunnel read err: %+v", t.Id, err1)
84 | exit <- err1
85 | break
86 | }
87 | if err2 := t.writeWsMessage([]byte(instruction.String())); err2 != nil {
88 | logger.Error(err2)
89 | exit <- err2
90 | break
91 | }
92 | }
93 | _ = t.ws.Close()
94 | }(m)
95 |
96 | go func(t *MonitorCon) {
97 | for {
98 | _, message, err1 := t.ws.ReadMessage()
99 | if err1 != nil {
100 | logger.Infof("Monitor[%s] ws read err: %+v", t.Id, err1)
101 |
102 | exit <- err1
103 | break
104 | }
105 | if ret, err2 := guacd.ParseInstructionString(string(message)); err2 == nil {
106 | if ret.Opcode == INTERNALDATAOPCODE && len(ret.Args) >= 2 && ret.Args[0] == PINGOPCODE {
107 | if err3 := t.SendWsMessage(guacd.NewInstruction(INTERNALDATAOPCODE, PINGOPCODE)); err3 != nil {
108 | logger.Error(err3)
109 | }
110 | continue
111 | }
112 | if t.lockedStatus.Load() {
113 | switch ret.Opcode {
114 | case guacd.InstructionClientSync,
115 | guacd.InstructionClientNop,
116 | guacd.InstructionStreamingAck:
117 | default:
118 | logger.Infof("Session[%s] in locked status drop receive web client message opcode[%s]",
119 | t.Id, ret.Opcode)
120 | continue
121 | }
122 | _, err4 := t.writeTunnelMessage(message)
123 | if err4 != nil {
124 | logger.Errorf("Session[%s] guacamole server write err: %+v", t.Id, err2)
125 | exit <- err4
126 | break
127 | }
128 | logger.Debugf("Session[%s] send guacamole server message when locked status", t.Id)
129 | continue
130 | }
131 | } else {
132 | logger.Errorf("Monitor[%s] parse instruction err %s", t.Id, err2)
133 | }
134 | _, err3 := t.writeTunnelMessage(message)
135 | if err3 != nil {
136 | logger.Errorf("Monitor[%s] guacamole tunnel write err: %+v", t.Id, err3)
137 | exit <- err3
138 | break
139 | }
140 | }
141 | _ = t.guacdTunnel.Close()
142 | }(m)
143 |
144 | for {
145 | select {
146 | case err = <-exit:
147 | logger.Infof("Monitor[%s] exit: %+v", m.Id, err)
148 | return err
149 | case <-ctx.Done():
150 | logger.Infof("Monitor[%s] done", m.Id)
151 | return nil
152 | case event := <-retChan.eventCh:
153 | if m.Meta == nil {
154 | logger.Debugf("Monitor[%s] do not need to handle event", m.Id)
155 | continue
156 | }
157 | go m.handleEvent(event)
158 | logger.Debugf("Monitor[%s] handle event: %s", m.Id, event.Type)
159 | }
160 | }
161 | }
162 |
163 | func (m *MonitorCon) handleEvent(eventMsg *Event) {
164 | logger.Debugf("Monitor[%s] handle event: %s", m.Id, eventMsg.Type)
165 | var inst guacd.Instruction
166 | switch eventMsg.Type {
167 | case ShareJoin:
168 | var meta MetaShareUserMessage
169 | _ = json.Unmarshal(eventMsg.Data, &meta)
170 | if m.Meta.ShareId == meta.ShareId {
171 | logger.Info("Ignore self join event")
172 | return
173 | }
174 | inst = NewJmsEventInstruction(ShareJoin, string(eventMsg.Data))
175 | case ShareExit:
176 | inst = NewJmsEventInstruction(ShareExit, string(eventMsg.Data))
177 | case ShareUsers:
178 | inst = NewJmsEventInstruction(ShareUsers, string(eventMsg.Data))
179 | case ShareRemoveUser:
180 | var removeData struct {
181 | User string `json:"user"`
182 | Meta MetaShareUserMessage `json:"meta"`
183 | }
184 | _ = json.Unmarshal(eventMsg.Data, &removeData)
185 | if m.Meta.ShareId != removeData.Meta.ShareId {
186 | logger.Info("Ignore not self remove user event")
187 | return
188 | }
189 | errInst := NewJMSGuacamoleError(1011, removeData.User)
190 | inst = errInst.Instruction()
191 | case ShareSessionPause, ShareSessionResume:
192 | inst = NewJmsEventInstruction(eventMsg.Type, string(eventMsg.Data))
193 | locked := eventMsg.Type == ShareSessionPause
194 | m.lockedStatus.Store(locked)
195 | default:
196 | return
197 | }
198 | _ = m.SendWsMessage(inst)
199 | }
200 |
--------------------------------------------------------------------------------
/pkg/session/parser.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "encoding/hex"
7 | "fmt"
8 | "strconv"
9 | "strings"
10 | "sync"
11 | "time"
12 |
13 | "lion/pkg/guacd"
14 | "lion/pkg/logger"
15 |
16 | "github.com/jumpserver-dev/sdk-go/model"
17 | "github.com/jumpserver-dev/sdk-go/service"
18 | )
19 |
20 | var (
21 | charEnter = []byte("\r")
22 | )
23 |
24 | var _ ParseEngine = (*Parser)(nil)
25 |
26 | type Parser struct {
27 | id string
28 | jmsService *service.JMService
29 | cmdRecordChan chan *ExecutedCommand
30 |
31 | buf bytes.Buffer
32 |
33 | inputPreState bool
34 | inputState bool
35 | once *sync.Once
36 | lock *sync.RWMutex
37 |
38 | command string
39 | cmdCreateDate time.Time
40 |
41 | closed chan struct{}
42 | currentActiveUser CurrentActiveUser
43 | }
44 |
45 | func (p *Parser) initial() {
46 | p.once = new(sync.Once)
47 | p.lock = new(sync.RWMutex)
48 | p.closed = make(chan struct{})
49 | p.cmdRecordChan = make(chan *ExecutedCommand, 1024)
50 | }
51 |
52 | // ParseStream 解析数据流
53 | func (p *Parser) ParseStream(userInChan chan *Message) {
54 | logger.Infof("Session %s: Parser start", p.id)
55 | go func() {
56 | defer func() {
57 | // 会话结束,结算命令结果
58 | p.sendCommandRecord()
59 | close(p.cmdRecordChan)
60 | logger.Infof("Session %s: Parser routine done", p.id)
61 | }()
62 | maxTimeout := time.Second * 20
63 | cmdRecordTicker := time.NewTicker(time.Second * 30)
64 | defer cmdRecordTicker.Stop()
65 | lastActiveTime := time.Now()
66 | for {
67 | select {
68 | case <-p.closed:
69 | return
70 | case now := <-cmdRecordTicker.C:
71 | if now.Sub(lastActiveTime) > maxTimeout {
72 | p.ParseUserInput(charEnter) //手动结算一次命令
73 | }
74 | continue
75 | case msg, ok := <-userInChan:
76 | if !ok {
77 | return
78 | }
79 | lastActiveTime = time.Now()
80 | p.UpdateActiveUser(msg)
81 | s := msg.Body
82 | var b []byte
83 | switch msg.Opcode {
84 | case guacd.InstructionMouse:
85 | var cmd string
86 | switch s[2] {
87 | case guacd.MouseLeft:
88 | cmd = "Left Button"
89 | case guacd.MouseRight:
90 | cmd = "Right Button"
91 | case guacd.MouseMiddle:
92 | cmd = "Middle Button"
93 | default:
94 | continue
95 | }
96 | p.ParseUserInput(charEnter) //手动结算一次命令
97 | cmd = fmt.Sprintf("Mouse Position[%s,%s] %s\r", s[0], s[1], cmd)
98 | b = append(b, []byte(cmd)...)
99 | case guacd.InstructionKey:
100 | switch s[1] {
101 | case guacd.KeyPress:
102 | keyCode, err := strconv.Atoi(s[0])
103 | if err == nil {
104 | cb := []byte(guacd.KeysymToCharacter(keyCode))
105 | if len(cb) == 0 {
106 | // guacamole-common.js unicode计算方法
107 | // if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
108 | // return 0x01000000 | codepoint;
109 | if keyCode > 0x01000000 {
110 | var to string
111 | unicode := strconv.FormatInt(int64(keyCode), 16)
112 | bs, _ := hex.DecodeString(unicode[3:])
113 | for i, bl, br, r := 0, len(bs), bytes.NewReader(bs), uint16(0); i < bl; i += 2 {
114 | _ = binary.Read(br, binary.BigEndian, &r)
115 | to += string(rune(r))
116 | }
117 | b = append(b, []byte(to)...)
118 | } else {
119 | // 未知的键值,转成 rune 字符
120 | b = append(b, []byte(string(rune(keyCode)))...)
121 | }
122 | } else {
123 | b = append(b, cb...)
124 | }
125 | } else {
126 | b = append(b, []byte(guacd.KeyCodeUnknown)...)
127 | }
128 | default:
129 | continue
130 | }
131 | }
132 | if len(b) == 0 {
133 | continue
134 | }
135 | _, _ = p.WriteData(b)
136 | p.ParseUserInput(b)
137 | }
138 | }
139 | }()
140 | }
141 |
142 | // ParseUserInput 解析用户的输入
143 | func (p *Parser) ParseUserInput(b []byte) {
144 | _ = p.parseInputState(b)
145 | }
146 |
147 | // parseInputState 切换用户输入状态, 并结算命令和结果
148 | func (p *Parser) parseInputState(b []byte) []byte {
149 | p.inputPreState = p.inputState
150 | if bytes.LastIndex(b, charEnter) >= 0 {
151 | // 连续输入enter key, 结算上一条可能存在的命令结果
152 | p.sendCommandRecord()
153 | p.inputState = false
154 | // 用户输入了Enter,开始结算命令
155 | p.parseCmdInput()
156 | } else {
157 | p.inputState = true
158 | // 用户又开始输入,并上次不处于输入状态,开始结算上次命令的结果
159 | if !p.inputPreState {
160 | p.sendCommandRecord()
161 | }
162 | }
163 | return b
164 | }
165 |
166 | // parseCmdInput 解析命令的输入
167 | func (p *Parser) parseCmdInput() {
168 | command := p.Parse()
169 | if len(command) <= 0 {
170 | p.command = ""
171 | } else {
172 | p.command = command
173 | }
174 | p.cmdCreateDate = time.Now()
175 | }
176 |
177 | func (p *Parser) WriteData(b []byte) (int, error) {
178 | p.lock.Lock()
179 | defer p.lock.Unlock()
180 | if p.buf.Len() >= 2048 {
181 | return 0, nil
182 | }
183 | if len(b) > 1 {
184 | p.buf.WriteByte(byte(' '))
185 | }
186 | return p.buf.Write(b)
187 | }
188 |
189 | func (p *Parser) Parse() string {
190 | line := p.buf.String()
191 | line = strings.TrimPrefix(line, string(charEnter))
192 | p.buf.Reset()
193 | return line
194 | }
195 |
196 | // Close 关闭parser
197 | func (p *Parser) Close() {
198 | select {
199 | case <-p.closed:
200 | return
201 | default:
202 | close(p.closed)
203 | }
204 | logger.Infof("Session %s: Parser close", p.id)
205 | }
206 |
207 | func (p *Parser) sendCommandRecord() {
208 | if p.command != "" {
209 | p.cmdRecordChan <- &ExecutedCommand{
210 | Command: p.command,
211 | CreatedDate: p.cmdCreateDate,
212 | RiskLevel: model.NormalLevel,
213 | User: p.currentActiveUser,
214 | }
215 | p.command = ""
216 | }
217 | }
218 |
219 | func (p *Parser) CommandRecordChan() chan *ExecutedCommand {
220 | return p.cmdRecordChan
221 | }
222 |
223 | func (p *Parser) UpdateActiveUser(msg *Message) {
224 | p.currentActiveUser.UserId = msg.Meta.UserId
225 | p.currentActiveUser.User = msg.Meta.User
226 | }
227 |
228 | type ExecutedCommand struct {
229 | Command string
230 | Output string
231 | CreatedDate time.Time
232 | RiskLevel int
233 | User CurrentActiveUser
234 | }
235 |
236 | type CurrentActiveUser struct {
237 | UserId string
238 | User string
239 | }
240 |
--------------------------------------------------------------------------------
/ui/src/components/Osk.vue:
--------------------------------------------------------------------------------
1 |
144 |
145 |
146 |
158 |
159 |
160 | ⋮⋮
161 |
162 |
163 |
164 |
165 |
210 |
--------------------------------------------------------------------------------
/ui/src/views/ShareView.vue:
--------------------------------------------------------------------------------
1 |
150 |
151 |
152 |
161 |
170 |
171 |
172 |
173 |
178 |
179 |
180 |
185 |
186 |
{{ errMessage }}
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
--------------------------------------------------------------------------------
/pkg/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "math/rand"
7 | "net"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 |
12 | "github.com/jumpserver-dev/sdk-go/common"
13 |
14 | "github.com/spf13/viper"
15 | )
16 |
17 | var GlobalConfig *Config
18 |
19 | type Config struct {
20 | Root string
21 | DrivePath string
22 | RecordPath string
23 | FTPFilePath string
24 | LogDirPath string
25 | AccessKeyFilePath string
26 | CertsFolderPath string
27 | SessionFolderPath string
28 |
29 | Name string `mapstructure:"NAME"`
30 | CoreHost string `mapstructure:"CORE_HOST"`
31 | BootstrapToken string `mapstructure:"BOOTSTRAP_TOKEN"`
32 | BindHost string `mapstructure:"BIND_HOST"`
33 | HTTPPort string `mapstructure:"HTTPD_PORT"`
34 | LogLevel string `mapstructure:"LOG_LEVEL"`
35 |
36 | GuacdAddrs string `mapstructure:"GUACD_ADDRS"`
37 |
38 | GuaHost string `mapstructure:"GUA_HOST"`
39 | GuaPort string `mapstructure:"GUA_PORT"`
40 | DisableAllCopyPaste bool `mapstructure:"JUMPSERVER_DISABLE_ALL_COPY_PASTE"`
41 | DisableAllUpDownload bool `mapstructure:"JUMPSERVER_DISABLE_ALL_UPLOAD_DOWNLOAD"`
42 | EnableRemoteAppUpDownLoad bool `mapstructure:"JUMPSERVER_REMOTE_APP_UPLOAD_DOWNLOAD_ENABLE"`
43 | EnableRemoteAPPCopyPaste bool `mapstructure:"JUMPSERVER_REMOTE_APP_COPY_PASTE_ENABLE"`
44 | CleanDriveScheduleTime int `mapstructure:"JUMPSERVER_CLEAN_DRIVE_SCHEDULE_TIME"`
45 |
46 | ShareRoomType string `mapstructure:"SHARE_ROOM_TYPE"`
47 | RedisHost string `mapstructure:"REDIS_HOST"`
48 | RedisPort int `mapstructure:"REDIS_PORT"`
49 | RedisPassword string `mapstructure:"REDIS_PASSWORD"`
50 | RedisDBIndex int `mapstructure:"REDIS_DB_ROOM"`
51 |
52 | RedisSentinelPassword string `mapstructure:"REDIS_SENTINEL_PASSWORD"`
53 | RedisSentinelHosts string `mapstructure:"REDIS_SENTINEL_HOSTS"`
54 | RedisUseSSL bool `mapstructure:"REDIS_USE_SSL"`
55 |
56 | EnableVideoWorker bool `mapstructure:"ENABLE_VIDEO_WORKER"`
57 | VideoWorkerHost string `mapstructure:"VIDEO_WORKER_HOST"`
58 | IgnoreVerifyCerts bool `mapstructure:"IGNORE_VERIFY_CERTS"`
59 | PandaHost string `mapstructure:"PANDA_HOST"`
60 | EnablePanda bool `mapstructure:"ENABLE_PANDA"`
61 |
62 | ReplayMaxSize int `mapstructure:"REPLAY_MAX_SIZE"`
63 | SecretEncryptKey string `mapstructure:"SECRET_ENCRYPT_KEY"`
64 |
65 | VncClipboardEncoding string `mapstructure:"VNC_CLIPBOARD_ENCODING"`
66 | }
67 |
68 | func (c *Config) UpdateRedisPassword(val string) {
69 | c.RedisPassword = val
70 | }
71 |
72 | func (c *Config) SelectGuacdAddr() string {
73 | if len(c.GuacdAddrs) == 0 {
74 | return net.JoinHostPort(c.GuaHost, c.GuaPort)
75 | }
76 | addresses := strings.Split(c.GuacdAddrs, ",")
77 | return addresses[rand.Intn(len(addresses))]
78 | }
79 |
80 | func Setup(configPath string) {
81 | var conf = getDefaultConfig()
82 | loadConfigFromEnv(&conf)
83 | loadConfigFromFile(configPath, &conf)
84 | GlobalConfig = &conf
85 | log.Printf("%+v\n", GlobalConfig)
86 |
87 | }
88 |
89 | func getDefaultConfig() Config {
90 | defaultName := getDefaultName()
91 | rootPath := getPwdDirPath()
92 | dataFolderPath := filepath.Join(rootPath, "data")
93 | driveFolderPath := filepath.Join(dataFolderPath, "drive")
94 | recordFolderPath := filepath.Join(dataFolderPath, "replays")
95 | sessionsPath := filepath.Join(dataFolderPath, "sessions")
96 | ftpFileFolderPath := filepath.Join(dataFolderPath, "ftp_files")
97 | LogDirPath := filepath.Join(dataFolderPath, "logs")
98 | keyFolderPath := filepath.Join(dataFolderPath, "keys")
99 | CertsFolderPath := filepath.Join(dataFolderPath, "certs")
100 | accessKeyFilePath := filepath.Join(keyFolderPath, ".access_key")
101 |
102 | folders := []string{dataFolderPath, driveFolderPath, recordFolderPath,
103 | keyFolderPath, LogDirPath, CertsFolderPath, sessionsPath}
104 | for i := range folders {
105 | if err := EnsureDirExist(folders[i]); err != nil {
106 | log.Fatalf("Create folder failed: %s", err.Error())
107 | }
108 | }
109 | return Config{
110 | Name: defaultName,
111 | Root: rootPath,
112 | RecordPath: recordFolderPath,
113 | FTPFilePath: ftpFileFolderPath,
114 | LogDirPath: LogDirPath,
115 | DrivePath: driveFolderPath,
116 | CertsFolderPath: CertsFolderPath,
117 | AccessKeyFilePath: accessKeyFilePath,
118 | SessionFolderPath: sessionsPath,
119 | CoreHost: "http://localhost:8080",
120 | BootstrapToken: "",
121 | BindHost: "0.0.0.0",
122 | HTTPPort: "8081",
123 | LogLevel: "INFO",
124 | GuaHost: "127.0.0.1",
125 | GuaPort: "4822",
126 | DisableAllCopyPaste: false,
127 | DisableAllUpDownload: false,
128 | EnableRemoteAppUpDownLoad: false,
129 | EnableRemoteAPPCopyPaste: false,
130 | CleanDriveScheduleTime: 1,
131 | PandaHost: "http://panda:9001",
132 | ReplayMaxSize: defaultMaxSize,
133 | VideoWorkerHost: "http://video:9000",
134 | }
135 |
136 | }
137 |
138 | // 300MB
139 | const defaultMaxSize = 1024 * 1024 * 300
140 |
141 | func EnsureDirExist(path string) error {
142 | if !haveDir(path) {
143 | if err := os.MkdirAll(path, os.ModePerm); err != nil {
144 | return err
145 | }
146 | }
147 | return nil
148 | }
149 |
150 | func have(path string) bool {
151 | _, err := os.Stat(path)
152 | return err == nil
153 | }
154 |
155 | func haveDir(file string) bool {
156 | fi, err := os.Stat(file)
157 | return err == nil && fi.IsDir()
158 | }
159 |
160 | func getPwdDirPath() string {
161 | if rootPath, err := os.Getwd(); err == nil {
162 | return rootPath
163 | }
164 | return ""
165 | }
166 |
167 | func loadConfigFromEnv(conf *Config) {
168 | viper.AutomaticEnv() // 全局配置,用于其他 pkg 包可以用 viper 获取环境变量的值
169 | envViper := viper.New()
170 | for _, item := range os.Environ() {
171 | envItem := strings.SplitN(item, "=", 2)
172 | if len(envItem) == 2 {
173 | envViper.Set(envItem[0], viper.Get(envItem[0]))
174 | }
175 | }
176 | if err := envViper.Unmarshal(conf); err == nil {
177 | log.Println("Load config from env")
178 | }
179 |
180 | }
181 |
182 | func loadConfigFromFile(path string, conf *Config) {
183 | var err error
184 | if have(path) {
185 | fileViper := viper.New()
186 | fileViper.SetConfigFile(path)
187 | if err = fileViper.ReadInConfig(); err == nil {
188 | if err = fileViper.Unmarshal(conf); err == nil {
189 | log.Printf("Load config from %s success\n", path)
190 | return
191 | }
192 | }
193 | }
194 | if err != nil {
195 | log.Fatalf("Load config from %s failed: %s\n", path, err)
196 | }
197 | }
198 |
199 | const (
200 | prefixName = "[Lion]-"
201 |
202 | hostEnvKey = "SERVER_HOSTNAME"
203 |
204 | defaultNameMaxLen = 128
205 | )
206 |
207 | /*
208 | SERVER_HOSTNAME: 环境变量名,可用于自定义默认注册名称的前缀
209 | default name rule:
210 | [Lion]-{SERVER_HOSTNAME}-{HOSTNAME}-{UUID}
211 | or
212 | [Lion]-{HOSTNAME}-{UUID}
213 | */
214 |
215 | func getDefaultName() string {
216 | hostname, _ := os.Hostname()
217 | hostname = fmt.Sprintf("%s-%s", hostname, common.RandomStr(7))
218 | if serverHostname, ok := os.LookupEnv(hostEnvKey); ok {
219 | hostname = fmt.Sprintf("%s-%s", serverHostname, hostname)
220 | }
221 | hostRune := []rune(prefixName + hostname)
222 | if len(hostRune) <= defaultNameMaxLen {
223 | return string(hostRune)
224 | }
225 | name := make([]rune, defaultNameMaxLen)
226 | index := defaultNameMaxLen / 2
227 | copy(name[:16], hostRune[:index])
228 | start := len(hostRune) - index
229 | copy(name[index:], hostRune[start:])
230 | return string(name)
231 | }
232 |
--------------------------------------------------------------------------------