├── 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 | 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 | 62 | 63 | 414 | 415 | 440 | --------------------------------------------------------------------------------