├── .appveyor.yml ├── .drone.yml ├── .github └── workflows │ └── go-test.yaml ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.json ├── go.mod ├── go.sum ├── index.html ├── index_generate.go ├── index_generated.go ├── novnc_generate.go ├── novnc_generated.go ├── novnc_test.go ├── server.go ├── server_test.go └── wstcp ├── Dockerfile ├── README.md └── wstcp.go /.appveyor.yml: -------------------------------------------------------------------------------- 1 | image: ubuntu 2 | 3 | # stack: go 1.14 4 | 5 | version: '{build}' 6 | 7 | cache: 8 | - go1.14.deb 9 | 10 | install: 11 | - 'if [[ $APPVEYOR_REPO_TAG == "true" ]]; then appveyor UpdateBuild -Version "$(git describe --tags --always)"; else appveyor UpdateBuild -Version "$(git rev-parse --short HEAD)"; fi' 12 | - 'wget --no-clobber -O go1.14.deb https://deb.geek1011.net/pool/main/g/go/go_1.14-godeb1_amd64.deb || true' 13 | - sudo dpkg -i go1.14.deb 14 | - go mod download 15 | 16 | build_script: 17 | - go test -v ./... 18 | 19 | - GOOS=windows GOARCH=386 go build -o easy-novnc_windows.exe . 20 | - GOOS=darwin GOARCH=amd64 go build -o easy-novnc_darwin-64bit . 21 | - GOOS=linux GOARCH=386 go build -o easy-novnc_linux-32bit . 22 | - GOOS=linux GOARCH=amd64 go build -o easy-novnc_linux-64bit . 23 | - GOOS=linux GOARCH=arm go build -o easy-novnc_linux-arm . 24 | 25 | - GOOS=windows GOARCH=386 go build -o wstcp_windows.exe ./wstcp 26 | - GOOS=darwin GOARCH=amd64 go build -o wstcp_darwin-64bit ./wstcp 27 | - GOOS=linux GOARCH=386 go build -o wstcp_linux-32bit ./wstcp 28 | - GOOS=linux GOARCH=amd64 go build -o wstcp_linux-64bit ./wstcp 29 | - GOOS=linux GOARCH=arm go build -o wstcp_linux-arm ./wstcp 30 | 31 | test: off 32 | 33 | artifacts: 34 | - path: easy-novnc_* 35 | - path: wstcp_* 36 | 37 | deploy: 38 | release: $(APPVEYOR_BUILD_VERSION) 39 | provider: GitHub 40 | auth_token: 41 | secure: oMHoA3qAfCcz3PsfBJmce+fKcSOtUF1cTC3RUj1qKT4J4BjbkOcawazIrXR4F1eb 42 | artifact: /.+/ 43 | draft: true 44 | prerelease: false 45 | on: 46 | branch: master 47 | APPVEYOR_REPO_TAG: true -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | steps: 5 | - name: test 6 | image: golang:1.14 7 | commands: 8 | - go mod download 9 | - go test -v ./... 10 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.14 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.14 14 | id: go 15 | 16 | - name: Check out code 17 | uses: actions/checkout@v1 18 | 19 | - name: Download dependencies 20 | run: go mod download 21 | 22 | - name: Run tests 23 | run: go test -v ./... 24 | 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine AS build 2 | ADD . /src 3 | WORKDIR /src 4 | RUN apk add --no-cache git 5 | RUN go mod download 6 | RUN go build . 7 | 8 | FROM alpine:latest 9 | COPY --from=build /src/easy-novnc / 10 | EXPOSE 8080 11 | ENTRYPOINT ["/easy-novnc"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Patrick G 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easy-novnc 2 | An easy way to run a [noVNC](https://github.com/novnc/noVNC) instance and proxy with a single binary. 3 | 4 | ## Features 5 | - Clean start page. 6 | - CIDR whitelist/blacklist. 7 | - Optionally allow connections to arbitrary hosts (and ports). 8 | - Ensures the target port is a VNC server to prevent tunneling to unauthorized ports. 9 | - Can be configured using environment variables or command line flags (but works out-of-the box). 10 | - IPv6 support. 11 | - Single binary, no dependencies. 12 | - Easy setup. 13 | - Optional [client](./wstcp) for local TCP connections tunneled through WebSockets. 14 | 15 | ## Installation 16 | - Binaries for the latest commit can be downloaded [here](https://ci.appveyor.com/project/pgaskin/easy-novnc/build/artifacts). 17 | - It can also be [deployed to Heroku](https://heroku.com/deploy). 18 | - A Docker image is available: [geek1011/easy-novnc:latest](https://hub.docker.com/r/geek1011/easy-novnc). 19 | - You can build your own binaries with go 1.13 or newer using `go get github.com/pgaskin/easy-novnc` or by cloning this repo and running `go build`. 20 | 21 | ## Usage 22 | ``` 23 | Usage: easy-novnc [options] 24 | 25 | Options: 26 | -a, --addr string The address to listen on (env NOVNC_ADDR) (default ":8080") 27 | -H, --arbitrary-hosts Allow connection to other hosts (env NOVNC_ARBITRARY_HOSTS) 28 | -P, --arbitrary-ports Allow connections to arbitrary ports (requires arbitrary-hosts) (env NOVNC_ARBITRARY_PORTS) 29 | -u, --basic-ui Hide connection options from the main screen (env NOVNC_BASIC_UI) 30 | -C, --cidr-blacklist strings CIDR blacklist for when arbitrary hosts are enabled (comma separated) (conflicts with whitelist) (env NOVNC_CIDR_BLACKLIST) 31 | -c, --cidr-whitelist strings CIDR whitelist for when arbitrary hosts are enabled (comma separated) (conflicts with blacklist) (env NOVNC_CIDR_WHITELIST) 32 | --default-view-only Use view-only by default (env NOVNC_DEFAULT_VIEW_ONLY) 33 | --help Show this help text 34 | -h, --host string The host/ip to connect to by default (env NOVNC_HOST) (default "localhost") 35 | --no-url-password Do not allow password in URL params (env NOVNC_NO_URL_PASSWORD) 36 | -p, --port uint16 The port to connect to by default (env NOVNC_PORT) (default 5900) 37 | -v, --verbose Show extra log info (env NOVNC_VERBOSE) 38 | ``` 39 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-novnc", 3 | "description": "An easy way to run a noVNC instance and proxy with a single binary.", 4 | "repository": "https://github.com/pgaskin/easy-novnc", 5 | "keywords": [ 6 | "novnc", 7 | "vnc", 8 | "websockify" 9 | ], 10 | "env": { 11 | "NOVNC_ARBITRARY_HOSTS": { 12 | "description": "Allow connection to other hosts", 13 | "value": "true" 14 | }, 15 | "NOVNC_ARBITRARY_PORTS": { 16 | "description": "Allow connections to arbitrary ports", 17 | "value": "true" 18 | }, 19 | "NOVNC_CIDR_WHITELIST": { 20 | "description": "CIDR whitelist for when arbitrary hosts are enabled (comma separated) (conflicts with blacklist)", 21 | "value": "" 22 | }, 23 | "NOVNC_CIDR_BLACKLIST": { 24 | "description": "CIDR blacklist for when arbitrary hosts are enabled (comma separated) (conflicts with blacklist)", 25 | "value": "" 26 | }, 27 | "NOVNC_BASIC_UI": { 28 | "description": "Hide connection options from the main screen", 29 | "value": "false" 30 | }, 31 | "NOVNC_HOST": { 32 | "description": "The host/ip to connect to by default", 33 | "value": "localhost" 34 | }, 35 | "NOVNC_PORT": { 36 | "description": "The port to connect to by default", 37 | "value": "5900" 38 | }, 39 | "NOVNC_NO_URL_PASSWORD": { 40 | "description": "Do not allow password in URL params", 41 | "value": "true" 42 | }, 43 | "NOVNC_PARAMS": { 44 | "description": "Extra URL params for noVNC (advanced) (comma separated key-value pairs) (e.g. resize=remote)", 45 | "value": "" 46 | }, 47 | "NOVNC_DEFAULT_VIEW_ONLY": { 48 | "description": "Use view-only by default", 49 | "value": "false" 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pgaskin/easy-novnc 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gorilla/mux v1.7.4 7 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 8 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd 9 | github.com/spf13/pflag v1.0.5 10 | github.com/spkg/zipfs v0.7.1 11 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a 12 | golang.org/x/tools v0.0.0-20200302213018-c4f5635f1074 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= 4 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 5 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 6 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= 10 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 11 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd h1:ug7PpSOB5RBPK1Kg6qskGBoP3Vnj/aNYFTznWvlkGo0= 12 | github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= 13 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 14 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 15 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 16 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 17 | github.com/spkg/zipfs v0.7.0 h1:Ilog8m/WwtDM+Q0LS8Yjvl3S4mPKhE4WrtScIPUWmQw= 18 | github.com/spkg/zipfs v0.7.0/go.mod h1:48LW+/Rh1G7aAav1ew1PdlYn52T+LM+ARmSHfDNJvg8= 19 | github.com/spkg/zipfs v0.7.1 h1:+2X5lvNHTybnDMQZAIHgedRXZK1WXdc+94R/P5v2XWE= 20 | github.com/spkg/zipfs v0.7.1/go.mod h1:48LW+/Rh1G7aAav1ew1PdlYn52T+LM+ARmSHfDNJvg8= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 23 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 24 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 25 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 26 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 27 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 28 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 29 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= 30 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 31 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= 32 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 33 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= 34 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 35 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 39 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f h1:ESK9Jb5JOE+y4u+ozMQeXfMHwEHm6zVbaDQkeaj6wI4= 40 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 41 | golang.org/x/tools v0.0.0-20200302213018-c4f5635f1074 h1:0nKaw3H/Gss/tnq0+jZzh6SiplGjXNDRfWFS85SiMtA= 42 | golang.org/x/tools v0.0.0-20200302213018-c4f5635f1074/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 43 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 44 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | noVNC 10 | 11 | 12 | 57 | 58 | 59 | 60 |
61 |
62 |

