├── frontend
├── src
│ ├── libs
│ │ ├── config.js
│ │ ├── states.js
│ │ ├── clipboard.js
│ │ ├── request.js
│ │ ├── store.js
│ │ └── GuacMouse.js
│ ├── assets
│ │ └── logo.png
│ ├── main.js
│ ├── App.vue
│ └── components
│ │ └── GuacClient.vue
├── public
│ ├── favicon.ico
│ └── index.html
├── babel.config.js
├── .gitignore
├── README.md
└── package.json
├── go-websocket-guacd.jpg
├── .dockerignore
├── guac
├── doc.go
├── readme.md
├── counted_lock.go
├── counted_lock_test.go
├── config.go
├── guac.go
├── errors.go
├── stream_conn_test.go
├── tunnel_pipe.go
├── guac_instruction.go
├── status.go
└── stream_conn.go
├── go.mod
├── .gitignore
├── readme.md
├── Dockerfile
├── docker-compose.yaml
├── main.go
├── go-websocket-guacd
├── api_ws_guaca.go
└── go.sum
/frontend/src/libs/config.js:
--------------------------------------------------------------------------------
1 | let config = {};
2 |
3 | export default config;
4 |
--------------------------------------------------------------------------------
/go-websocket-guacd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mojocn/rdpgo/HEAD/go-websocket-guacd.jpg
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .run
3 | *.md
4 | *.toml
5 | *.yam
6 |
7 | frontend
8 | !frontend/dist
9 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mojocn/rdpgo/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mojocn/rdpgo/HEAD/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/guac/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package guac implements a HTTP client and a WebSocket client that connects to an Apache Guacamole server.
3 | */
4 | package guac
5 |
--------------------------------------------------------------------------------
/guac/readme.md:
--------------------------------------------------------------------------------
1 | # 实现 https://guacamole.apache.org/doc/gug/guacamole-protocol.html#guacamole-protocol-handshake
2 |
3 |
4 | Chapter 19. The Guacamole protocol
5 | - 章节:Design(这个package实现)
6 | - 章节:Handshake(这个package实现)
7 | - 章节:Draw(客户端实现js,不关我事,)
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module rdpgo
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/gin-gonic/gin v1.6.3
7 | github.com/gorilla/websocket v1.4.2
8 | github.com/sirupsen/logrus v1.8.0
9 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
10 | )
11 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import ElementUI from 'element-ui';
4 | import 'element-ui/lib/theme-chalk/index.css';
5 | Vue.config.productionTip = false
6 | Vue.use(ElementUI);
7 |
8 | new Vue({
9 | render: h => h(App),
10 | }).$mount('#app')
11 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | .idea
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | frontend/.idea
5 | frontend/node_modules
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 | .idea
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # gei
2 |
3 |
4 |
5 | ## 运行demo
6 | ```shell
7 | cd $代码目录
8 |
9 | # 编译前端代码
10 | cd frontend
11 | npm install
12 | npm run build
13 |
14 | # 编辑golang 代码(docker)
15 | cd ..
16 |
17 | docker-compose build
18 |
19 | # 运行 docker-compose
20 | docker-compose up --remove-orphans
21 |
22 |
23 | echo '浏览器访问 http://127.0.0.1:9528'
24 | ```
25 |
26 | ## todo:(懒...)
27 |
28 | 1. guac 目录独立单独的package,只依赖标准库,减少go.mod的行数
29 | 2. 优化前端代码,
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Vuejs 前端Demo代码
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | npm run lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
30 |
--------------------------------------------------------------------------------
/guac/counted_lock.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | import (
4 | "sync"
5 | "sync/atomic"
6 | )
7 |
8 | // CountedLock counts how many goroutines are waiting on the lock
9 | type CountedLock struct {
10 | core sync.Mutex
11 | numLocks int32
12 | }
13 |
14 | // Lock locks the mutex
15 | func (r *CountedLock) Lock() {
16 | atomic.AddInt32(&r.numLocks, 1)
17 | r.core.Lock()
18 | }
19 |
20 | // Unlock unlocks the mutex
21 | func (r *CountedLock) Unlock() {
22 | atomic.AddInt32(&r.numLocks, -1)
23 | r.core.Unlock()
24 | }
25 |
26 | // HasQueued returns true if a goroutine is waiting on the lock
27 | func (r *CountedLock) HasQueued() bool {
28 | return atomic.LoadInt32(&r.numLocks) > 1
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/guac/counted_lock_test.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestCountedLock_HasQueued(t *testing.T) {
9 | lock := CountedLock{}
10 |
11 | if lock.HasQueued() {
12 | t.Error("Expected false got true (1)")
13 | }
14 |
15 | lock.Lock()
16 |
17 | if lock.HasQueued() {
18 | t.Error("Expected false got true (2)")
19 | }
20 |
21 | go func() {
22 | lock.Lock()
23 | }()
24 |
25 | // ensure the goroutine above runs
26 | time.Sleep(time.Millisecond)
27 |
28 | if !lock.HasQueued() {
29 | t.Error("Expected true got false")
30 | }
31 |
32 | lock.Unlock()
33 |
34 | if lock.HasQueued() {
35 | t.Error("Expected false got true (3)")
36 | }
37 |
38 | // ensure the lock is taken in the goroutine
39 | time.Sleep(time.Millisecond)
40 |
41 | lock.Unlock()
42 |
43 | if lock.HasQueued() {
44 | t.Error("Expected false got true (4)")
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ## 1. 编译golang代码
2 | FROM golang:1.16-alpine as builder
3 | RUN sed -i "s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g" /etc/apk/repositories &&\
4 | apk add --no-cache git
5 |
6 | ENV GOPROXY=https://goproxy.io
7 | ARG GO_DIR=.
8 |
9 | WORKDIR /sshark
10 | # git describe --tags --always
11 |
12 | COPY $GO_DIR/go.mod .
13 | COPY $GO_DIR/go.sum .
14 | RUN go mod download
15 |
16 | COPY $GO_DIR .
17 | RUN export GITHASH=$(git rev-parse --short HEAD) && \
18 | export BUILDAT=$(date) && \
19 | go build -ldflags "-w -s -X 'main.buildAt=$BUILDAT' -X 'main.gitHash=$GITHASH'"
20 |
21 |
22 |
23 |
24 | ## 3. copy聚合前端代码
25 | FROM alpine
26 | LABEL maintainer="Eric Zhou"
27 |
28 | RUN sed -i "s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g" /etc/apk/repositories && apk add curl
29 |
30 | # web+api端口建议映射到443 80
31 | EXPOSE 9528
32 |
33 | WORKDIR /opt/bin
34 |
35 | # 需要挂在的目录,配置文件和日志
36 | # /data/kssh.config 挂在配置文件
37 | # /data/sshark 读写目录挂载 ssh-sessrion-terminal 日志文件
38 | VOLUME /data
39 |
40 | COPY --from=builder /sshark/rdpgo rdpgo
41 |
42 | CMD ["./rdpgo"]
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Go.Web.RDP.VNC",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "core-js": "^3.6.5",
12 | "element-ui": "^2.15.1",
13 | "guacamole-common-js": "^1.3.0",
14 | "vue": "^2.6.11"
15 | },
16 | "devDependencies": {
17 | "@vue/cli-plugin-babel": "~4.5.0",
18 | "@vue/cli-plugin-eslint": "~4.5.0",
19 | "@vue/cli-service": "~4.5.0",
20 | "babel-eslint": "^10.1.0",
21 | "eslint": "^6.7.2",
22 | "eslint-plugin-vue": "^6.2.2",
23 | "vue-template-compiler": "^2.6.11"
24 | },
25 | "eslintConfig": {
26 | "root": true,
27 | "env": {
28 | "node": true
29 | },
30 | "extends": [
31 | "plugin:vue/essential",
32 | "eslint:recommended"
33 | ],
34 | "parserOptions": {
35 | "parser": "babel-eslint"
36 | },
37 | "rules": {}
38 | },
39 | "browserslist": [
40 | "> 1%",
41 | "last 2 versions",
42 | "not dead"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 |
2 | version: '3'
3 |
4 | services:
5 | app:
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | container_name: appgo.mojotv.cn
10 | ports:
11 | - 9528:9528
12 | depends_on:
13 | - rdp
14 | - vnc
15 |
16 | # 提供guac 协议
17 | guacd:
18 | container_name: guacd.mojotv.cn
19 | image: guacamole/guacd
20 | # ports:
21 | # - 4822:4822
22 | restart: always
23 |
24 | vnc:
25 | # vnc 图形界面Ubuntu虚拟机容器 password: vncpassword 容器 vnc端口5901
26 | image: consol/ubuntu-xfce-vnc:1.4.0
27 | container_name: guacd-vnc.mojotv.cn
28 | # environment:
29 | # - VNC_RESOLUTION=1024x768
30 | depends_on:
31 | - guacd
32 | # ports:
33 | # - 5901:5901
34 | # - 6901:6901
35 |
36 |
37 | rdp: #rdp 图形界面Ubuntu虚拟机容器 远程桌面支持rdp协议 容器rdp端口 3389
38 | image: umis/xubuntu-office-xrdp-desktop:v1.0
39 | container_name: guacd-rdp.mojotv.cn #容器名称 aka ( hostname,内网域名)
40 | environment:
41 | - "USERNAME:root"
42 | - "PASSWORD:Docker"
43 | depends_on:
44 | - guacd
45 | # ports:
46 | # - 3389:3389
47 |
48 | networks:
49 | mojo:
50 | driver: bridge
51 |
--------------------------------------------------------------------------------
/frontend/src/libs/states.js:
--------------------------------------------------------------------------------
1 | export default {
2 | /**
3 | * The Guacamole connection has not yet been attempted.
4 | *
5 | * @type String
6 | */
7 | IDLE: "IDLE",
8 |
9 | /**
10 | * The Guacamole connection is being established.
11 | *
12 | * @type String
13 | */
14 | CONNECTING: "CONNECTING",
15 |
16 | /**
17 | * The Guacamole connection has been successfully established, and the
18 | * client is now waiting for receipt of initial graphical data.
19 | *
20 | * @type String
21 | */
22 | WAITING: "WAITING",
23 |
24 | /**
25 | * The Guacamole connection has been successfully established, and
26 | * initial graphical data has been received.
27 | *
28 | * @type String
29 | */
30 | CONNECTED: "CONNECTED",
31 |
32 | /**
33 | * The Guacamole connection has terminated successfully. No errors are
34 | * indicated.
35 | *
36 | * @type String
37 | */
38 | DISCONNECTED: "DISCONNECTED",
39 |
40 | /**
41 | * The Guacamole connection has terminated due to an error reported by
42 | * the client. The associated error code is stored in statusCode.
43 | *
44 | * @type String
45 | */
46 | CLIENT_ERROR: "CLIENT_ERROR",
47 |
48 | /**
49 | * The Guacamole connection has terminated due to an error reported by
50 | * the tunnel. The associated error code is stored in statusCode.
51 | *
52 | * @type String
53 | */
54 | TUNNEL_ERROR: "TUNNEL_ERROR"
55 | }
56 |
--------------------------------------------------------------------------------
/guac/config.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | // Config is the data sent to guacd to configure the session during the handshake.
4 | type Config struct {
5 | // ConnectionID is used to reconnect to an existing session, otherwise leave blank for a new session.
6 | ConnectionID string
7 | // Protocol is the protocol of the connection from guacd to the remote (rdp, ssh, etc).
8 | Protocol string
9 | // Parameters are used to configure protocol specific options like sla for rdp or terminal color schemes.
10 | Parameters map[string]string
11 |
12 | // OptimalScreenWidth is the desired width of the screen
13 | OptimalScreenWidth int
14 | // OptimalScreenHeight is the desired height of the screen
15 | OptimalScreenHeight int
16 | // OptimalResolution is the desired resolution of the screen
17 | OptimalResolution int
18 | // AudioMimetypes is an array of the supported audio types
19 | AudioMimetypes []string
20 | // VideoMimetypes is an array of the supported video types
21 | VideoMimetypes []string
22 | // ImageMimetypes is an array of the supported image types
23 | ImageMimetypes []string
24 | }
25 |
26 | // NewGuacamoleConfiguration returns a Config with sane defaults
27 | func NewGuacamoleConfiguration() *Config {
28 | return &Config{
29 | Parameters: map[string]string{},
30 | OptimalScreenWidth: 1024,
31 | OptimalScreenHeight: 768,
32 | OptimalResolution: 96,
33 | AudioMimetypes: make([]string, 0, 1),
34 | VideoMimetypes: make([]string, 0, 1),
35 | ImageMimetypes: make([]string, 0, 1),
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/guac/guac.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | import (
4 | "github.com/sirupsen/logrus"
5 | "net"
6 | )
7 |
8 | //scheme: this.scheme,
9 | //hostname: this.hostname,
10 | //port: this.port,
11 | //'ignore-cert': this.ignoreCert,
12 | //security: this.security,
13 | //username: this.user,
14 | //password: this.pass
15 | func NewGuacamoleTunnel(guacadAddr, protocol, host, port, user, password, uuid string, w, h, dpi int) (s *SimpleTunnel, err error) {
16 | config := NewGuacamoleConfiguration()
17 | config.ConnectionID = uuid
18 | config.Protocol = protocol
19 | config.OptimalScreenHeight = h
20 | config.OptimalScreenWidth = w
21 | config.OptimalResolution = dpi
22 | config.AudioMimetypes = []string{"audio/L16", "rate=44100", "channels=2"}
23 | config.Parameters = map[string]string{
24 | "scheme": protocol,
25 | "hostname": host,
26 | "port": port,
27 | "ignore-cert": "true",
28 | "security": "",
29 | "username": user,
30 | "password": password,
31 | }
32 | addr, err := net.ResolveTCPAddr("tcp", guacadAddr)
33 | if err != nil {
34 | logrus.Errorln("error while connecting to guacd", err)
35 | return nil, err
36 | }
37 | conn, err := net.DialTCP("tcp", nil, addr)
38 | if err != nil {
39 | logrus.Errorln("error while connecting to guacd", err)
40 | return nil, err
41 | }
42 | stream := NewStream(conn, SocketTimeout)
43 | // 这一步才是初始化 rdp/vnc guacd 并认证资产的身份
44 | err = stream.Handshake(config)
45 | if err != nil {
46 | return nil, err
47 | }
48 | tunnel := NewSimpleTunnel(stream)
49 | return tunnel, nil
50 | }
51 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "github.com/gin-gonic/gin"
6 | "github.com/sirupsen/logrus"
7 | "path"
8 | "path/filepath"
9 | "strings"
10 | )
11 |
12 | var buildAt string
13 | var gitHash string
14 |
15 | func main() {
16 | logrus.SetReportCaller(true)
17 | r := gin.Default()
18 | r.GET("/version", func(c *gin.Context) { c.JSON(200, gin.H{gitHash: buildAt}) })
19 | r.Use(feMw("/")) //替换nginx serve 前端HTML代码
20 | r.GET("/ws", ApiWsGuacamole()) //websocket proxy to guacd
21 | r.Run(":9528")
22 | }
23 |
24 | //go:embed frontend/dist/*
25 | var fs embed.FS
26 |
27 | const fsBase = "frontend/dist" //和 embed一样
28 |
29 | //feMw 使用go.16新的特性embed 到包前端编译后的代码. 替代nginx. one binary rules them all
30 | func feMw(urlPrefix string) gin.HandlerFunc {
31 | const indexHtml = "index.html"
32 |
33 | return func(c *gin.Context) {
34 | urlPath := strings.TrimSpace(c.Request.URL.Path)
35 | if urlPath == urlPrefix {
36 | urlPath = path.Join(urlPrefix, indexHtml)
37 | }
38 | urlPath = filepath.Join(fsBase, urlPath)
39 |
40 | f, err := fs.Open(urlPath)
41 | if err != nil {
42 | return
43 | }
44 | fi, err := f.Stat()
45 | if strings.HasSuffix(urlPath, ".html") {
46 | c.Header("Cache-Control", "no-cache")
47 | c.Header("Content-Type", "text/html; charset=utf-8")
48 | }
49 |
50 | if strings.HasSuffix(urlPath, ".js") {
51 | c.Header("Content-Type", "text/javascript; charset=utf-8")
52 | }
53 | if strings.HasSuffix(urlPath, ".css") {
54 | c.Header("Content-Type", "text/css; charset=utf-8")
55 | }
56 |
57 | if err != nil || !fi.IsDir() {
58 | bs, err := fs.ReadFile(urlPath)
59 | if err != nil {
60 | logrus.WithError(err).Error("embed fs")
61 | return
62 | }
63 | c.Status(200)
64 | c.Writer.Write(bs)
65 | c.Abort()
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/go-websocket-guacd:
--------------------------------------------------------------------------------
1 | 7Vtbc5s4FP41PJoBCTB+TH1pdzbdzW4ybfOUkUHGagGxIMdOf/1KRtyxcWITPNN6MmPrIAnpXL5zjo6iwGmw+xijaP2ZuthXgObuFDhTANB12+RfgvKSUmxNEryYuLJTQbgnP7EkapK6IS5OKh0ZpT4jUZXo0DDEDqvQUBzTbbXbivrVt0bIww3CvYP8JvUrcdla7gKMC/onTLx19mbdmqRPApR1ljtJ1sil2xIJzhU4jSll6a9gN8W+YF7Gl3Tc4sDTfGExDtkpA/4ORo/f/nt+NJ4//zH/8/mfhLNspBtycewl2zF2OQNkk8ZsTT0aIn9eUD/EdBO6WEyr8VbR55bSiBN1TvyOGXuR0kQbRjlpzQJfPsU7wr6J4aopW4+lJ7OdnHnfeMkaIYtfSoNE87H8rBi2b2XjEoZidiM0gRMcHyUJcTLygvjZklI2iL0fZK8kJXQTO/gITyGUeopiD7NjHUGuBtx+MA0wXzgfGGMfMfJcXQmSiuzl/fKhd5TwNQJNGp2R2Y40OUPTqlOkO5CjyhpTnwiY1YkmtYnSHTYm4j9K+ylIe4V8hXJmW39G/kYyQwGWz1n6YUXFKy2P7UWX0khG+LhBDgoo19W5qdgzxV4oc1u5mYs/2Zevh9THc1p12hbDuEVLDnAVZUY+8UKhW1xRcMwJzzhmhCPIjXwQENdN7QYn5Cda7ucTqhkJxu1ZaX5QzJncVmZ7umxPqU/j/QrgYqHxT7428SK8U1pgUL6kAJ+yHh8BgqYuyulHmgommlFRhxFIm2eqK6hMqpvV8XS1SnAv2mW1KFdN4iiJUp+yIjuBd2WxN4XXUAQS7J1LKtUUC3VQ0Gck8PjKfbIU608chPn3l89PtyTc7NTk2Tsm6AYwHRQdGFfhwJrIjW4Lf2bKLuuSKzOsw5KsyOK1jB93M76FuUdE0WB87mqFrbgoWefOKhNJmwiQ7xPsPjHscztNOMGhQbThMz6hUJDjgHAnJx7c45iL5GmGkx+MRpcTlWnWRGUbDVEZQDUn5Y/RFJ1p9CQ6HQ4SLrzR9V/Qp9ununTYj0uveWI4rsFkz57Y7jbZipnRDfNJiKd5UK51WKwv/OodTQgjtNWZ3tY6LCljNGjxtkwolUCASCws2HkiL1G3eOlzrUtUZx1zblzAVvOoSookx9AOWDW1w8I/zzatQWzzDaH8ACE5ONF+gfUu9muODdWuTnJqUA5tXbW04qNXgzJrYqmlpxp4V5wARgMovvw1bQvDry62Fp/Fop/Y+oBSFbG1JZXhrWqWYZCtQnhE+v0F0/p4UPApQU8BRO3gMwSq9AMqsBbYmxCohm7ln/FJAHMx04cN0/93dnc9pt9m6nVI6MH0x0dNX1O5vPRqHHEeEmSYMlRWrdtDAoF+5UBwQBvOjS7sqwICswEE9/effnkgsDuAwLCzTP5c09dUDVb0YaQD1R4mLgDDwEFxYGBMqkcGqgZywoFjg33rDseEb14oXD85S3cq0hdY1FIReFp14GIaMWnAwwP2Q86KXxwhQBdCWIZ5GYTQVFCNOUYTdWwOgg9wmLwhx4cqNnQiQ+thRx5ZXMlhh34qwpxdfzzrvAo2DxV/JwzSII6dFZi6VT3yuUzGUE0YaiFjj/lCW423JvBG7ScqOWf+DhIluOSv31gaSiLkkNB72GMI7KoVOYLVTy6J+cqo4OtiS0I+c/KUpMUhcLmykN4oC0nTKZeF3vOoGfwqtdP6If/gtVPwu3h6SFaWfeXF0+yUoCS7GXV+cKYDTVxZcbOLKMs4v4Myt5TJVDhE4RZvlBu9Ie0iGBLS3K4Jw/ccycTTbYyiqrBXPOAo+bXVCltOGo3E9AcuPXHHk+X+pknV450vJWjV0g+tWTbTQYtJWX2BGWzWLIaISU8PADvjugOJxKVr0HX30nPiCJuJ46eHhztO+YqXiTAkdgWhYn5F6x1Cxe4rW5pdOwmC58WK75AQNssIGSzyAC3Mr/hRH4We2IkbeZR/tyFlfq2vPPKiAOqa2HaNNgC1wRJa1jsAaF4FGgxAjUEuBZ2YVx+v8l4k385C795x+Twpad1urvXaTiUEfON1nAbglgVZu6qDtglUedAYU7GfusUBzZzv8ZV3dgkuhBvSEHfK+7wA07qyGz9Gs9ISRlwIN/kF6BGPwgPe83vSkDXnA+vPKwp7TWTadvJ5ygXytXG3iHS7RUbg9TLizeKfN1L3V/wLDJz/Dw==
--------------------------------------------------------------------------------
/guac/errors.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type ErrGuac struct {
9 | error
10 | Status Status
11 | Kind ErrKind
12 | }
13 |
14 | type ErrKind int
15 |
16 | const (
17 | ErrClientBadType ErrKind = iota
18 | ErrClient
19 | ErrClientOverrun
20 | ErrClientTimeout
21 | ErrClientTooMany
22 | ErrConnectionClosed
23 | ErrOther
24 | ErrResourceClosed
25 | ErrResourceConflict
26 | ErrResourceNotFound
27 | ErrSecurity
28 | ErrServerBusy
29 | ErrServer
30 | ErrSessionClosed
31 | ErrSessionConflict
32 | ErrSessionTimeout
33 | ErrUnauthorized
34 | ErrUnsupported
35 | ErrUpstream
36 | ErrUpstreamNotFound
37 | ErrUpstreamTimeout
38 | ErrUpstreamUnavailable
39 | )
40 |
41 | // Status convert ErrKind to Status
42 | func (e ErrKind) Status() (state Status) {
43 | switch e {
44 | case ErrClientBadType:
45 | return ClientBadType
46 | case ErrClient:
47 | return ClientBadRequest
48 | case ErrClientOverrun:
49 | return ClientOverrun
50 | case ErrClientTimeout:
51 | return ClientTimeout
52 | case ErrClientTooMany:
53 | return ClientTooMany
54 | case ErrConnectionClosed:
55 | return ServerError
56 | case ErrOther:
57 | return ServerError
58 | case ErrResourceClosed:
59 | return ResourceClosed
60 | case ErrResourceConflict:
61 | return ResourceConflict
62 | case ErrResourceNotFound:
63 | return ResourceNotFound
64 | case ErrSecurity:
65 | return ClientForbidden
66 | case ErrServerBusy:
67 | return ServerBusy
68 | case ErrServer:
69 | return ServerError
70 | case ErrSessionClosed:
71 | return SessionClosed
72 | case ErrSessionConflict:
73 | return SessionConflict
74 | case ErrSessionTimeout:
75 | return SessionTimeout
76 | case ErrUnauthorized:
77 | return ClientUnauthorized
78 | case ErrUnsupported:
79 | return Unsupported
80 | case ErrUpstream:
81 | return UpstreamError
82 | case ErrUpstreamNotFound:
83 | return UpstreamNotFound
84 | case ErrUpstreamTimeout:
85 | return UpstreamTimeout
86 | case ErrUpstreamUnavailable:
87 | return UpstreamUnavailable
88 | }
89 | return
90 | }
91 |
92 | // NewError creates a new error struct instance with Kind and included message
93 | func (e ErrKind) NewError(args ...string) error {
94 | return &ErrGuac{
95 | error: fmt.Errorf("%v", strings.Join(args, ", ")),
96 | Status: e.Status(),
97 | Kind: e,
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/guac/stream_conn_test.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "net"
7 | "testing"
8 | "time"
9 | )
10 |
11 | func TestInstructionReader_ReadSome(t *testing.T) {
12 | conn := &fakeConn{
13 | ToRead: []byte("4.copy,2.ab;4.copy"),
14 | }
15 | stream := NewStream(conn, 1*time.Minute)
16 |
17 | ins, err := stream.ReadSome()
18 |
19 | if err != nil {
20 | t.Error("Unexpected error", err)
21 | }
22 | if !bytes.Equal(ins, []byte("4.copy,2.ab;")) {
23 | t.Error("Unexpected bytes returned")
24 | }
25 | if !stream.Available() {
26 | t.Error("Stream has more available but returned false")
27 | }
28 |
29 | // Read the rest of the fragmented instruction
30 | n := copy(conn.ToRead, ",2.ab;")
31 | conn.ToRead = conn.ToRead[:n]
32 | conn.HasRead = false
33 | ins, err = stream.ReadSome()
34 |
35 | if err != nil {
36 | t.Error("Unexpected error", err)
37 | }
38 | if !bytes.Equal(ins, []byte("4.copy,2.ab;")) {
39 | t.Error("Unexpected bytes returned")
40 | }
41 | if stream.Available() {
42 | t.Error("Stream thinks it has more available but doesn't")
43 | }
44 | }
45 |
46 | func TestInstructionReader_Flush(t *testing.T) {
47 | s := NewStream(&fakeConn{}, time.Second)
48 | s.buffer = s.buffer[:4]
49 | s.buffer[0] = '1'
50 | s.buffer[1] = '2'
51 | s.buffer[2] = '3'
52 | s.buffer[3] = '4'
53 | s.buffer = s.buffer[2:]
54 |
55 | s.Flush()
56 |
57 | if s.buffer[0] != '3' && s.buffer[1] != '4' {
58 | t.Error("Unexpected buffer contents:", string(s.buffer[:2]))
59 | }
60 | if len(s.buffer) != 2 {
61 | t.Error("Unexpected length", len(s.buffer))
62 | }
63 | }
64 |
65 | type fakeConn struct {
66 | ToRead []byte
67 | HasRead bool
68 | Closed bool
69 | }
70 |
71 | func (f *fakeConn) Read(b []byte) (n int, err error) {
72 | if f.HasRead {
73 | return 0, io.EOF
74 | } else {
75 | f.HasRead = true
76 | return copy(b, f.ToRead), nil
77 | }
78 | }
79 |
80 | func (f *fakeConn) Write(b []byte) (n int, err error) {
81 | return 0, nil
82 | }
83 |
84 | func (f *fakeConn) Close() error {
85 | f.Closed = true
86 | return nil
87 | }
88 |
89 | func (f *fakeConn) LocalAddr() net.Addr {
90 | return nil
91 | }
92 |
93 | func (f *fakeConn) RemoteAddr() net.Addr {
94 | return nil
95 | }
96 |
97 | func (f *fakeConn) SetDeadline(t time.Time) error {
98 | return nil
99 | }
100 |
101 | func (f *fakeConn) SetReadDeadline(t time.Time) error {
102 | return nil
103 | }
104 |
105 | func (f *fakeConn) SetWriteDeadline(t time.Time) error {
106 | return nil
107 | }
108 |
--------------------------------------------------------------------------------
/frontend/src/libs/clipboard.js:
--------------------------------------------------------------------------------
1 | import Guacamole from 'guacamole-common-js'
2 |
3 | const clipboard = {}
4 |
5 | clipboard.install = (client) => {
6 | clipboard.getLocalClipboard().then(data => clipboard.cache = data)
7 |
8 | window.addEventListener('load', clipboard.update(client), true)
9 | window.addEventListener('copy', clipboard.update(client))
10 | window.addEventListener('cut', clipboard.update(client))
11 | window.addEventListener('focus', e => {
12 | if (e.target === window) {
13 | clipboard.update(client)()
14 | }
15 | }, true)
16 | }
17 |
18 | clipboard.update = client => {
19 | return () => {
20 | clipboard.getLocalClipboard().then(data => {
21 | clipboard.cache = data
22 | clipboard.setRemoteClipboard(client)
23 | })
24 | }
25 | }
26 |
27 | clipboard.setRemoteClipboard = (client) => {
28 | if (!clipboard.cache) {
29 | return
30 | }
31 |
32 | let writer
33 |
34 | const stream = client.createClipboardStream(clipboard.cache.type)
35 |
36 | if (typeof clipboard.cache.data === 'string') {
37 | writer = new Guacamole.StringWriter(stream)
38 | writer.sendText(clipboard.cache.data)
39 | writer.sendEnd()
40 | } else {
41 | writer = new Guacamole.BlobWriter(stream)
42 | writer.oncomplete = function clipboardSent() {
43 | writer.sendEnd()
44 | };
45 | writer.sendBlob(clipboard.cache.data)
46 | }
47 | }
48 |
49 | clipboard.getLocalClipboard = async () => {
50 | if (navigator.clipboard && navigator.clipboard.readText) {
51 | const text = await navigator.clipboard.readText()
52 | return {
53 | type: 'text/plain',
54 | data: text
55 | }
56 | }
57 | }
58 |
59 | clipboard.setLocalClipboard = async (data) => {
60 | if (navigator.clipboard && navigator.clipboard.writeText) {
61 | if (data.type === 'text/plain') {
62 | await navigator.clipboard.writeText(data.data)
63 | }
64 | }
65 | }
66 |
67 | clipboard.onClipboard = (stream, mimetype) => {
68 | let reader
69 |
70 | if (/^text\//.exec(mimetype)) {
71 | reader = new Guacamole.StringReader(stream);
72 |
73 | // Assemble received data into a single string
74 | let data = '';
75 | reader.ontext = text => {
76 | data += text;
77 | }
78 |
79 | // Set clipboard contents once stream is finished
80 | reader.onend = () => {
81 | clipboard.setLocalClipboard({
82 | type: mimetype,
83 | data: data
84 | })
85 | }
86 | } else {
87 | reader = new Guacamole.BlobReader(stream, mimetype);
88 | reader.onend = () => {
89 | clipboard.setLocalClipboard({
90 | type: mimetype,
91 | data: reader.getBlob()
92 | })
93 | }
94 | }
95 | }
96 |
97 | export default clipboard
98 |
--------------------------------------------------------------------------------
/frontend/src/libs/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import {Message, MessageBox} from 'element-ui';
3 |
4 |
5 | // 创建axios实例
6 | const service = axios.create({
7 | //baseURL: 'http://localhost:2222', // api的base_url
8 | timeout: 120000, // 请求超时时间,
9 | // request payload
10 | headers: {
11 | 'Content-Type': 'application/json;charset=UTF-8'
12 | }
13 | // 修改请求数据,去除data.q中为空的数据项,只针对post请求
14 | });
15 |
16 | service.interceptors.request.use(config => {
17 |
18 | return config;
19 | }, error => {
20 |
21 | return Promise.reject(error);
22 | });
23 |
24 |
25 | // http response 拦截器
26 | service.interceptors.response.use(response => {
27 | if (response.status !== 200) {
28 | return false
29 | }
30 | let {code, data, msg} = response.data
31 | if (code === 200) {
32 | if (response.config.method === 'POST' || response.config.method === 'post') {
33 | //Message.success("创建成功")
34 | return true
35 | } else if (response.config.method === 'DELETE' || response.config.method === 'delete') {
36 | //Message.success("删除成功")
37 | return true
38 | } else if (response.config.method === 'PATCH' || response.config.method === 'patch') {
39 | //Message.success("更新成功")
40 | return true
41 | }
42 | return data
43 | } else if (code === 207) { // wps cookie 换取 jwt 错误
44 | //0alert(response)
45 | Message.warning(msg)
46 | return false
47 | } else if (code === 206) { // 参数错误
48 | Message.warning(msg)
49 | return false
50 | } else if (code === 204) { //service 层错误 业务错误
51 | Message.error(msg)
52 | return false
53 | } else if (code === 205) { // 用户中间件错误 缺少jwt token
54 | //需要 *.wps.cn cookie 换取用户信息
55 | //Message.error(msg)
56 | wpsAccountCookieExchangeJwt();
57 |
58 | return false
59 | } else if (code === 203) { // ssh-ark jwt cookie 出错 错误 获取wps 用户错误
60 | //alert(msg)
61 |
62 | //let thisURL = encodeURIComponent(location.href)
63 | //window.location.href = `https://account.wps.cn/login?cb=${encodeURIComponent(location.href)}`
64 | //let to = vueRouter.currentRoute.name
65 | //vueRouter.push({name: "wpsAuth", query: {callback: to}})
66 |
67 | return false
68 | } else {
69 | //alert(response)
70 | Message.warning('json code 位置错误')
71 | return false
72 | }
73 | },
74 | error => {
75 | Message.error(`网络错误`);
76 | return Promise.reject(error.response.data)
77 |
78 | }
79 | )
80 |
81 |
82 | export default service
83 |
84 |
85 | // Want to use async/await? Add the `async` keyword to your outer function/method.
86 | export async function wpsAccountCookieExchangeJwt() {
87 | try {
88 | const {data} = await axios.get('/api/wps/account');
89 | if (data.code === 200) {
90 | return data.data
91 | } else if (data.code === 203) {
92 | //跳转到 认证页面
93 | window.location.href = `https://account.wps.cn/login?cb=${encodeURIComponent(location.href)}`
94 | } else {
95 | console.log(JSON.stringify(data))
96 | MessageBox.alert(data.msg, '提示', {
97 | confirmButtonText: '关闭窗口',
98 | callback: () => {
99 | window.close()
100 | }
101 | })
102 | }
103 | } catch (error) {
104 | //console.error(error);
105 | alert(error)
106 | }
107 | }
--------------------------------------------------------------------------------
/guac/tunnel_pipe.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/base64"
6 | "fmt"
7 | "io"
8 | "log"
9 | )
10 |
11 | // The Guacamole protocol instruction Opcode reserved for arbitrary
12 | // internal use by tunnel implementations. The value of this Opcode is
13 | // guaranteed to be the empty string (""). TunnelPipe implementations may use
14 | // this Opcode for any purpose. It is currently used by the HTTP tunnel to
15 | // mark the end of the HTTP response, and by the WebSocket tunnel to
16 | // transmit the tunnel UUID.
17 | const InternalDataOpcode = ""
18 |
19 | var InternalOpcodeIns = []byte(fmt.Sprint(len(InternalDataOpcode), ".", InternalDataOpcode))
20 |
21 | // InstructionReader provides reading functionality to a Stream
22 | type InstructionReader interface {
23 | // ReadSome returns the next complete guacd message from the stream
24 | ReadSome() ([]byte, error)
25 | // Available returns true if there are bytes buffered in the stream
26 | Available() bool
27 | // Flush resets the internal buffer for reuse
28 | Flush()
29 | }
30 |
31 | // TunnelPipe provides a unique identifier and synchronized access to the InstructionReader and Writer
32 | // associated with a Stream.
33 | type TunnelPipe interface {
34 | // AcquireReader returns a reader to the tunnel if it isn't locked
35 | AcquireReader() InstructionReader
36 | // ReleaseReader releases the lock on the reader
37 | ReleaseReader()
38 | // HasQueuedReaderThreads returns true if there is a reader locked
39 | HasQueuedReaderThreads() bool
40 | // AcquireWriter returns a writer to the tunnel if it isn't locked
41 | AcquireWriter() io.Writer
42 | // ReleaseWriter releases the lock on the writer
43 | ReleaseWriter()
44 | // HasQueuedWriterThreads returns true if there is a writer locked
45 | HasQueuedWriterThreads() bool
46 | // GetUUID returns the uuid of the tunnel
47 | GetUUID() string
48 | // ConnectionId returns the guacd Connection ID of the tunnel
49 | ConnectionID() string
50 | // Close closes the tunnel
51 | Close() error
52 | }
53 |
54 | // Base TunnelPipe implementation which synchronizes access to the underlying reader and writer with locks
55 | type SimpleTunnel struct {
56 | stream *Stream
57 | readerLock CountedLock
58 | writerLock CountedLock
59 | }
60 |
61 | // NewSimpleTunnel creates a new tunnel
62 | func NewSimpleTunnel(stream *Stream) *SimpleTunnel {
63 | return &SimpleTunnel{
64 | stream: stream,
65 | }
66 | }
67 |
68 | // AcquireReader acquires the reader lock
69 | func (t *SimpleTunnel) AcquireReader() InstructionReader {
70 | t.readerLock.Lock()
71 | return t.stream
72 | }
73 |
74 | // ReleaseReader releases the reader
75 | func (t *SimpleTunnel) ReleaseReader() {
76 | t.readerLock.Unlock()
77 | }
78 |
79 | // HasQueuedReaderThreads returns true if more than one goroutine is trying to read
80 | func (t *SimpleTunnel) HasQueuedReaderThreads() bool {
81 | return t.readerLock.HasQueued()
82 | }
83 |
84 | // AcquireWriter locks the writer lock
85 | func (t *SimpleTunnel) AcquireWriter() io.Writer {
86 | t.writerLock.Lock()
87 | return t.stream
88 | }
89 |
90 | // ReleaseWriter releases the writer lock
91 | func (t *SimpleTunnel) ReleaseWriter() {
92 | t.writerLock.Unlock()
93 | }
94 |
95 | // ConnectionID returns the underlying Guacamole connection ID
96 | func (t *SimpleTunnel) ConnectionID() string {
97 | return t.stream.ConnectionID
98 | }
99 |
100 | // HasQueuedWriterThreads returns true if more than one goroutine is trying to write
101 | func (t *SimpleTunnel) HasQueuedWriterThreads() bool {
102 | return t.writerLock.HasQueued()
103 | }
104 |
105 | // Close closes the underlying stream
106 | func (t *SimpleTunnel) Close() (err error) {
107 | return t.stream.Close()
108 | }
109 |
110 | // GetUUID returns the tunnel's UUID
111 | func (t *SimpleTunnel) GetUUID() string {
112 | /**
113 | * The UUID associated with this tunnel. Every tunnel must have a
114 | * corresponding UUID such that tunnel read/write requests can be
115 | * directed to the proper tunnel.
116 | */
117 | data := make([]byte, 32)
118 | _, err := io.ReadFull(rand.Reader, data)
119 | if err != nil {
120 | log.Println(err)
121 | return ""
122 | }
123 | return base64.RawURLEncoding.EncodeToString(data)
124 | }
125 |
--------------------------------------------------------------------------------
/frontend/src/libs/store.js:
--------------------------------------------------------------------------------
1 | import Vuex from 'vuex';
2 | import Vue from 'vue';
3 | import service from "@/libs/request";
4 |
5 | Vue.use(Vuex);
6 |
7 |
8 | const menuUser = [
9 | {
10 | txt: '个人中心',
11 | icon: 'el-icon-c-scale-to-original',
12 | subs: [
13 | {path: '/requisition-my', txt: '我的工单', name: 'requisitionMy'},
14 | {path: '/requisition-todo', txt: '我的审批', name: 'requisitionTodo'},
15 | //{path: '/machine-my', txt: '我的机器', name: 'machineMy'},
16 | ]
17 | },
18 | ]
19 | const menuAdmin = [
20 | // {
21 | // txt: '配置中心',
22 | // icon: 'el-icon-setting',
23 | // subs: [{path: '/approve-flow', txt: '授权管理', name: 'listApproveFlow'}]
24 | // },
25 | {
26 | txt: '个人中心',
27 | icon: 'el-icon-c-scale-to-original',
28 | subs: [
29 | {path: '/requisition-my', txt: '我的工单', name: 'requisitionMy'},
30 | {path: '/requisition-todo', txt: '我的审批', name: 'requisitionTodo'},
31 | // {path: '/machine-my', txt: '我的机器', name: 'machineMy'},
32 | ]
33 | },
34 | {
35 | txt: '系统管理',
36 | icon: 'el-icon-camera',
37 | subs: [
38 | {path: '/approve-flow', txt: '审批配置', name: 'listApproveFlow'},
39 | {path: '/user', txt: '用户管理', name: 'user'},
40 | //{path: '/machine-user', txt: '机器管理', name: 'machineUser'},
41 | {path: '/requisition', txt: '工单审计', name: 'requisition'},
42 | //{path: '/session-live', txt: '实时会话', name: 'sessionLive'},
43 | //{path: '/session-log', txt: '日志审计', name: 'sessionLog'},
44 | ]
45 | },
46 | ]
47 |
48 |
49 | export default new Vuex.Store({
50 | state: {
51 | userPage: null,
52 | meta: null,
53 | user: null,
54 | todoCount: 0,//待审批信息数量
55 | },
56 | getters: {
57 | getWhiteRules(state) {
58 | let meta = state.meta
59 | if (!meta) {
60 | return []
61 | }
62 | return meta.rule_white
63 | },
64 | getBlackRules(state) {
65 | let meta = state.meta
66 | if (!meta) {
67 | return []
68 | }
69 | return meta.rule_black
70 | },
71 | getUserList(state) {
72 | let up = state.userPage
73 | if (!up) {
74 | return []
75 | }
76 | return state.userPage.list || []
77 | },
78 | getVersion(state) {
79 | return state.meta ? state.meta.version : []
80 | },
81 | getRequisitionStatus(state) {
82 | return state.meta ? state.meta.requisitionStatus : null
83 | },
84 | getTodoCount(state) {
85 | return state.todoCount
86 | },
87 | getUser(statue) {
88 | return statue.user
89 | },
90 | isAdmin(state, getters) {
91 | let user = getters.getUser
92 | return user ? user.role === 'admin' : false
93 | },
94 | getNavi(state, getters) {
95 | let user = getters.getUser
96 | if (user && user.role === 'admin') {
97 | return menuAdmin
98 | }
99 | return menuUser
100 | }
101 | },
102 | mutations: {
103 | setUserPage(state, page) {
104 | state.userPage = page
105 | },
106 | setMeta(state, obj) {
107 | state.meta = obj
108 | },
109 | setUser(state, obj) {
110 | state.user = obj
111 | },
112 | setTodoCount(state, obj) {
113 | state.todoCount = obj
114 | }
115 | },
116 | actions: {
117 | fetchMeta({commit}) {
118 | service.get("/api/meta").then(res => {
119 | if (res) {
120 | commit('setMeta', res)
121 | }
122 | })
123 | },
124 | fetchUserList({commit}, q) {
125 | service.get("/api/user", {params: q}).then(res => {
126 | commit('setUserPage', res)
127 | })
128 | },
129 | fetchRequisitionTodoCount({commit}) {
130 | service.get("/api/requisition-todo-count").then(res => {
131 | if (Number.isInteger(res)) {
132 | commit('setTodoCount', res)
133 | }
134 | })
135 | }
136 | },
137 | });
138 |
--------------------------------------------------------------------------------
/guac/guac_instruction.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | )
7 |
8 | //The Guacamole protocol consists of instructions. Each instruction is a comma-delimited list followed by a terminating semicolon, where the first element of the list is the instruction opcode, and all following elements are the arguments for that instruction:
9 | //
10 | //OPCODE,ARG1,ARG2,ARG3,...;
11 | //Each element of the list has a positive decimal integer length prefix separated by the value of the element by a period. This length denotes the number of Unicode characters in the value of the element, which is encoded in UTF-8:
12 | //
13 | //LENGTH.VALUE
14 | //Any number of complete instructions make up a message which is sent from client to server or from server to client. Client to server instructions are generally control instructions (for connecting or disconnecting) and events (mouse and keyboard). Server to client instructions are generally drawing instructions (caching, clipping, drawing images), using the client as a remote display.
15 | //
16 | //For example, a complete and valid instruction for setting the display size to 1024x768 would be:
17 | //
18 | //4.size,1.0,4.1024,3.768;
19 | //Here, the instruction would be decoded into four elements: "size", the opcode of the size instruction, "0", the index of the default layer, "1024", the desired width in pixels, and "768", the desired height in pixels.
20 | //
21 | //The structure of the Guacamole protocol is important as it allows the protocol to be streamed while also being easily parsable by JavaScript. JavaScript does have native support for conceptually-similar structures like XML or JSON, but neither of those formats is natively supported in a way that can be streamed; JavaScript requires the entirety of the XML or JSON message to be available at the time of decoding. The Guacamole protocol, on the other hand, can be parsed as it is received, and the presence of length prefixes within each instruction element means that the parser can quickly skip around from instruction to instruction without having to iterate over every character.
22 |
23 | // Instruction represents a Guacamole instruction
24 | type Instruction struct {
25 | Opcode string
26 | Args []string
27 | cache string
28 | }
29 |
30 | // NewInstruction creates an instruction
31 | func NewInstruction(opcode string, args ...string) *Instruction {
32 | return &Instruction{
33 | Opcode: opcode,
34 | Args: args,
35 | }
36 | }
37 |
38 | // String returns the on-wire representation of the instruction
39 | func (i *Instruction) String() string {
40 | if len(i.cache) > 0 {
41 | return i.cache
42 | }
43 |
44 | i.cache = fmt.Sprintf("%d.%s", len(i.Opcode), i.Opcode)
45 | for _, value := range i.Args {
46 | i.cache += fmt.Sprintf(",%d.%s", len(value), value)
47 | }
48 | i.cache += ";"
49 |
50 | return i.cache
51 | }
52 |
53 | func (i *Instruction) Byte() []byte {
54 | return []byte(i.String())
55 | }
56 |
57 | //Parse 解析data 到 guacd instruction todo:: 优化这个算法可以 提高net.io
58 | func Parse(data []byte) (*Instruction, error) {
59 | elementStart := 0
60 |
61 | // Build list of elements
62 | elements := make([]string, 0, 1)
63 | for elementStart < len(data) {
64 | // Find end of length
65 | lengthEnd := -1
66 | for i := elementStart; i < len(data); i++ {
67 | if data[i] == '.' {
68 | lengthEnd = i
69 | break
70 | }
71 | }
72 | // read() is required to return a complete instruction. If it does
73 | // not, this is a severe internal error.
74 | if lengthEnd == -1 {
75 | return nil, ErrServer.NewError("ReadSome returned incomplete instruction.")
76 | }
77 |
78 | // Parse length
79 | length, e := strconv.Atoi(string(data[elementStart:lengthEnd]))
80 | if e != nil {
81 | return nil, ErrServer.NewError("ReadSome returned wrong pattern instruction.", e.Error())
82 | }
83 |
84 | // Parse element from just after period
85 | elementStart = lengthEnd + 1
86 | element := string(data[elementStart : elementStart+length])
87 |
88 | // Append element to list of elements
89 | elements = append(elements, element)
90 |
91 | // ReadSome terminator after element
92 | elementStart += length
93 | terminator := data[elementStart]
94 |
95 | // Continue reading instructions after terminator
96 | elementStart++
97 |
98 | // If we've reached the end of the instruction
99 | if terminator == ';' {
100 | break
101 | }
102 |
103 | }
104 |
105 | return NewInstruction(elements[0], elements[1:]...), nil
106 | }
107 |
108 | // ReadOne takes an instruction from the stream and parses it into an Instruction
109 | func ReadOne(stream *Stream) (instruction *Instruction, err error) {
110 | var instructionBuffer []byte
111 | instructionBuffer, err = stream.ReadSome()
112 | if err != nil {
113 | return
114 | }
115 |
116 | return Parse(instructionBuffer)
117 | }
118 |
--------------------------------------------------------------------------------
/api_ws_guaca.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "github.com/gin-gonic/gin"
8 | "github.com/gorilla/websocket"
9 | "github.com/sirupsen/logrus"
10 | "golang.org/x/sync/errgroup"
11 | "net/http"
12 | "rdpgo/guac"
13 | )
14 |
15 | type ReqArg struct {
16 | GuacadAddr string `form:"guacad_addr"`
17 | AssetProtocol string `form:"asset_protocol"`
18 | AssetHost string `form:"asset_host"`
19 | AssetPort string `form:"asset_port"`
20 | AssetUser string `form:"asset_user"`
21 | AssetPassword string `form:"asset_password"`
22 | ScreenWidth int `form:"screen_width"`
23 | ScreenHeight int `form:"screen_height"`
24 | ScreenDpi int `form:"screen_dpi"`
25 | }
26 |
27 | //ApiWsGuacamole websocket 转 guacamole协议
28 | func ApiWsGuacamole() gin.HandlerFunc {
29 | //0. 初始化 websocket 配置
30 | websocketReadBufferSize := guac.MaxGuacMessage
31 | websocketWriteBufferSize := guac.MaxGuacMessage * 2
32 | upgrade := websocket.Upgrader{
33 | ReadBufferSize: websocketReadBufferSize,
34 | WriteBufferSize: websocketWriteBufferSize,
35 | CheckOrigin: func(r *http.Request) bool {
36 | //检查origin 限定websocket 被其他的域名访问
37 | return true
38 | },
39 | }
40 | return func(c *gin.Context) {
41 | //1. 解析参数, 因为 websocket 只能个通过浏览器url,request-header,cookie 传参数, 这里之接收 url-query 参数.
42 | logrus.Println("1. 解析参数, 因为 websocket 只能个通过浏览器url,request-header,cookie 传参数, 这里之接收 url-query 参数.")
43 |
44 | arg := new(ReqArg)
45 | err := c.BindQuery(arg)
46 | if err != nil {
47 | c.JSON(202, err.Error())
48 | return
49 | }
50 |
51 | //2. 设置为http-get websocket 升级
52 | logrus.Println("2. 设置为http-get websocket 升级")
53 |
54 | protocol := c.Request.Header.Get("Sec-Websocket-Protocol")
55 | ws, err := upgrade.Upgrade(c.Writer, c.Request, http.Header{
56 | "Sec-Websocket-Protocol": {protocol},
57 | })
58 | if err != nil {
59 | logrus.WithError(err).Error("升级ws失败")
60 | return
61 | }
62 | defer func() {
63 | if err = ws.Close(); err != nil {
64 | logrus.Traceln("Error closing websocket", err)
65 | }
66 | }()
67 |
68 | //3. 开始使用参数连接RDP远程桌面资产
69 | logrus.Println("3. 开始使用参数连接RDP远程桌面资产, 对应guacamole protocol 文档的handshake章节")
70 | uid := ""
71 |
72 | pipeTunnel, err := guac.NewGuacamoleTunnel(arg.GuacadAddr, arg.AssetProtocol, arg.AssetHost, arg.AssetPort, arg.AssetUser, arg.AssetPassword, uid, arg.ScreenWidth, arg.ScreenHeight, arg.ScreenDpi)
73 | if err != nil {
74 | logrus.Error("Failed to upgrade websocket", err)
75 | return
76 | }
77 | defer func() {
78 | if err = pipeTunnel.Close(); err != nil {
79 | logrus.Traceln("Error closing pipeTunnel", err)
80 | }
81 | }()
82 | //4. 开始处理 guacad-tunnel的io(reader,writer)
83 | logrus.Println("4. 开始处理 guacad-tunnel的io(reader,writer)")
84 | //id := pipeTunnel.ConnectionID()
85 |
86 | ioCopy(ws, pipeTunnel)
87 | logrus.Info("websocket session end")
88 | }
89 | }
90 |
91 | func ioCopy(ws *websocket.Conn, tunnl *guac.SimpleTunnel) {
92 |
93 | writer := tunnl.AcquireWriter()
94 | reader := tunnl.AcquireReader()
95 | //if pipeTunnel.OnDisconnectWs != nil {
96 | // defer pipeTunnel.OnDisconnectWs(id, ws, c.Request, pipeTunnel.TunnelPipe)
97 | //}
98 | defer tunnl.ReleaseWriter()
99 | defer tunnl.ReleaseReader()
100 |
101 | //使用 errgroup 来处理(管理) goroutine for-loop, 防止 for-goroutine zombie
102 | eg, _ := errgroup.WithContext(context.Background())
103 |
104 | eg.Go(func() error {
105 | buf := bytes.NewBuffer(make([]byte, 0, guac.MaxGuacMessage*2))
106 |
107 | for {
108 | ins, err := reader.ReadSome()
109 | if err != nil {
110 | return err
111 | }
112 |
113 | if bytes.HasPrefix(ins, guac.InternalOpcodeIns) {
114 | // messages starting with the InternalDataOpcode are never sent to the websocket
115 | continue
116 | }
117 |
118 | if _, err = buf.Write(ins); err != nil {
119 | return err
120 | }
121 |
122 | // if the buffer has more data in it or we've reached the max buffer size, send the data and reset
123 | if !reader.Available() || buf.Len() >= guac.MaxGuacMessage {
124 | if err = ws.WriteMessage(1, buf.Bytes()); err != nil {
125 | if err == websocket.ErrCloseSent {
126 | return fmt.Errorf("websocket:%v", err)
127 | }
128 | logrus.Traceln("Failed sending message to ws", err)
129 | return err
130 | }
131 | buf.Reset()
132 | }
133 | }
134 |
135 | })
136 | eg.Go(func() error {
137 | for {
138 | _, data, err := ws.ReadMessage()
139 | if err != nil {
140 | logrus.Traceln("Error reading message from ws", err)
141 | return err
142 | }
143 | if bytes.HasPrefix(data, guac.InternalOpcodeIns) {
144 | // messages starting with the InternalDataOpcode are never sent to guacd
145 | continue
146 | }
147 | if _, err = writer.Write(data); err != nil {
148 | logrus.Traceln("Failed writing to guacd", err)
149 | return err
150 | }
151 | }
152 |
153 | })
154 | if err := eg.Wait(); err != nil {
155 | logrus.WithError(err).Error("session-err")
156 | }
157 |
158 | }
159 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
5 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
6 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
7 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
8 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
9 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
10 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
11 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
12 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
13 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
14 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
15 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
16 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
17 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
18 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
19 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
20 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
21 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
22 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
23 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
24 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
25 | github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
26 | github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
27 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
28 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
29 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
30 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
31 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
32 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
35 | github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
36 | github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
38 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
39 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
40 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
41 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
42 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
43 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
44 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
45 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
46 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
47 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
48 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
49 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
50 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
51 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
52 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
55 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
56 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
57 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
58 |
--------------------------------------------------------------------------------
/guac/status.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | type Status int
4 |
5 | const (
6 | // Undefined Add to instead null
7 | Undefined Status = -1
8 |
9 | // Success indicates the operation succeeded.
10 | Success Status = iota
11 |
12 | // Unsupported indicates the requested operation is unsupported.
13 | Unsupported
14 |
15 | // ServerError indicates the operation could not be performed due to an internal failure.
16 | ServerError
17 |
18 | // ServerBusy indicates the operation could not be performed as the server is busy.
19 | ServerBusy
20 |
21 | // UpstreamTimeout indicates the operation could not be performed because the upstream server is not responding.
22 | UpstreamTimeout
23 |
24 | // UpstreamError indicates the operation was unsuccessful due to an error or otherwise unexpected
25 | // condition of the upstream server.
26 | UpstreamError
27 |
28 | // ResourceNotFound indicates the operation could not be performed as the requested resource does not exist.
29 | ResourceNotFound
30 |
31 | // ResourceConflict indicates the operation could not be performed as the requested resource is already in use.
32 | ResourceConflict
33 |
34 | // ResourceClosed indicates the operation could not be performed as the requested resource is now closed.
35 | ResourceClosed
36 |
37 | // UpstreamNotFound indicates the operation could not be performed because the upstream server does
38 | // not appear to exist.
39 | UpstreamNotFound
40 |
41 | // UpstreamUnavailable indicates the operation could not be performed because the upstream server is not
42 | // available to service the request.
43 | UpstreamUnavailable
44 |
45 | // SessionConflict indicates the session within the upstream server has ended because it conflicted
46 | // with another session.
47 | SessionConflict
48 |
49 | // SessionTimeout indicates the session within the upstream server has ended because it appeared to be inactive.
50 | SessionTimeout
51 |
52 | // SessionClosed indicates the session within the upstream server has been forcibly terminated.
53 | SessionClosed
54 |
55 | // ClientBadRequest indicates the operation could not be performed because bad parameters were given.
56 | ClientBadRequest
57 |
58 | // ClientUnauthorized indicates the user is not authorized.
59 | ClientUnauthorized
60 |
61 | // ClientForbidden indicates the user is not allowed to do the operation.
62 | ClientForbidden
63 |
64 | // ClientTimeout indicates the client took too long to respond.
65 | ClientTimeout
66 |
67 | // ClientOverrun indicates the client sent too much data.
68 | ClientOverrun
69 |
70 | // ClientBadType indicates the client sent data of an unsupported or unexpected type.
71 | ClientBadType
72 |
73 | // ClientTooMany indivates the operation failed because the current client is already using too many resources.
74 | ClientTooMany
75 | )
76 |
77 | type statusData struct {
78 | name string
79 | // The most applicable HTTP error code.
80 | httpCode int
81 |
82 | // The most applicable WebSocket error code.
83 | websocketCode int
84 |
85 | // The Guacamole protocol Status code.
86 | guacCode int
87 | }
88 |
89 | func newStatusData(name string, httpCode, websocketCode, guacCode int) (ret statusData) {
90 | ret.name = name
91 | ret.httpCode = httpCode
92 | ret.websocketCode = websocketCode
93 | ret.guacCode = guacCode
94 | return
95 | }
96 |
97 | var guacamoleStatusMap = map[Status]statusData{
98 | Success: newStatusData("Success", 200, 1000, 0x0000),
99 | Unsupported: newStatusData("Unsupported", 501, 1011, 0x0100),
100 | ServerError: newStatusData("SERVER_ERROR", 500, 1011, 0x0200),
101 | ServerBusy: newStatusData("SERVER_BUSY", 503, 1008, 0x0201),
102 | UpstreamTimeout: newStatusData("UPSTREAM_TIMEOUT", 504, 1011, 0x0202),
103 | UpstreamError: newStatusData("UPSTREAM_ERROR", 502, 1011, 0x0203),
104 | ResourceNotFound: newStatusData("RESOURCE_NOT_FOUND", 404, 1002, 0x0204),
105 | ResourceConflict: newStatusData("RESOURCE_CONFLICT", 409, 1008, 0x0205),
106 | ResourceClosed: newStatusData("RESOURCE_CLOSED", 404, 1002, 0x0206),
107 | UpstreamNotFound: newStatusData("UPSTREAM_NOT_FOUND", 502, 1011, 0x0207),
108 | UpstreamUnavailable: newStatusData("UPSTREAM_UNAVAILABLE", 502, 1011, 0x0208),
109 | SessionConflict: newStatusData("SESSION_CONFLICT", 409, 1008, 0x0209),
110 | SessionTimeout: newStatusData("SESSION_TIMEOUT", 408, 1002, 0x020A),
111 | SessionClosed: newStatusData("SESSION_CLOSED", 404, 1002, 0x020B),
112 | ClientBadRequest: newStatusData("CLIENT_BAD_REQUEST", 400, 1002, 0x0300),
113 | ClientUnauthorized: newStatusData("CLIENT_UNAUTHORIZED", 403, 1008, 0x0301),
114 | ClientForbidden: newStatusData("CLIENT_FORBIDDEN", 403, 1008, 0x0303),
115 | ClientTimeout: newStatusData("CLIENT_TIMEOUT", 408, 1002, 0x0308),
116 | ClientOverrun: newStatusData("CLIENT_OVERRUN", 413, 1009, 0x030D),
117 | ClientBadType: newStatusData("CLIENT_BAD_TYPE", 415, 1003, 0x030F),
118 | ClientTooMany: newStatusData("CLIENT_TOO_MANY", 429, 1008, 0x031D),
119 | }
120 |
121 | // String returns the name of the status.
122 | func (s Status) String() string {
123 | if v, ok := guacamoleStatusMap[s]; ok {
124 | return v.name
125 | }
126 | return ""
127 | }
128 |
129 | // GetHTTPStatusCode returns the most applicable HTTP error code.
130 | func (s Status) GetHTTPStatusCode() int {
131 | if v, ok := guacamoleStatusMap[s]; ok {
132 | return v.httpCode
133 | }
134 | return -1
135 | }
136 |
137 | // GetWebSocketCode returns the most applicable HTTP error code.
138 | func (s Status) GetWebSocketCode() int {
139 | if v, ok := guacamoleStatusMap[s]; ok {
140 | return v.websocketCode
141 | }
142 | return -1
143 | }
144 |
145 | // GetGuacamoleStatusCode returns the corresponding Guacamole protocol Status code.
146 | func (s Status) GetGuacamoleStatusCode() int {
147 | if v, ok := guacamoleStatusMap[s]; ok {
148 | return v.guacCode
149 | }
150 | return -1
151 | }
152 |
153 | // FromGuacamoleStatusCode returns the Status corresponding to the given Guacamole protocol Status code.
154 | func FromGuacamoleStatusCode(code int) (ret Status) {
155 | // Search for a Status having the given Status code
156 | for k, v := range guacamoleStatusMap {
157 | if v.guacCode == code {
158 | ret = k
159 | return
160 | }
161 | }
162 | // No such Status found
163 | ret = Undefined
164 | return
165 |
166 | }
167 |
--------------------------------------------------------------------------------
/guac/stream_conn.go:
--------------------------------------------------------------------------------
1 | package guac
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "time"
7 |
8 | "github.com/sirupsen/logrus"
9 | )
10 |
11 | const (
12 | SocketTimeout = 15 * time.Second
13 | MaxGuacMessage = 8192
14 | )
15 |
16 | // Stream wraps the connection to Guacamole providing timeouts and reading
17 | // a single instruction at a time (since returning partial instructions
18 | // would be an error)
19 | type Stream struct {
20 | conn net.Conn
21 | ConnectionID string // ConnectionID is the ID Guacamole gives and can be used to reconnect or share sessions
22 | parseStart int // if more than a single instruction is read, the rest are buffered here
23 | buffer []byte
24 | reset []byte
25 | timeout time.Duration
26 | }
27 |
28 | // NewStream creates a new stream
29 | func NewStream(conn net.Conn, timeout time.Duration) (ret *Stream) {
30 | buffer := make([]byte, 0, MaxGuacMessage*3)
31 | return &Stream{
32 | conn: conn,
33 | timeout: timeout,
34 | buffer: buffer,
35 | reset: buffer[:cap(buffer)],
36 | }
37 | }
38 |
39 | // Write sends messages to Guacamole with a timeout
40 | func (s *Stream) Write(data []byte) (n int, err error) {
41 | if err = s.conn.SetWriteDeadline(time.Now().Add(s.timeout)); err != nil {
42 | logrus.Error(err)
43 | return
44 | }
45 | return s.conn.Write(data)
46 | }
47 |
48 | // Available returns true if there are messages buffered
49 | func (s *Stream) Available() bool {
50 | return len(s.buffer) > 0
51 | }
52 |
53 | // Flush resets the internal buffer
54 | func (s *Stream) Flush() {
55 | copy(s.reset, s.buffer)
56 | s.buffer = s.reset[:len(s.buffer)]
57 | }
58 |
59 | // ReadSome takes the next instruction (from the network or from the buffer) and returns it.
60 | // io.Reader is not implemented because this seems like the right place to maintain a buffer.
61 | func (s *Stream) ReadSome() (instruction []byte, err error) {
62 | if err = s.conn.SetReadDeadline(time.Now().Add(s.timeout)); err != nil {
63 | logrus.Error(err)
64 | return
65 | }
66 |
67 | var n int
68 | // While we're blocking, or input is available
69 | for {
70 | // Length of element
71 | var elementLength int
72 |
73 | // Resume where we left off
74 | i := s.parseStart
75 |
76 | parseLoop:
77 | // Parse instruction in buffer
78 | for i < len(s.buffer) {
79 | // ReadSome character
80 | readChar := s.buffer[i]
81 | i++
82 |
83 | switch readChar {
84 | // If digit, update length
85 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
86 | elementLength = elementLength*10 + int(readChar-'0')
87 |
88 | // If not digit, check for end-of-length character
89 | case '.':
90 | if i+elementLength >= len(s.buffer) {
91 | // break for i < s.usedLength { ... }
92 | // Otherwise, read more data
93 | break parseLoop
94 | }
95 | // Check if element present in buffer
96 | terminator := s.buffer[i+elementLength]
97 | // Move to character after terminator
98 | i += elementLength + 1
99 |
100 | // Reset length
101 | elementLength = 0
102 |
103 | // Continue here if necessary
104 | s.parseStart = i
105 |
106 | // If terminator is semicolon, we have a full
107 | // instruction.
108 | switch terminator {
109 | case ';':
110 | instruction = s.buffer[0:i]
111 | s.parseStart = 0
112 | s.buffer = s.buffer[i:]
113 | return
114 | case ',':
115 | // keep going
116 | default:
117 | err = ErrServer.NewError("Element terminator of instruction was not ';' nor ','")
118 | return
119 | }
120 | default:
121 | // Otherwise, parse error
122 | err = ErrServer.NewError("Non-numeric character in element length:", string(readChar))
123 | return
124 | }
125 | }
126 |
127 | if cap(s.buffer) < MaxGuacMessage {
128 | s.Flush()
129 | }
130 |
131 | n, err = s.conn.Read(s.buffer[len(s.buffer):cap(s.buffer)])
132 | if err != nil && n == 0 {
133 | switch err.(type) {
134 | case net.Error:
135 | ex := err.(net.Error)
136 | if ex.Timeout() {
137 | err = ErrUpstreamTimeout.NewError("Connection to guacd timed out.", err.Error())
138 | } else {
139 | err = ErrConnectionClosed.NewError("Connection to guacd is closed.", err.Error())
140 | }
141 | default:
142 | err = ErrServer.NewError(err.Error())
143 | }
144 | return
145 | }
146 | if n == 0 {
147 | err = ErrServer.NewError("read 0 bytes")
148 | }
149 | // must reslice so len is changed
150 | s.buffer = s.buffer[:len(s.buffer)+n]
151 | }
152 | }
153 |
154 | // Close closes the underlying network connection
155 | func (s *Stream) Close() error {
156 | return s.conn.Close()
157 | }
158 |
159 | // Handshake configures the guacd session
160 | func (s *Stream) Handshake(config *Config) error {
161 | // Get protocol / connection ID
162 | selectArg := config.ConnectionID
163 | if len(selectArg) == 0 {
164 | selectArg = config.Protocol
165 | }
166 |
167 | // Send requested protocol or connection ID
168 | _, err := s.Write(NewInstruction("select", selectArg).Byte())
169 | if err != nil {
170 | return err
171 | }
172 |
173 | // Wait for server Args
174 | args, err := s.AssertOpcode("args")
175 | if err != nil {
176 | return err
177 | }
178 |
179 | // Build Args list off provided names and config
180 | argNameS := args.Args
181 | argValueS := make([]string, 0, len(argNameS))
182 | for _, argName := range argNameS {
183 |
184 | // Retrieve argument name
185 |
186 | // Get defined value for name
187 | value := config.Parameters[argName]
188 |
189 | // If value defined, set that value
190 | if len(value) == 0 {
191 | value = ""
192 | }
193 | argValueS = append(argValueS, value)
194 | }
195 |
196 | // Send size
197 | _, err = s.Write(NewInstruction("size",
198 | fmt.Sprintf("%v", config.OptimalScreenWidth),
199 | fmt.Sprintf("%v", config.OptimalScreenHeight),
200 | fmt.Sprintf("%v", config.OptimalResolution)).Byte(),
201 | )
202 |
203 | if err != nil {
204 | return err
205 | }
206 |
207 | // Send supported audio formats
208 | _, err = s.Write(NewInstruction("audio", config.AudioMimetypes...).Byte())
209 | if err != nil {
210 | return err
211 | }
212 |
213 | // Send supported video formats
214 | _, err = s.Write(NewInstruction("video", config.VideoMimetypes...).Byte())
215 | if err != nil {
216 | return err
217 | }
218 |
219 | // Send supported image formats
220 | _, err = s.Write(NewInstruction("image", config.ImageMimetypes...).Byte())
221 | if err != nil {
222 | return err
223 | }
224 |
225 | // Send Args
226 | _, err = s.Write(NewInstruction("connect", argValueS...).Byte())
227 | if err != nil {
228 | return err
229 | }
230 |
231 | // Wait for ready, store ID
232 | ready, err := s.AssertOpcode("ready")
233 | if err != nil {
234 | return err
235 | }
236 |
237 | readyArgs := ready.Args
238 | if len(readyArgs) == 0 {
239 | err = ErrServer.NewError("No connection ID received")
240 | return err
241 | }
242 |
243 | s.Flush()
244 | s.ConnectionID = readyArgs[0]
245 |
246 | return nil
247 | }
248 |
249 | // AssertOpcode checks the next opcode in the stream matches what is expected. Useful during handshake.
250 | func (s *Stream) AssertOpcode(opcode string) (instruction *Instruction, err error) {
251 | instruction, err = ReadOne(s)
252 | if err != nil {
253 | return
254 | }
255 |
256 | if len(instruction.Opcode) == 0 {
257 | err = ErrServer.NewError("End of stream while waiting for \"" + opcode + "\".")
258 | return
259 | }
260 |
261 | if instruction.Opcode != opcode {
262 | err = ErrServer.NewError("Expected \"" + opcode + "\" instruction but instead received \"" + instruction.Opcode + "\".")
263 | return
264 | }
265 | return
266 | }
267 |
--------------------------------------------------------------------------------
/frontend/src/libs/GuacMouse.js:
--------------------------------------------------------------------------------
1 | import Guacamole from 'guacamole-common-js'
2 |
3 | const mouse = function (element) {
4 |
5 | /**
6 | * Reference to this Guacamole.Mouse.
7 | * @private
8 | */
9 | let guac_mouse = this;
10 |
11 | /**
12 | * The number of mousemove events to require before re-enabling mouse
13 | * event handling after receiving a touch event.
14 | */
15 | this.touchMouseThreshold = 3;
16 |
17 | /**
18 | * The minimum amount of pixels scrolled required for a single scroll button
19 | * click.
20 | */
21 | this.scrollThreshold = 53;
22 |
23 | /**
24 | * The number of pixels to scroll per line.
25 | */
26 | this.PIXELS_PER_LINE = 18;
27 |
28 | /**
29 | * The number of pixels to scroll per page.
30 | */
31 | this.PIXELS_PER_PAGE = this.PIXELS_PER_LINE * 16;
32 |
33 | /**
34 | * The current mouse state. The properties of this state are updated when
35 | * mouse events fire. This state object is also passed in as a parameter to
36 | * the handler of any mouse events.
37 | *
38 | * @type {Guacamole.Mouse.State}
39 | */
40 | this.currentState = new Guacamole.Mouse.State(
41 | 0, 0,
42 | false, false, false, false, false
43 | );
44 |
45 | /**
46 | * Fired whenever the user presses a mouse button down over the element
47 | * associated with this Guacamole.Mouse.
48 | *
49 | * @event
50 | * @param {Guacamole.Mouse.State} state The current mouse state.
51 | */
52 | this.onmousedown = null;
53 |
54 | /**
55 | * Fired whenever the user releases a mouse button down over the element
56 | * associated with this Guacamole.Mouse.
57 | *
58 | * @event
59 | * @param {Guacamole.Mouse.State} state The current mouse state.
60 | */
61 | this.onmouseup = null;
62 |
63 | /**
64 | * Fired whenever the user moves the mouse over the element associated with
65 | * this Guacamole.Mouse.
66 | *
67 | * @event
68 | * @param {Guacamole.Mouse.State} state The current mouse state.
69 | */
70 | this.onmousemove = null;
71 |
72 | /**
73 | * Fired whenever the mouse leaves the boundaries of the element associated
74 | * with this Guacamole.Mouse.
75 | *
76 | * @event
77 | */
78 | this.onmouseout = null;
79 |
80 | /**
81 | * Counter of mouse events to ignore. This decremented by mousemove, and
82 | * while non-zero, mouse events will have no effect.
83 | * @private
84 | */
85 | var ignore_mouse = 0;
86 |
87 | /**
88 | * Cumulative scroll delta amount. This value is accumulated through scroll
89 | * events and results in scroll button clicks if it exceeds a certain
90 | * threshold.
91 | *
92 | * @private
93 | */
94 | var scroll_delta = 0;
95 |
96 | function cancelEvent(e) {
97 | e.stopPropagation();
98 | if (e.preventDefault) e.preventDefault();
99 | e.returnValue = false;
100 | }
101 |
102 | // Block context menu so right-click gets sent properly
103 | element.addEventListener("contextmenu", function (e) {
104 | cancelEvent(e);
105 | }, false);
106 |
107 | element.addEventListener("mousemove", function (e) {
108 |
109 | // If ignoring events, decrement counter
110 | if (ignore_mouse) {
111 | ignore_mouse--;
112 | return;
113 | }
114 |
115 | guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY);
116 |
117 | if (guac_mouse.onmousemove)
118 | guac_mouse.onmousemove(guac_mouse.currentState);
119 |
120 | }, false);
121 |
122 | element.addEventListener("mousedown", function (e) {
123 |
124 | cancelEvent(e);
125 |
126 | // Do not handle if ignoring events
127 | if (ignore_mouse)
128 | return;
129 |
130 | switch (e.button) {
131 | case 0:
132 | guac_mouse.currentState.left = true;
133 | break;
134 | case 1:
135 | guac_mouse.currentState.middle = true;
136 | break;
137 | case 2:
138 | guac_mouse.currentState.right = true;
139 | break;
140 | }
141 |
142 | if (guac_mouse.onmousedown)
143 | guac_mouse.onmousedown(guac_mouse.currentState);
144 |
145 | }, false);
146 |
147 | element.addEventListener("mouseup", function (e) {
148 |
149 | cancelEvent(e);
150 |
151 | // Do not handle if ignoring events
152 | if (ignore_mouse)
153 | return;
154 |
155 | switch (e.button) {
156 | case 0:
157 | guac_mouse.currentState.left = false;
158 | break;
159 | case 1:
160 | guac_mouse.currentState.middle = false;
161 | break;
162 | case 2:
163 | guac_mouse.currentState.right = false;
164 | break;
165 | }
166 |
167 | if (guac_mouse.onmouseup)
168 | guac_mouse.onmouseup(guac_mouse.currentState);
169 |
170 | }, false);
171 |
172 | element.addEventListener("mouseout", function (e) {
173 |
174 | // Get parent of the element the mouse pointer is leaving
175 | if (!e) e = window.event;
176 |
177 | // Check that mouseout is due to actually LEAVING the element
178 | var target = e.relatedTarget || e.toElement;
179 | while (target) {
180 | if (target === element)
181 | return;
182 | target = target.parentNode;
183 | }
184 |
185 | cancelEvent(e);
186 |
187 | // Release all buttons
188 | if (guac_mouse.currentState.left
189 | || guac_mouse.currentState.middle
190 | || guac_mouse.currentState.right) {
191 |
192 | guac_mouse.currentState.left = false;
193 | guac_mouse.currentState.middle = false;
194 | guac_mouse.currentState.right = false;
195 |
196 | if (guac_mouse.onmouseup)
197 | guac_mouse.onmouseup(guac_mouse.currentState);
198 | }
199 |
200 | // Fire onmouseout event
201 | if (guac_mouse.onmouseout)
202 | guac_mouse.onmouseout();
203 |
204 | }, false);
205 |
206 | // Override selection on mouse event element.
207 | element.addEventListener("selectstart", function (e) {
208 | cancelEvent(e);
209 | }, false);
210 |
211 | // Ignore all pending mouse events when touch events are the apparent source
212 | function ignorePendingMouseEvents() {
213 | ignore_mouse = guac_mouse.touchMouseThreshold;
214 | }
215 |
216 | element.addEventListener("touchmove", ignorePendingMouseEvents, false);
217 | element.addEventListener("touchstart", ignorePendingMouseEvents, false);
218 | element.addEventListener("touchend", ignorePendingMouseEvents, false);
219 |
220 | // Scroll wheel support
221 | function mousewheel_handler(e) {
222 |
223 | // Determine approximate scroll amount (in pixels)
224 | var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta;
225 |
226 | // If successfully retrieved scroll amount, convert to pixels if not
227 | // already in pixels
228 | if (delta) {
229 |
230 | // Convert to pixels if delta was lines
231 | if (e.deltaMode === 1)
232 | delta = e.deltaY * guac_mouse.PIXELS_PER_LINE;
233 |
234 | // Convert to pixels if delta was pages
235 | else if (e.deltaMode === 2)
236 | delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE;
237 |
238 | }
239 |
240 | // Otherwise, assume legacy mousewheel event and line scrolling
241 | else
242 | delta = e.detail * guac_mouse.PIXELS_PER_LINE;
243 |
244 | // Update overall delta
245 | scroll_delta += delta;
246 |
247 | // Up
248 | if (scroll_delta <= -guac_mouse.scrollThreshold) {
249 |
250 | // Repeatedly click the up button until insufficient delta remains
251 | do {
252 |
253 | if (guac_mouse.onmousedown) {
254 | guac_mouse.currentState.up = true;
255 | guac_mouse.onmousedown(guac_mouse.currentState);
256 | }
257 |
258 | if (guac_mouse.onmouseup) {
259 | guac_mouse.currentState.up = false;
260 | guac_mouse.onmouseup(guac_mouse.currentState);
261 | }
262 |
263 | scroll_delta += guac_mouse.scrollThreshold;
264 |
265 | } while (scroll_delta <= -guac_mouse.scrollThreshold);
266 |
267 | // Reset delta
268 | scroll_delta = 0;
269 |
270 | }
271 |
272 | // Down
273 | if (scroll_delta >= guac_mouse.scrollThreshold) {
274 |
275 | // Repeatedly click the down button until insufficient delta remains
276 | do {
277 |
278 | if (guac_mouse.onmousedown) {
279 | guac_mouse.currentState.down = true;
280 | guac_mouse.onmousedown(guac_mouse.currentState);
281 | }
282 |
283 | if (guac_mouse.onmouseup) {
284 | guac_mouse.currentState.down = false;
285 | guac_mouse.onmouseup(guac_mouse.currentState);
286 | }
287 |
288 | scroll_delta -= guac_mouse.scrollThreshold;
289 |
290 | } while (scroll_delta >= guac_mouse.scrollThreshold);
291 |
292 | // Reset delta
293 | scroll_delta = 0;
294 |
295 | }
296 |
297 | cancelEvent(e);
298 |
299 | }
300 |
301 | element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
302 | element.addEventListener('mousewheel', mousewheel_handler, false);
303 | element.addEventListener('wheel', mousewheel_handler, false);
304 |
305 | /**
306 | * Whether the browser supports CSS3 cursor styling, including hotspot
307 | * coordinates.
308 | *
309 | * @private
310 | * @type {Boolean}
311 | */
312 | var CSS3_CURSOR_SUPPORTED = (function () {
313 |
314 | var div = document.createElement("div");
315 |
316 | // If no cursor property at all, then no support
317 | if (!("cursor" in div.style))
318 | return false;
319 |
320 | try {
321 | // Apply simple 1x1 PNG
322 | div.style.cursor = "url(data:image/png;base64,"
323 | + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
324 | + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI"
325 | + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA"
326 | + "AABJRU5ErkJggg==) 0 0, auto";
327 | } catch (e) {
328 | return false;
329 | }
330 |
331 | // Verify cursor property is set to URL with hotspot
332 | return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || "");
333 |
334 | })();
335 |
336 | /**
337 | * Changes the local mouse cursor to the given canvas, having the given
338 | * hotspot coordinates. This affects styling of the element backing this
339 | * Guacamole.Mouse only, and may fail depending on browser support for
340 | * setting the mouse cursor.
341 | *
342 | * If setting the local cursor is desired, it is up to the implementation
343 | * to do something else, such as use the software cursor built into
344 | * Guacamole.Display, if the local cursor cannot be set.
345 | *
346 | * @param {HTMLCanvasElement} canvas The cursor image.
347 | * @param {Number} x The X-coordinate of the cursor hotspot.
348 | * @param {Number} y The Y-coordinate of the cursor hotspot.
349 | * @return {Boolean} true if the cursor was successfully set, false if the
350 | * cursor could not be set for any reason.
351 | */
352 | this.setCursor = function (canvas, x, y) {
353 |
354 | // Attempt to set via CSS3 cursor styling
355 | if (CSS3_CURSOR_SUPPORTED) {
356 | var dataURL = canvas.toDataURL('image/png');
357 | element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto";
358 | return true;
359 | }
360 |
361 | // Otherwise, setting cursor failed
362 | return false;
363 |
364 | };
365 |
366 | };
367 |
368 | //attach supporting classes
369 | mouse.State = Guacamole.Mouse.State
370 | mouse.Touchpad = Guacamole.Mouse.Touchpad
371 | mouse.Touchscreen = Guacamole.Mouse.Touchscreen
372 |
373 |
374 | export default {
375 | mouse
376 | }
--------------------------------------------------------------------------------
/frontend/src/components/GuacClient.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 | Go桌面(RDP/VNC)
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 连接
53 | 取消
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
414 |
415 |
440 |
--------------------------------------------------------------------------------