noVNC

63 |
64 | {{if .arbitraryHosts}} 65 | {{if .arbitraryPorts}} 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | {{else}} 77 |
78 | 79 | 80 |
81 | {{end}} 82 | {{end}} 83 | 84 | {{if not .noURLPassword}} 85 |
86 | 87 | 88 |
89 | {{end}} 90 | 91 | 92 | 93 | 94 | {{range $key, $value := .params}} 95 | 96 | {{end}} 97 | 98 | 99 | 100 | {{if not .basicUI}} 101 |

Connection Options

102 | 103 |
104 |
105 |
106 | 107 | 108 |
109 |
110 |
111 |
112 | 113 | 114 |
115 |
116 |
117 |
118 |
119 |
120 | 121 | 122 |
123 |
124 |
125 |
126 | 127 | 128 |
129 |
130 |
131 | {{else}} 132 | 133 | 134 | 135 | 136 | {{end}} 137 |
138 |
139 |
140 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /index_generate.go: -------------------------------------------------------------------------------- 1 | // +build index_generate 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | fmt.Println("writing index_generated.go") 13 | 14 | buf, err := ioutil.ReadFile("index.html") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | f, err := os.Create("index_generated.go") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | _, err = fmt.Fprintf(f, "// Code generated by index_generate.go; DO NOT EDIT.\n\npackage main\n\nimport \"html/template\"\n\nvar indexTMPL = template.Must(template.New(\"\").Parse(%#v))\n", string(buf)) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | err = f.Close() 30 | if err != nil { 31 | panic(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /index_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by index_generate.go; DO NOT EDIT. 2 | 3 | package main 4 | 5 | import "html/template" 6 | 7 | var indexTMPL = template.Must(template.New("").Parse("\n\n\n\n \n \n \n \n noVNC\n \n \n \n\n\n\n
\n
\n

noVNC

\n
\n {{if .arbitraryHosts}}\n {{if .arbitraryPorts}}\n
\n
\n \n \n
\n
\n \n \n
\n
\n {{else}}\n
\n \n \n
\n {{end}}\n {{end}}\n\n {{if not .noURLPassword}}\n
\n \n \n
\n {{end}}\n\n \n \n\n {{range $key, $value := .params}}\n \n {{end}}\n\n \n\n {{if not .basicUI}}\n

Connection Options

\n\n
\n
\n
\n \n \n
\n
\n
\n
\n \n \n
\n
\n
\n
\n
\n
\n \n \n
\n
\n
\n
\n \n \n
\n
\n
\n {{else}}\n \n \n \n \n {{end}}\n
\n
\n
\n \n\n\n")) 8 | -------------------------------------------------------------------------------- /novnc_generate.go: -------------------------------------------------------------------------------- 1 | // +build novnc_generate 2 | 3 | package main 4 | 5 | import ( 6 | "archive/zip" 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/shurcooL/vfsgen" 17 | "github.com/spkg/zipfs" 18 | ) 19 | 20 | const noVNCZip = "https://github.com/novnc/noVNC/archive/master.zip" 21 | const vncScript = ` 22 | try { 23 | function parseQuery(e){for(var o=e.split("&"),n={},t=0;t"), []byte(fmt.Sprintf("", vncScript))) 112 | fi, err := os.Stat("novnc_generate.go") 113 | if err != nil { 114 | return err 115 | } 116 | w, err = zw.CreateHeader(&zip.FileHeader{ 117 | Name: e.Name, 118 | Flags: e.Flags, 119 | Method: e.Method, 120 | Modified: fi.ModTime(), 121 | Extra: e.Extra, 122 | ExternalAttrs: e.ExternalAttrs, 123 | }) 124 | } else { 125 | w, err = zw.CreateHeader(&e.FileHeader) 126 | } 127 | 128 | if err != nil { 129 | return err 130 | } 131 | 132 | _, err = io.Copy(w, bytes.NewReader(fbuf)) 133 | if err != nil { 134 | return err 135 | } 136 | rc.Close() 137 | } 138 | 139 | if !found { 140 | return errors.New("could not find vnc.html") 141 | } 142 | 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /novnc_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/shurcooL/httpfs/vfsutil" 10 | ) 11 | 12 | func TestNoVNC(t *testing.T) { 13 | f, err := noVNC.Open("noVNC-master") 14 | if err != nil { 15 | t.Errorf("could not open noVNC root dir: %v", err) 16 | } 17 | 18 | _, err = f.Readdir(1) 19 | if err != nil { 20 | t.Errorf("could not read noVNC root dir: %v", err) 21 | } 22 | 23 | f, err = noVNC.Open("noVNC-master/vnc.html") 24 | if err != nil { 25 | t.Errorf("could not open vnc.html: %v", err) 26 | } 27 | 28 | buf, err := ioutil.ReadAll(f) 29 | if err != nil { 30 | t.Errorf("could not read vnc.html: %v", err) 31 | } 32 | 33 | if len(buf) < 100 { 34 | t.Errorf("vnc.html is too small") 35 | } 36 | 37 | f, err = noVNC.Open("noVNC-master/VERSION") 38 | if err != nil { 39 | t.Errorf("could not open VERSION: %v", err) 40 | } 41 | 42 | buf, err = ioutil.ReadAll(f) 43 | if err != nil { 44 | t.Errorf("could not read VERSION: %v", err) 45 | } 46 | 47 | t.Logf("noVNC %s", string(buf)) 48 | 49 | err = vfsutil.WalkFiles(noVNC, "/", func(path string, info os.FileInfo, rs io.ReadSeeker, err error) error { 50 | return err 51 | }) 52 | if err != nil { 53 | t.Errorf("could not read fs: %v", err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // +build !index_generate 2 | // +build !novnc_generate 3 | 4 | package main 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "strings" 15 | "time" 16 | 17 | "github.com/gorilla/mux" 18 | "github.com/spf13/pflag" 19 | "golang.org/x/net/websocket" 20 | ) 21 | 22 | //go:generate go run novnc_generate.go 23 | //go:generate go run index_generate.go 24 | 25 | // https://stackoverflow.com/a/17871737 26 | var ipv6Regexp = `(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))` 27 | 28 | func main() { 29 | pflag.Usage = func() { 30 | fmt.Printf("Usage: %s [options]\n\nOptions:\n", os.Args[0]) 31 | pflag.PrintDefaults() 32 | } 33 | 34 | arbitraryHosts := pflag.BoolP("arbitrary-hosts", "H", false, "Allow connection to other hosts") 35 | arbitraryPorts := pflag.BoolP("arbitrary-ports", "P", false, "Allow connections to arbitrary ports (requires arbitrary-hosts)") 36 | cidrWhitelist := pflag.StringSliceP("cidr-whitelist", "c", []string{}, "CIDR whitelist for when arbitrary hosts are enabled (comma separated) (conflicts with blacklist)") 37 | cidrBlacklist := pflag.StringSliceP("cidr-blacklist", "C", []string{}, "CIDR blacklist for when arbitrary hosts are enabled (comma separated) (conflicts with whitelist)") 38 | host := pflag.StringP("host", "h", "localhost", "The host/ip to connect to by default") 39 | port := pflag.Uint16P("port", "p", 5900, "The port to connect to by default") 40 | addr := pflag.StringP("addr", "a", ":8080", "The address to listen on") 41 | basicUI := pflag.BoolP("basic-ui", "u", false, "Hide connection options from the main screen") 42 | verbose := pflag.BoolP("verbose", "v", false, "Show extra log info") 43 | noURLPassword := pflag.Bool("no-url-password", false, "Do not allow password in URL params") 44 | novncParams := pflag.StringSlice("novnc-params", nil, "Extra URL params for noVNC (advanced) (comma separated key-value pairs) (e.g. resize=remote)") 45 | defaultViewOnly := pflag.Bool("default-view-only", false, "Use view-only by default") 46 | help := pflag.Bool("help", false, "Show this help text") 47 | 48 | envmap := map[string]string{ 49 | "arbitrary-hosts": "NOVNC_ARBITRARY_HOSTS", 50 | "arbitrary-ports": "NOVNC_ARBITRARY_PORTS", 51 | "cidr-whitelist": "NOVNC_CIDR_WHITELIST", 52 | "cidr-blacklist": "NOVNC_CIDR_BLACKLIST", 53 | "host": "NOVNC_HOST", 54 | "port": "NOVNC_PORT", 55 | "addr": "NOVNC_ADDR", 56 | "basic-ui": "NOVNC_BASIC_UI", 57 | "no-url-password": "NOVNC_NO_URL_PASSWORD", 58 | "novnc-params": "NOVNC_PARAMS", 59 | "default-view-only": "NOVNC_DEFAULT_VIEW_ONLY", 60 | "verbose": "NOVNC_VERBOSE", 61 | } 62 | 63 | if val, ok := os.LookupEnv("PORT"); ok { 64 | val = ":" + val 65 | fmt.Printf("Setting --addr from PORT to %#v\n", val) 66 | if err := pflag.Set("addr", val); err != nil { 67 | fmt.Printf("Error: %v\n", err) 68 | os.Exit(2) 69 | } 70 | } 71 | 72 | pflag.VisitAll(func(flag *pflag.Flag) { 73 | if env, ok := envmap[flag.Name]; ok { 74 | flag.Usage += fmt.Sprintf(" (env %s)", env) 75 | if val, ok := os.LookupEnv(env); ok { 76 | fmt.Printf("Setting --%s from %s to %#v\n", flag.Name, env, val) 77 | if err := flag.Value.Set(val); err != nil { 78 | fmt.Printf("Error: %v\n", err) 79 | os.Exit(2) 80 | } 81 | } 82 | } 83 | }) 84 | 85 | pflag.Parse() 86 | 87 | if *arbitraryPorts && !*arbitraryHosts { 88 | fmt.Printf("Error: arbitrary-ports requires arbitrary-hosts to be enabled.\n") 89 | os.Exit(2) 90 | } 91 | 92 | cidrList, isWhitelist, err := parseCIDRBlackWhiteList(*cidrBlacklist, *cidrWhitelist) 93 | if err != nil { 94 | fmt.Printf("Error: error parsing cidr blacklist/whitelist: %v.\n", err) 95 | os.Exit(2) 96 | } 97 | 98 | if len(cidrList) != 0 { 99 | if err := checkCIDRBlackWhiteListHost(*host, cidrList, isWhitelist); err != nil { 100 | fmt.Printf("Warning: default host does not parse cidr blacklist/whitelist: %v.\n", err) 101 | } 102 | } 103 | 104 | novncParamsMap := map[string]string{ 105 | "resize": "scale", 106 | } 107 | for _, p := range *novncParams { 108 | spl := strings.SplitN(p, "=", 2) 109 | if len(spl) != 2 { 110 | fmt.Printf("Error: error parsing noVNC params: must be in key=value format.\n") 111 | os.Exit(2) 112 | } 113 | 114 | // https://github.com/novnc/noVNC/blob/master/docs/EMBEDDING.md 115 | switch spl[0] { 116 | case "resize", "logging", "repeaterID", "reconnect_delay", "view_clip": 117 | novncParamsMap[spl[0]] = spl[1] 118 | case "encrypt", "reconnect", "path", "password", "view_only", "show_dot", "bell", "autoconnect": 119 | fmt.Printf("Error: error parsing noVNC params: option %#v reserved for use by easy-novnc.\n", spl[0]) 120 | os.Exit(2) 121 | default: 122 | fmt.Printf("Error: error parsing noVNC params: unknown option %#v.\n", spl[0]) 123 | os.Exit(2) 124 | } 125 | } 126 | 127 | if *help { 128 | pflag.Usage() 129 | os.Exit(1) 130 | } 131 | 132 | r := mux.NewRouter() 133 | r.Use(noCache) 134 | r.Use(serverHeader) 135 | 136 | vnc := vncHandler(*host, *port, *verbose, *arbitraryHosts, *arbitraryPorts, cidrList, isWhitelist) 137 | r.Handle("/vnc", vnc) 138 | r.Handle("/vnc/{host:[a-zA-Z0-9_.-]+}", vnc) 139 | r.Handle("/vnc/{host:[a-zA-Z0-9_.-]+}/{port:[0-9]+}", vnc) 140 | r.Handle("/vnc/{host:"+ipv6Regexp+"}", vnc) 141 | r.Handle("/vnc/{host:"+ipv6Regexp+"}/{port:[0-9]+}", vnc) 142 | 143 | r.NotFoundHandler = fs("noVNC-master", noVNC) 144 | r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 145 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 146 | w.WriteHeader(http.StatusOK) 147 | indexTMPL.Execute(w, map[string]interface{}{ 148 | "arbitraryHosts": *arbitraryHosts, 149 | "arbitraryPorts": *arbitraryPorts, 150 | "host": *host, 151 | "port": *port, 152 | "addr": *addr, 153 | "basicUI": *basicUI, 154 | "noURLPassword": *noURLPassword, 155 | "defaultViewOnly": *defaultViewOnly, 156 | "params": novncParamsMap, 157 | }) 158 | }) 159 | 160 | fmt.Printf("Listening on http://%s\n", *addr) 161 | if !*arbitraryHosts && !*arbitraryPorts && *host == "localhost" && *port == 5900 && !*basicUI { 162 | fmt.Printf("Run with --help for more options\n") 163 | } 164 | if err := http.ListenAndServe(*addr, r); err != nil { 165 | logf(true, "Error: %v.\n", err) 166 | os.Exit(1) 167 | } 168 | } 169 | 170 | // vncHandler creates a handler for vnc connections. If host and port are set in 171 | // the url vars, they will be used if allowed. 172 | func vncHandler(defhost string, defport uint16, verbose, allowHosts, allowPorts bool, cidrList []*net.IPNet, isWhitelist bool) http.Handler { 173 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 174 | var host, port string 175 | 176 | if host = mux.Vars(r)["host"]; host == "" { 177 | host = defhost 178 | } else if !allowHosts { 179 | logf(verbose, "connect %s disabled\n", host) 180 | http.Error(w, "--arbitrary-hosts disabled", http.StatusUnauthorized) 181 | return 182 | } 183 | 184 | if port = mux.Vars(r)["port"]; port == "" { 185 | port = fmt.Sprint(defport) 186 | } else if !allowPorts { 187 | logf(verbose, "connect %s:%s disabled\n", host, port) 188 | http.Error(w, "--arbitrary-ports disabled", http.StatusUnauthorized) 189 | return 190 | } 191 | 192 | if len(cidrList) != 0 { 193 | if err := checkCIDRBlackWhiteListHost(host, cidrList, isWhitelist); err != nil { 194 | logf(verbose, "connect %s:%s not allowed: %v\n", host, port, err) 195 | http.Error(w, fmt.Sprintf("connect %s:%s not allowed: %v\n", host, port, err), http.StatusUnauthorized) 196 | return 197 | } 198 | } 199 | 200 | addr := host + ":" + port 201 | if ip := net.ParseIP(host); ip != nil && ip.To4() == nil { 202 | addr = "[" + host + "]:" + port 203 | } 204 | 205 | logf(verbose, "connect %s\n", addr) 206 | w.Header().Set("X-Target-Addr", addr) 207 | websockify(addr, []byte("RFB")).ServeHTTP(w, r) 208 | }) 209 | } 210 | 211 | // logf calls fmt.Printf with the date if the condition is true. 212 | func logf(cond bool, format string, a ...interface{}) { 213 | if cond { 214 | fmt.Printf("%s: %s", time.Now().Format("Jan 02 15:04:05"), fmt.Sprintf(format, a...)) 215 | } 216 | } 217 | 218 | // noCache disables caching on a http.Handler. 219 | func noCache(next http.Handler) http.Handler { 220 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 221 | w.Header().Set("Cache-Control", "no-cache") 222 | next.ServeHTTP(w, r) 223 | }) 224 | } 225 | 226 | // serverHeader sets the Server header for a http.Handler. 227 | func serverHeader(next http.Handler) http.Handler { 228 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 229 | w.Header().Set("Server", "easy-novnc") 230 | next.ServeHTTP(w, r) 231 | }) 232 | } 233 | 234 | // fs returns a http.Handler which serves a directory from a http.FileSystem. 235 | func fs(dir string, fs http.FileSystem) http.Handler { 236 | return addPrefix("/"+strings.Trim(dir, "/"), http.FileServer(fs)) 237 | } 238 | 239 | // addPrefix is similar to http.StripPrefix, except it adds a prefix. 240 | func addPrefix(prefix string, h http.Handler) http.Handler { 241 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 242 | r2 := new(http.Request) 243 | *r2 = *r 244 | r2.URL = new(url.URL) 245 | *r2.URL = *r.URL 246 | r2.URL.Path = prefix + r.URL.Path 247 | h.ServeHTTP(w, r2) 248 | }) 249 | } 250 | 251 | // websockify returns an http.Handler which proxies websocket requests to a tcp 252 | // address and checks magic bytes. 253 | func websockify(to string, magic []byte) http.Handler { 254 | return websocket.Server{ 255 | Handshake: wsProxyHandshake, 256 | Handler: wsProxyHandler(to, magic), 257 | } 258 | } 259 | 260 | // wsProxyHandshake is a handshake handler for a websocket.Server. 261 | func wsProxyHandshake(config *websocket.Config, r *http.Request) error { 262 | if r.Header.Get("Sec-WebSocket-Protocol") != "" { 263 | config.Protocol = []string{"binary"} 264 | } 265 | r.Header.Set("Access-Control-Allow-Origin", "*") 266 | r.Header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE") 267 | return nil 268 | } 269 | 270 | // wsProxyHandler is a websocket.Handler which proxies to a tcp address with a 271 | // magic byte check. 272 | func wsProxyHandler(to string, magic []byte) websocket.Handler { 273 | return func(ws *websocket.Conn) { 274 | conn, err := net.Dial("tcp", to) 275 | if err != nil { 276 | ws.Close() 277 | return 278 | } 279 | 280 | ws.PayloadType = websocket.BinaryFrame 281 | 282 | m := newMagicCheck(conn, magic) 283 | 284 | done := make(chan error) 285 | go copyCh(conn, ws, done) 286 | go copyCh(ws, m, done) 287 | 288 | err = <-done 289 | if m.Failed() { 290 | logf(true, "attempt to connect to non-VNC port (%s, %#v)\n", to, string(m.Magic())) 291 | } else if err != nil { 292 | logf(true, "%v\n", err) 293 | } 294 | 295 | conn.Close() 296 | ws.Close() 297 | <-done 298 | } 299 | } 300 | 301 | // copyCh is like io.Copy, but it writes to a channel when finished. 302 | func copyCh(dst io.Writer, src io.Reader, done chan error) { 303 | _, err := io.Copy(dst, src) 304 | done <- err 305 | } 306 | 307 | // checkCIDRBlackWhiteListHost checks the provided host/ip against a blacklist/whitelist. 308 | func checkCIDRBlackWhiteListHost(host string, cidrList []*net.IPNet, isWhitelist bool) error { 309 | ips, err := net.LookupIP(host) 310 | if err != nil { 311 | return err 312 | } 313 | for _, ip := range ips { 314 | if err := checkCIDRBlackWhiteList(ip, cidrList, isWhitelist); err != nil { 315 | return err 316 | } 317 | } 318 | return nil 319 | } 320 | 321 | // checkCIDRBlackWhiteList checks an IP against a blacklist/whitelist. 322 | func checkCIDRBlackWhiteList(ip net.IP, cidrList []*net.IPNet, isWhitelist bool) error { 323 | var matchedCIDR *net.IPNet 324 | for _, cidr := range cidrList { 325 | if cidr.Contains(ip) { 326 | matchedCIDR = cidr 327 | break 328 | } 329 | } 330 | if matchedCIDR == nil && isWhitelist { 331 | return fmt.Errorf("ip %s does not match any whitelisted cidr", ip) 332 | } else if matchedCIDR != nil && !isWhitelist { 333 | return fmt.Errorf("ip %s matches blacklisted cidr %s", ip, matchedCIDR) 334 | } 335 | return nil 336 | } 337 | 338 | // parseCIDRBlackWhiteList returns either a parsed blacklist or whitelist of 339 | // CIDRs. If neither is specified, isWhitelist is false and the slice is empty. 340 | func parseCIDRBlackWhiteList(blacklist []string, whitelist []string) (cidrs []*net.IPNet, isWhitelist bool, err error) { 341 | if len(blacklist) != 0 && len(whitelist) != 0 { 342 | err = errors.New("only one of blacklist/whitelist can be specified") 343 | return 344 | } 345 | if len(whitelist) != 0 { 346 | isWhitelist = true 347 | cidrs, err = parseCIDRList(whitelist) 348 | } else { 349 | cidrs, err = parseCIDRList(blacklist) 350 | } 351 | return 352 | } 353 | 354 | // parseCIDRList parses a list of CIDRs. 355 | func parseCIDRList(cidrs []string) ([]*net.IPNet, error) { 356 | res := make([]*net.IPNet, len(cidrs)) 357 | for i, str := range cidrs { 358 | _, cidr, err := net.ParseCIDR(str) 359 | if err != nil { 360 | return nil, fmt.Errorf("error parsing CIDR '%s': %v", str, err) 361 | } 362 | res[i] = cidr 363 | } 364 | return res, nil 365 | } 366 | 367 | // magicCheck implements an efficient wrapper around an io.Reader which checks 368 | // for magic bytes at the beginning, and will return a sticky io.EOF and stop 369 | // reading from the original reader as soon as a mismatch starts. 370 | type magicCheck struct { 371 | rdr io.Reader 372 | exp []byte 373 | len int 374 | rem int 375 | act []byte 376 | fld bool 377 | } 378 | 379 | func newMagicCheck(r io.Reader, magic []byte) *magicCheck { 380 | return &magicCheck{r, magic, len(magic), len(magic), make([]byte, len(magic)), false} 381 | } 382 | 383 | // Failed returns true if the magic check has failed (note that it returns false 384 | // if the source io.Reader reached io.EOF before the check was complete). 385 | func (m *magicCheck) Failed() bool { 386 | return m.fld 387 | } 388 | 389 | // Magic returns the magic which was read so far. 390 | func (m *magicCheck) Magic() []byte { 391 | return m.act 392 | } 393 | 394 | func (m *magicCheck) Read(buf []byte) (n int, err error) { 395 | if m.fld { 396 | return 0, io.EOF 397 | } 398 | n, err = m.rdr.Read(buf) 399 | if err == nil && n > 0 && m.rem > 0 { 400 | m.rem -= copy(m.act[m.len-m.rem:], buf[:n]) 401 | for i := 0; i < m.len-m.rem; i++ { 402 | if m.act[i] != m.exp[i] { 403 | m.fld = true 404 | return 0, io.EOF 405 | } 406 | } 407 | } 408 | return n, err 409 | } 410 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "net/http/httptest" 13 | "os" 14 | "path/filepath" 15 | "regexp" 16 | "strings" 17 | "testing" 18 | "time" 19 | 20 | "github.com/gorilla/mux" 21 | ) 22 | 23 | func TestVNCHandler(t *testing.T) { 24 | testCase := func(url string, expectedStatus int, expectedAddr string, defhost string, defport uint16, allowHosts, allowPorts bool, cidrList []*net.IPNet, isWhitelist bool) func(*testing.T) { 25 | return func(t *testing.T) { 26 | r := httptest.NewRequest("GET", url, nil) 27 | w := httptest.NewRecorder() 28 | 29 | var ws bool 30 | func() { 31 | defer func() { 32 | // workaround for websocket library issue with a fake http response 33 | if err := recover(); strings.Contains(fmt.Sprint(err), "not http.Hijacker") { 34 | ws = true 35 | } else if err != nil { 36 | panic(err) 37 | } 38 | }() 39 | vnc := vncHandler(defhost, defport, false, allowHosts, allowPorts, cidrList, isWhitelist) 40 | m := mux.NewRouter() 41 | m.Handle("/vnc", vnc) 42 | m.Handle("/vnc/{host:[a-zA-Z0-9_.-]+}", vnc) 43 | m.Handle("/vnc/{host:[a-zA-Z0-9_.-]+}/{port:[0-9]+}", vnc) 44 | m.Handle("/vnc/{host:"+ipv6Regexp+"}", vnc) 45 | m.Handle("/vnc/{host:"+ipv6Regexp+"}/{port:[0-9]+}", vnc) 46 | m.ServeHTTP(w, r) 47 | }() 48 | 49 | c := w.Result().StatusCode 50 | if ws && c == 200 { 51 | c = 101 52 | } 53 | if c != expectedStatus { 54 | t.Errorf("expected status %d, got %d", expectedStatus, c) 55 | } 56 | 57 | if a := w.Result().Header.Get("X-Target-Addr"); a != expectedAddr { 58 | t.Errorf("expected addr %#v, got %#v", expectedAddr, a) 59 | } 60 | } 61 | } 62 | t.Run("Simple", testCase("http://example.com/vnc", 101, "localhost:5900", "localhost", 5900, false, false, nil, false)) 63 | t.Run("SimpleBlockHost", testCase("http://example.com/vnc/test", 401, "", "localhost", 5900, false, false, nil, false)) 64 | t.Run("SimpleBlockHostPort", testCase("http://example.com/vnc/test/1234", 401, "", "localhost", 5900, true, false, nil, false)) 65 | 66 | t.Run("Custom", testCase("http://example.com/vnc", 101, "example.com:1234", "example.com", 1234, false, false, nil, false)) 67 | t.Run("CustomHost", testCase("http://example.com/vnc/test", 101, "test:1234", "example.com", 1234, true, false, nil, false)) 68 | t.Run("CustomHostPort", testCase("http://example.com/vnc/test/3456", 101, "test:3456", "example.com", 1234, true, true, nil, false)) 69 | 70 | t.Run("CIDRWhitelistAllowIP", testCase("http://example.com/vnc/10.0.0.1", 101, "10.0.0.1:5900", "localhost", 5900, true, true, mustParseCIDRList("192.168.0.0/24,10.0.0.0/24"), true)) 71 | t.Run("CIDRWhitelistBlockIP", testCase("http://example.com/vnc/127.0.0.1", 401, "", "localhost", 5900, true, true, mustParseCIDRList("192.168.0.0/24,10.0.0.0/24"), true)) 72 | t.Run("CIDRBlacklistBlockIP", testCase("http://example.com/vnc/10.0.0.1", 401, "", "localhost", 5900, true, true, mustParseCIDRList("192.168.0.0/24,10.0.0.0/24"), false)) 73 | t.Run("CIDRBlacklistAllowIP", testCase("http://example.com/vnc/127.0.0.1/5900", 101, "127.0.0.1:5900", "localhost", 5900, true, true, mustParseCIDRList("192.168.0.0/24,10.0.0.0/24"), false)) 74 | 75 | t.Run("CIDRWhitelistAllowHost", testCase("http://example.com/vnc/10.0.0.1.ip.dns.geek1011.net", 101, "10.0.0.1.ip.dns.geek1011.net:5900", "localhost", 5900, true, true, mustParseCIDRList("192.168.0.0/24,10.0.0.0/24"), true)) 76 | t.Run("CIDRWhitelistBlockHost", testCase("http://example.com/vnc/127.0.0.1.ip.dns.geek1011.net", 401, "", "localhost", 5900, true, true, mustParseCIDRList("192.168.0.0/24,10.0.0.0/24"), true)) 77 | t.Run("CIDRBlacklistBlockHost", testCase("http://example.com/vnc/10.0.0.1.ip.dns.geek1011.net", 401, "", "localhost", 5900, true, true, mustParseCIDRList("192.168.0.0/24,10.0.0.0/24"), false)) 78 | t.Run("CIDRBlacklistAllowHost", testCase("http://example.com/vnc/127.0.0.1.ip.dns.geek1011.net/5900", 101, "127.0.0.1.ip.dns.geek1011.net:5900", "localhost", 5900, true, true, mustParseCIDRList("192.168.0.0/24,10.0.0.0/24"), false)) 79 | 80 | t.Run("CIDRWhitelistAllowIPv6", testCase("http://example.com/vnc/a%3Ab%3Ac%3Ad%3Aa%3Ab%3Ac%3Ad", 101, "[a:b:c:d:a:b:c:d]:5900", "localhost", 5900, true, true, mustParseCIDRList("a:b:c:d:a:b:c:d/120"), true)) 81 | t.Run("CIDRWhitelistBlockIPv6", testCase("http://example.com/vnc/a%3Ab%3Ac%3Ad%3Aa%3Ab%3Ad%3Ad", 401, "", "localhost", 5900, true, true, mustParseCIDRList("a:b:c:d:a:b:c:d/120"), true)) 82 | t.Run("CIDRBlacklistBlockIPv6", testCase("http://example.com/vnc/a%3Ab%3Ac%3Ad%3Aa%3Ab%3Ac%3Ad", 401, "", "localhost", 5900, true, true, mustParseCIDRList("a:b:c:d:a:b:c:d/120"), false)) 83 | t.Run("CIDRBlacklistAllowIPv6", testCase("http://example.com/vnc/a%3Ab%3Ac%3Ad%3Aa%3Ab%3Ad%3Ad/5900", 101, "[a:b:c:d:a:b:d:d]:5900", "localhost", 5900, true, true, mustParseCIDRList("a:b:c:d:a:b:c:d/120"), false)) 84 | 85 | t.Run("CIDRWhitelistAllowHostv6", testCase("http://example.com/vnc/a.b.c.d.a.b.c.d.ip.dns.geek1011.net", 101, "a.b.c.d.a.b.c.d.ip.dns.geek1011.net:5900", "localhost", 5900, true, true, mustParseCIDRList("a:b:c:d:a:b:c:d/120"), true)) 86 | t.Run("CIDRWhitelistBlockHostv6", testCase("http://example.com/vnc/a.b.c.d.a.b.d.d.ip.dns.geek1011.net", 401, "", "localhost", 5900, true, true, mustParseCIDRList("a:b:c:d:a:b:c:d/120"), true)) 87 | t.Run("CIDRBlacklistBlockHostv6", testCase("http://example.com/vnc/a.b.c.d.a.b.c.d.ip.dns.geek1011.net", 401, "", "localhost", 5900, true, true, mustParseCIDRList("a:b:c:d:a:b:c:d/120"), false)) 88 | t.Run("CIDRBlacklistAllowHostv6", testCase("http://example.com/vnc/a.b.c.d.a.b.d.d.ip.dns.geek1011.net/5900", 101, "a.b.c.d.a.b.d.d.ip.dns.geek1011.net:5900", "localhost", 5900, true, true, mustParseCIDRList("a:b:c:d:a:b:c:d/120"), false)) 89 | } 90 | 91 | func TestWebsockify(t *testing.T) { 92 | defer func() { 93 | if err := recover(); err != nil && !strings.Contains(fmt.Sprint(err), "not implemented") { 94 | panic(err) 95 | } 96 | }() 97 | websockify("google.com:80", []byte(nil)).ServeHTTP(nilResponseWriter{}, httptest.NewRequest("GET", "/", nil)) 98 | // TODO: proper testing 99 | } 100 | 101 | type nilResponseWriter struct{} 102 | 103 | func (nilResponseWriter) Write(buf []byte) (int, error) { 104 | return len(buf), nil 105 | } 106 | func (nilResponseWriter) WriteHeader(int) {} 107 | func (nilResponseWriter) Header() http.Header { 108 | return http.Header{} 109 | } 110 | func (nilResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 111 | return nil, nil, errors.New("not implemented") 112 | } 113 | 114 | func TestLogf(t *testing.T) { 115 | for _, c := range []struct { 116 | Cond bool 117 | Format string 118 | Args []interface{} 119 | Out string 120 | }{ 121 | {false, "test\n", nil, ""}, 122 | {true, "test\n", nil, "test"}, 123 | {true, "test %s\n", []interface{}{"test"}, "test test"}, 124 | } { 125 | logf(c.Cond, c.Format, c.Args...) 126 | // TODO: figure out a way to test c.Out 127 | } 128 | } 129 | 130 | func TestNoCache(t *testing.T) { 131 | r := httptest.NewRequest("GET", "http://example.com/go.mod", nil) 132 | w := httptest.NewRecorder() 133 | 134 | noCache(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 | })).ServeHTTP(w, r) 136 | 137 | if cc := w.Result().Header.Get("Cache-Control"); cc != "no-cache" { 138 | t.Errorf("wrong Cache-Control header: %#v", cc) 139 | } 140 | } 141 | 142 | func TestServerHeader(t *testing.T) { 143 | r := httptest.NewRequest("GET", "http://example.com/go.mod", nil) 144 | w := httptest.NewRecorder() 145 | 146 | serverHeader(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 147 | })).ServeHTTP(w, r) 148 | 149 | if cc := w.Result().Header.Get("Server"); cc != "easy-novnc" { 150 | t.Errorf("wrong Server header: %#v", cc) 151 | } 152 | } 153 | 154 | func TestFS(t *testing.T) { 155 | d, err := ioutil.TempDir("", "easy-novnc") 156 | if err != nil { 157 | panic(err) 158 | } 159 | defer os.RemoveAll(d) 160 | 161 | err = ioutil.WriteFile(filepath.Join(d, "test.txt"), []byte("foo"), 0644) 162 | if err != nil { 163 | panic(err) 164 | } 165 | 166 | err = os.Mkdir(filepath.Join(d, "tmp"), 0755) 167 | if err != nil { 168 | panic(err) 169 | } 170 | 171 | err = ioutil.WriteFile(filepath.Join(d, "tmp", "test.txt"), []byte("foobar"), 0644) 172 | if err != nil { 173 | panic(err) 174 | } 175 | 176 | r := httptest.NewRequest("GET", "http://example.com/test.txt", nil) 177 | w := httptest.NewRecorder() 178 | 179 | fs("tmp", http.Dir(d)).ServeHTTP(w, r) 180 | 181 | buf, _ := ioutil.ReadAll(w.Result().Body) 182 | if !strings.Contains(string(buf), "foo") { 183 | if !strings.Contains(string(buf), "foobar") { 184 | t.Errorf("serving from wrong subdir, got %#v", string(buf)) 185 | } 186 | t.Errorf("wrong response, got %#v", string(buf)) 187 | } 188 | } 189 | 190 | func TestAddPrefix(t *testing.T) { 191 | for _, c := range [][]string{ 192 | {"", "http://example.com/", "http://example.com/"}, 193 | {"prefix", "http://example.com/", "http://example.com/prefix/"}, 194 | {"prefix", "http://example.com/test", "http://example.com/prefix/test"}, 195 | {"prefix", "http://example.com/test/", "http://example.com/prefix/test/"}, 196 | {"prefix/prefix1", "http://example.com/test/", "http://example.com/prefix/prefix1/test/"}, 197 | {"prefix", "/test/", "prefix/test/"}, 198 | } { 199 | r := httptest.NewRequest("GET", c[1], nil) 200 | w := httptest.NewRecorder() 201 | 202 | addPrefix(c[0], http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 203 | fmt.Fprint(w, r.URL.String()) 204 | })).ServeHTTP(w, r) 205 | 206 | buf, _ := ioutil.ReadAll(w.Result().Body) 207 | if string(buf) != c[2] { 208 | t.Errorf("expected %#v for addPrefix %#v to %#v, got %#v", c[2], c[0], c[1], string(buf)) 209 | } 210 | } 211 | } 212 | 213 | func TestCopyCh(t *testing.T) { 214 | testCase := func(r *testReader, shouldError bool) func(*testing.T) { 215 | return func(t *testing.T) { 216 | dst := new(bytes.Buffer) 217 | src := r 218 | ch := make(chan error) 219 | 220 | go copyCh(dst, src, ch) 221 | n := time.Now() 222 | 223 | select { 224 | case err := <-ch: 225 | if !shouldError && err != nil { 226 | t.Errorf("unexpected error: %v", err) 227 | } else if shouldError && err == nil { 228 | t.Errorf("expected error") 229 | } 230 | if time.Now().Sub(n) < r.MinTime() { 231 | t.Errorf("returned too fast") 232 | } 233 | case <-time.After(time.Second): 234 | t.Errorf("error channel not written to") 235 | } 236 | } 237 | } 238 | t.Run("NoError", testCase(&testReader{5, time.Millisecond * 50, 0, 0}, false)) 239 | t.Run("Error", testCase(&testReader{5, time.Millisecond * 50, 2, 0}, true)) 240 | } 241 | 242 | func TestCIDRBlackWhiteList(t *testing.T) { 243 | testCase := func(cidrList []*net.IPNet, isWhitelist bool, hosts []string, shouldFail bool) func(t *testing.T) { 244 | return func(t *testing.T) { 245 | for _, host := range hosts { 246 | err := checkCIDRBlackWhiteListHost(host, cidrList, isWhitelist) 247 | if err == nil && shouldFail { 248 | t.Errorf("expected %s to fail test for cidr list (isWhitelist=%t) %s", host, isWhitelist, cidrList) 249 | } else if err != nil && !shouldFail { 250 | t.Errorf("expected %s not to fail test for cidr list (isWhitelist=%t) %s", host, isWhitelist, cidrList) 251 | } 252 | } 253 | } 254 | } 255 | t.Run("WhitelistAllow", testCase(mustParseCIDRList("10.0.0.0/24,127.0.0.0/16"), true, []string{"10.0.0.1", "127.0.1.1", "10.0.0.9.ip.dns.geek1011.net"}, false)) 256 | t.Run("WhitelistBlock", testCase(mustParseCIDRList("10.0.0.0/24,127.0.0.0/16"), true, []string{"11.0.0.1", "1.0.1.1", "1.2.3.4.ip.dns.geek1011.net"}, true)) 257 | t.Run("BlacklistAllow", testCase(mustParseCIDRList("10.0.0.0/24,127.0.0.0/16"), false, []string{"11.0.0.1", "1.0.1.1", "1.2.3.4.ip.dns.geek1011.net"}, false)) 258 | t.Run("BlacklistBlock", testCase(mustParseCIDRList("10.0.0.0/24,127.0.0.0/16"), false, []string{"10.0.0.1", "127.0.1.1", "10.0.0.9.ip.dns.geek1011.net"}, true)) 259 | t.Run("WhitelistAllowv6", testCase(mustParseCIDRList("a:b:c:d:a:b:c:d/120"), true, []string{"a:b:c:d:a:b:c:d", "a:b:c:d:a:b:c:a", "a.b.c.d.a.b.c.d.ip.dns.geek1011.net"}, false)) 260 | t.Run("WhitelistBlockv6", testCase(mustParseCIDRList("a:b:c:d:a:b:c:d/120"), true, []string{"a:b:c:d:a:b:d:d", "a:b:c:d:a:b:d:a", "a.b.c.d.a.b.d.d.ip.dns.geek1011.net"}, true)) 261 | t.Run("BlacklistAllowv6", testCase(mustParseCIDRList("a:b:c:d:a:b:c:d/120"), false, []string{"a:b:c:d:a:b:d:d", "a:b:c:d:a:b:d:a", "a.b.c.d.a.b.d.d.ip.dns.geek1011.net"}, false)) 262 | t.Run("BlacklistBlockv6", testCase(mustParseCIDRList("a:b:c:d:a:b:c:d/120"), false, []string{"a:b:c:d:a:b:c:d", "a:b:c:d:a:b:c:a", "a.b.c.d.a.b.c.d.ip.dns.geek1011.net"}, true)) 263 | } 264 | 265 | func TestParseCIDRList(t *testing.T) { 266 | strs := []string{ 267 | "127.0.0.0/16", 268 | "192.168.0.0/24", 269 | "a:b:c:d:a:b:c:0/120", 270 | } 271 | cidrs, err := parseCIDRList(strs) 272 | if err != nil { 273 | t.Errorf("unexpected error: %v", err) 274 | return 275 | } 276 | for i, expected := range strs { 277 | if actual := cidrs[i].String(); expected != actual { 278 | t.Errorf("expected cidr %s at index %d, got %s", expected, i, actual) 279 | } 280 | } 281 | 282 | strs = []string{ 283 | "127.0.0.0/16", 284 | "192.168.0.0.123.4/24", 285 | "a:b:c:d:a:b:c:d/120", 286 | } 287 | _, err = parseCIDRList(strs) 288 | if err == nil { 289 | t.Errorf("expected error: when parsing erroneous list") 290 | } 291 | } 292 | 293 | func TestIPv6Regexp(t *testing.T) { 294 | re := regexp.MustCompile(ipv6Regexp) 295 | for _, ipv6 := range []string{ 296 | "1:2:3:4:5:6:7:8", 297 | "1::", 298 | "1:2:3:4:5:6:7::", 299 | "1::8", 300 | "1:2:3:4:5:6::8", 301 | "1:2:3:4:5:6::8", 302 | "1::7:8", 303 | "1:2:3:4:5::7:8", 304 | "1:2:3:4:5::8", 305 | "1::5:6:7:8", 306 | "1:2:3::5:6:7:8", 307 | "1:2:3::8", 308 | "1::4:5:6:7:8", 309 | "1:2::4:5:6:7:8", 310 | "1:2::8", 311 | "::2:3:4:5:6:7:8", 312 | "::2:3:4:5:6:7:8", 313 | "::8", 314 | "::", 315 | "fe80::7:8%eth0", 316 | "fe80::7:8%1", 317 | "::255.255.255.255", 318 | "::ffff:255.255.255.255", 319 | "::ffff:0:255.255.255.255", 320 | "2001:db8:3:4::192.0.2.33", 321 | "64:ff9b::192.0.2.33", 322 | } { 323 | if !re.MatchString(ipv6) { 324 | t.Errorf("expected regexp to match %#v", ipv6) 325 | } 326 | } 327 | } 328 | 329 | func TestMagicCheck(t *testing.T) { 330 | for _, tc := range []struct { 331 | Name string 332 | 333 | Magic []byte 334 | Input []byte 335 | 336 | EOFAt int 337 | Failed bool 338 | }{ 339 | {"Good_BothEmpty", []byte(""), []byte(""), 0, false}, 340 | {"Good_EmptyMagicWithInput", []byte(""), []byte(" "), 1, false}, 341 | {"Good_EmptyInputWithMagic", []byte("RFB"), []byte(""), 0, false}, 342 | {"Good_ExactMatch", []byte("RFB"), []byte("RFB"), 3, false}, 343 | {"Good_ExactMatchWithExtra", []byte("RFB"), []byte("RFB 005.000"), 11, false}, 344 | {"Bad_NoMatch", []byte("RFB"), []byte("..."), 0, true}, 345 | {"Bad_PartialMatch", []byte("RFB"), []byte("R.."), 1, true}, 346 | } { 347 | t.Run(tc.Name, func(t *testing.T) { 348 | m := newMagicCheck(bytes.NewReader(tc.Input), tc.Magic) 349 | var buf []byte 350 | 351 | rbuf := make([]byte, 1) 352 | for { 353 | n, err := m.Read(rbuf) 354 | if err == io.EOF { 355 | if n, err := m.Read(rbuf); err != io.EOF || n != 0 { 356 | t.Errorf("expected io.EOF to stick with no bytes read") 357 | } 358 | if tc.EOFAt < 0 { 359 | t.Errorf("unexpected eof after %d bytes", len(buf)) 360 | } else if len(buf) != tc.EOFAt { 361 | t.Errorf("unexpected eof after %d bytes, expected %d bytes (buf: %s)", len(buf), tc.EOFAt, string(buf)) 362 | } else if m.Failed() != tc.Failed { 363 | t.Errorf("expected failed=%t, got %t", tc.Failed, m.Failed()) 364 | } else if !m.Failed() && len(tc.Input) >= len(tc.Magic) && !bytes.Equal(m.Magic(), tc.Magic) { 365 | t.Errorf("shouldn't have passed the magic check: %s != %s", string(m.Magic()), string(tc.Magic)) 366 | } 367 | break 368 | } else if err != nil { 369 | panic(err) 370 | } else if n > 0 { 371 | buf = append(buf, rbuf[:n]...) 372 | } 373 | } 374 | }) 375 | } 376 | } 377 | 378 | // testReader is a custom io.Reader which throttles the reads and can return 379 | // an error at a specific point. 380 | type testReader struct { 381 | N int 382 | Delay time.Duration 383 | Errn int 384 | v int 385 | } 386 | 387 | func (t *testReader) Read(buf []byte) (int, error) { 388 | if t.v >= t.N { 389 | return 0, io.EOF 390 | } 391 | 392 | t.v++ 393 | time.Sleep(t.Delay) 394 | 395 | if t.Errn == t.v { 396 | return 1, errors.New("test error") 397 | } 398 | 399 | buf[0] = 0xFF 400 | return 1, nil 401 | } 402 | 403 | func (t *testReader) MinTime() time.Duration { 404 | if t.Errn < t.N { 405 | return t.Delay * time.Duration(t.Errn) 406 | } 407 | return t.Delay * time.Duration(t.N) 408 | } 409 | 410 | func mustParseCIDRList(str string) []*net.IPNet { 411 | cidrs, err := parseCIDRList(strings.Split(str, ",")) 412 | if err != nil { 413 | panic(err) 414 | } 415 | return cidrs 416 | } 417 | -------------------------------------------------------------------------------- /wstcp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13-alpine AS build 2 | ADD . /src 3 | WORKDIR /src 4 | RUN apk add --no-cache git 5 | RUN go mod download 6 | RUN go build -o ./wstcp/wstcp ./wstcp 7 | 8 | FROM alpine:latest 9 | COPY --from=build /src/wstcp/wstcp / 10 | EXPOSE 5900 11 | ENTRYPOINT ["/wstcp"] -------------------------------------------------------------------------------- /wstcp/README.md: -------------------------------------------------------------------------------- 1 | # wstcp 2 | Tunnels local VNC connections over TCP to an easy-novnc server over WebSockets. 3 | 4 | ## Installation 5 | - A Docker image is available, and can be used like: `docker run -p 5900 --rm -it geek1011/easy-novnc:wstcp-latest proxy_host ...`. 6 | - Binaries for the latest commit can be downloaded [here](https://ci.appveyor.com/project/pgaskin/easy-novnc/build/artifacts). 7 | - You can build your own binaries with go 1.13 or newer using `go get github.com/pgaskin/easy-novnc/wstcp` or by cloning this repo and running `go build ./wstcp`. 8 | 9 | ## Usage 10 | ``` 11 | Usage: wstcp [options] proxy_host [target_host [target_port]] 12 | 13 | Options: 14 | --help Show this help text 15 | -l, --listen string Address to listen for connections on (default ":5900") 16 | 17 | 18 | Arguments: 19 | proxy_host The easy-novnc server in the format [http[s]://]hostname[:port]. 20 | If the protocol isn't specified, it is autodetected. 21 | target_host The target address to connect to. Requires --arbitrary-hosts to 22 | be set on the server. 23 | target_port The target port to connect to. Requires --arbitrary-ports to be 24 | set on the server. 25 | ``` 26 | -------------------------------------------------------------------------------- /wstcp/wstcp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net" 8 | "net/http" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/spf13/pflag" 14 | "golang.org/x/net/websocket" 15 | ) 16 | 17 | func main() { 18 | retry := pflag.IntP("retry", "r", -1, "Interval (seconds) to retry initial connection on failure") 19 | listen := pflag.StringP("listen", "l", ":5900", "Address to listen for connections on") 20 | help := pflag.Bool("help", false, "Show this help text") 21 | pflag.Parse() 22 | 23 | if *help || pflag.NArg() < 1 || pflag.NArg() > 3 { 24 | fmt.Fprintf(os.Stderr, "Usage: %s [options] proxy_host [target_host [target_port]]\n\nOptions:\n", os.Args[0]) 25 | pflag.PrintDefaults() 26 | fmt.Fprintf(os.Stderr, "\n\nArguments:\n") 27 | fmt.Fprintf(os.Stderr, " proxy_host The easy-novnc server in the format [http[s]://]hostname[:port].\n If the protocol isn't specified, it is autodetected.\n") 28 | fmt.Fprintf(os.Stderr, " target_host The target address to connect to. Requires --arbitrary-hosts to\n be set on the server.\n") 29 | fmt.Fprintf(os.Stderr, " target_port The target port to connect to. Requires --arbitrary-ports to be\n set on the server.\n") 30 | os.Exit(2) 31 | } 32 | 33 | host, addr, port := pflag.Arg(0), pflag.Arg(1), pflag.Arg(2) 34 | 35 | var url string 36 | for { 37 | host, err := detect(host, true) 38 | if err != nil { 39 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 40 | if *retry >= 0 { 41 | fmt.Fprintf(os.Stderr, "Retrying after %d seconds...\n", *retry) 42 | time.Sleep(time.Second * time.Duration(*retry)) 43 | continue 44 | } 45 | os.Exit(1) 46 | } 47 | 48 | url = host + "/vnc" 49 | if addr != "" { 50 | url += "/" + addr 51 | if port != "" { 52 | url += "/" + port 53 | } 54 | } 55 | 56 | fmt.Printf("Testing connection to %s.\n", url) 57 | if err := check(url); err != nil { 58 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 59 | if *retry >= 0 { 60 | fmt.Fprintf(os.Stderr, "Retrying after %d seconds...\n", *retry) 61 | time.Sleep(time.Second * time.Duration(*retry)) 62 | continue 63 | } 64 | os.Exit(1) 65 | } 66 | 67 | break 68 | } 69 | 70 | fmt.Printf("Listening %s => %s.\n", *listen, url) 71 | if err := wstun(*listen, url); err != nil { 72 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 73 | os.Exit(1) 74 | } 75 | } 76 | 77 | func detect(host string, verbose bool) (string, error) { 78 | if strings.Contains(host, "://") { 79 | if verbose { 80 | fmt.Printf("Testing connection to %s.\n", host) 81 | } 82 | resp, err := (&http.Client{Timeout: time.Second}).Get(host) 83 | if err != nil { 84 | return "", err 85 | } else if resp.StatusCode != http.StatusOK { 86 | return "", fmt.Errorf("unexpected status %s", resp.Status) 87 | } 88 | resp.Body.Close() 89 | return host, nil 90 | } 91 | 92 | if verbose { 93 | fmt.Printf("No protocol specified, autodetecting.\n") 94 | } 95 | 96 | c := &http.Client{ 97 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 98 | if req.URL.Scheme != via[len(via)-1].URL.Scheme { 99 | return http.ErrUseLastResponse 100 | } 101 | return nil 102 | }, 103 | Timeout: time.Second, 104 | } 105 | 106 | var err error 107 | orig := host 108 | for _, proto := range []string{"https", "http"} { 109 | host = fmt.Sprintf("%s://%s", proto, orig) 110 | if verbose { 111 | fmt.Printf("... trying %s", host) 112 | } 113 | 114 | resp, herr := c.Get(host) 115 | if herr != nil { 116 | err = fmt.Errorf("proto %s: %v", proto, herr) 117 | if verbose { 118 | fmt.Printf(": %v\n", err) 119 | } 120 | continue 121 | } 122 | resp.Body.Close() 123 | 124 | if resp.StatusCode != http.StatusOK { 125 | err = fmt.Errorf("unexpected status %s", resp.Status) 126 | if verbose { 127 | fmt.Printf(": %v\n", err) 128 | } 129 | continue 130 | } 131 | 132 | if verbose { 133 | fmt.Printf(": ok\n") 134 | } 135 | return host, nil 136 | } 137 | return "", err 138 | } 139 | 140 | func check(url string) error { 141 | resp, err := http.Get(url) 142 | if err != nil { 143 | return err 144 | } 145 | defer resp.Body.Close() 146 | 147 | if resp.StatusCode == http.StatusUnauthorized { 148 | buf, err := ioutil.ReadAll(resp.Body) 149 | if err != nil { 150 | return err 151 | } 152 | return fmt.Errorf("easy-novnc server: %s", string(buf)) 153 | } 154 | return nil 155 | } 156 | 157 | func wstun(listen, target string) error { 158 | listener, err := net.Listen("tcp", listen) 159 | if err != nil { 160 | return err 161 | } 162 | var i int 163 | for { 164 | i++ 165 | conn, err := listener.Accept() 166 | 167 | fmt.Printf("Accepted connection %d from %s\n", i, conn.RemoteAddr()) 168 | if nerr, ok := err.(net.Error); ok && nerr.Temporary() { 169 | fmt.Printf("Warning: connection %d: temporary error: %v, trying again in 100ms\n", i, err) 170 | time.Sleep(time.Millisecond * 100) 171 | continue 172 | } else if err != nil { 173 | return err 174 | } 175 | 176 | go func(i int, conn net.Conn) { 177 | wsconn, err := websocket.Dial(strings.Replace(target, "http", "ws", 1), "binary", target) 178 | if err != nil { 179 | fmt.Printf("Warning: connection %d: dial target websocket: %v, closing connection\n", i, err) 180 | conn.Close() 181 | fmt.Printf("Connection %d closed\n", i) 182 | return 183 | } 184 | 185 | done := make(chan error) 186 | go copyCh(wsconn, conn, done) 187 | go copyCh(conn, wsconn, done) 188 | 189 | if err := <-done; err != nil { 190 | fmt.Printf("Warning: connection %d: %v, closing connection\n", i, err) 191 | wsconn.Close() 192 | conn.Close() 193 | fmt.Printf("Connection %d closed\n", i) 194 | return 195 | } 196 | 197 | wsconn.Close() 198 | conn.Close() 199 | <-done 200 | 201 | fmt.Printf("Connection %d closed\n", i) 202 | }(i, conn) 203 | } 204 | } 205 | 206 | func copyCh(dst io.Writer, src io.Reader, done chan error) { 207 | _, err := io.Copy(dst, src) 208 | done <- err 209 | } 210 | --------------------------------------------------------------------------------