mirror of
https://github.com/sorenisanerd/gotty.git
synced 2024-11-23 20:54:24 +00:00
Remove a bunch of superfluous stuff
This commit is contained in:
parent
ac61302d18
commit
1729ed42c1
@ -1,44 +0,0 @@
|
|||||||
# How to contribute
|
|
||||||
|
|
||||||
GoTTY is MIT licensed and accepts contributions via GitHub pull requests. We also accepts feature requests on GitHub issues.
|
|
||||||
|
|
||||||
## Reporting a bug
|
|
||||||
|
|
||||||
Reporting a bug is always welcome and one of the best ways to contribute. A good bug report helps the developers to improve the product much easier. We therefore would like to ask you to fill out the quesions on the issue template as much as possible. That helps us to figure out what's happening and discover the root cause.
|
|
||||||
|
|
||||||
|
|
||||||
## Requesting a new feature
|
|
||||||
|
|
||||||
When you find that GoTTY cannot fullfill your requirements because of lack of ability, you may want to open a new feature request. In that case, please file a new issue with your usecase and requirements.
|
|
||||||
|
|
||||||
|
|
||||||
## Opening a pull request
|
|
||||||
|
|
||||||
### Code Style
|
|
||||||
|
|
||||||
Please run `go fmt` on your Go code and make sure that your commits are organized for each logical change and your commit messages are in proper format (see below).
|
|
||||||
|
|
||||||
[Go's official code style guide](https://github.com/golang/go/wiki/CodeReviewComments) is also helpful.
|
|
||||||
|
|
||||||
### Format of the commit message
|
|
||||||
|
|
||||||
When you write a commit message, we recommend include following information to make review easier and keep the history cleaerer.
|
|
||||||
|
|
||||||
* What is the change
|
|
||||||
* The reason for the change
|
|
||||||
|
|
||||||
The following is an example:
|
|
||||||
|
|
||||||
```
|
|
||||||
Add something new to existing package
|
|
||||||
|
|
||||||
Since the existing function lacks that mechanism for some purpose,
|
|
||||||
this commit adds a new structure to provide it.
|
|
||||||
```
|
|
||||||
|
|
||||||
When your pull request is to add a new feature, we recommend add an actual usecase so that we can discuss the best way to achive your requirement. Opening a proposal issue in advance is another good way to start discussion of new features.
|
|
||||||
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
If you have a trivial question about GoTTY for a bug or new feature, you can contact @sorenisanerd on Twitter (unfortunately, I cannot provide support on GoTTY though).
|
|
15
Dockerfile
15
Dockerfile
@ -1,15 +0,0 @@
|
|||||||
FROM golang:1.13.1
|
|
||||||
|
|
||||||
WORKDIR /gotty
|
|
||||||
COPY . /gotty
|
|
||||||
RUN CGO_ENABLED=0 make
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
RUN apk update && \
|
|
||||||
apk upgrade && \
|
|
||||||
apk --no-cache add ca-certificates && \
|
|
||||||
apk add bash
|
|
||||||
WORKDIR /root
|
|
||||||
COPY --from=0 /gotty/gotty /usr/bin/
|
|
||||||
CMD ["gotty", "-w", "bash"]
|
|
98
Makefile
98
Makefile
@ -1,98 +0,0 @@
|
|||||||
OUTPUT_DIR = ./builds
|
|
||||||
GIT_COMMIT = `git rev-parse HEAD | cut -c1-7`
|
|
||||||
VERSION = 2.1.0
|
|
||||||
BUILD_OPTIONS = -ldflags "-X main.Version=$(VERSION) -X main.CommitID=$(GIT_COMMIT)"
|
|
||||||
|
|
||||||
gotty: main.go server/*.go webtty/*.go backend/*.go Makefile asset
|
|
||||||
go build ${BUILD_OPTIONS}
|
|
||||||
|
|
||||||
docker:
|
|
||||||
docker build . -t gotty-bash:$(VERSION)
|
|
||||||
|
|
||||||
.PHONY: asset
|
|
||||||
asset: bindata/static/js/gotty.js bindata/static/index.html bindata/static/favicon.png bindata/static/css/index.css bindata/static/css/xterm.css bindata/static/css/xterm_customize.css bindata/static/manifest.json bindata/static/icon_192.png server/asset.go
|
|
||||||
|
|
||||||
server/asset.go:
|
|
||||||
go-bindata -prefix bindata -pkg server -ignore=\\.gitkeep -o server/asset.go bindata/...
|
|
||||||
gofmt -w server/asset.go
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: asset gotty docker
|
|
||||||
|
|
||||||
bindata:
|
|
||||||
mkdir bindata
|
|
||||||
|
|
||||||
bindata/static: bindata
|
|
||||||
mkdir bindata/static
|
|
||||||
|
|
||||||
bindata/static/index.html: bindata/static resources/index.html
|
|
||||||
cp resources/index.html bindata/static/index.html
|
|
||||||
|
|
||||||
bindata/static/manifest.json: bindata/static resources/manifest.json
|
|
||||||
cp resources/manifest.json bindata/static/manifest.json
|
|
||||||
|
|
||||||
bindata/static/favicon.png: bindata/static resources/favicon.png
|
|
||||||
cp resources/favicon.png bindata/static/favicon.png
|
|
||||||
|
|
||||||
bindata/static/icon_192.png: bindata/static resources/icon_192.png
|
|
||||||
cp resources/icon_192.png bindata/static/icon_192.png
|
|
||||||
|
|
||||||
bindata/static/js: bindata/static
|
|
||||||
mkdir -p bindata/static/js
|
|
||||||
|
|
||||||
bindata/static/css: bindata/static
|
|
||||||
mkdir -p bindata/static/css
|
|
||||||
|
|
||||||
bindata/static/css/index.css: bindata/static/css resources/index.css
|
|
||||||
cp resources/index.css bindata/static/css/index.css
|
|
||||||
|
|
||||||
bindata/static/css/xterm_customize.css: bindata/static/css resources/xterm_customize.css
|
|
||||||
cp resources/xterm_customize.css bindata/static/css/xterm_customize.css
|
|
||||||
|
|
||||||
bindata/static/css/xterm.css: bindata/static/css js/node_modules/xterm/dist/xterm.css
|
|
||||||
cp js/node_modules/xterm/dist/xterm.css bindata/static/css/xterm.css
|
|
||||||
|
|
||||||
js/node_modules/xterm/dist/xterm.css:
|
|
||||||
cd js && \
|
|
||||||
npm install
|
|
||||||
|
|
||||||
bindata/static/js/gotty.js: js/src/* js/node_modules/webpack
|
|
||||||
cd js && \
|
|
||||||
npx webpack
|
|
||||||
|
|
||||||
js/node_modules/webpack:
|
|
||||||
cd js && \
|
|
||||||
npm install
|
|
||||||
|
|
||||||
README.md: README.md.in
|
|
||||||
(cat $< ; git log --pretty=format:' * %aN' | \
|
|
||||||
grep -v 'S.*ren L. Hansen' | \
|
|
||||||
grep -v 'Iwasaki Yudai' | \
|
|
||||||
sort -u ) > $@
|
|
||||||
|
|
||||||
tools:
|
|
||||||
go get github.com/tools/godep
|
|
||||||
go get github.com/mitchellh/gox
|
|
||||||
go get github.com/tcnksm/ghr
|
|
||||||
go get github.com/jteeuwen/go-bindata/...
|
|
||||||
|
|
||||||
test:
|
|
||||||
if [ `go fmt $(go list ./... | grep -v /vendor/) | wc -l` -gt 0 ]; then echo "go fmt error"; exit 1; fi
|
|
||||||
|
|
||||||
cross_compile:
|
|
||||||
GOARM=5 gox -os="darwin linux freebsd netbsd openbsd solaris" -arch="386 amd64 arm" -osarch="!darwin/386" -osarch="!darwin/arm" -output "${OUTPUT_DIR}/pkg/{{.OS}}_{{.Arch}}/{{.Dir}}"
|
|
||||||
|
|
||||||
targz:
|
|
||||||
mkdir -p ${OUTPUT_DIR}/dist
|
|
||||||
cd ${OUTPUT_DIR}/pkg/; for osarch in *; do (cd $$osarch; tar zcvf ../../dist/gotty_${VERSION}_$$osarch.tar.gz ./*); done;
|
|
||||||
|
|
||||||
shasums:
|
|
||||||
cd ${OUTPUT_DIR}/dist; sha256sum * > ./SHA256SUMS
|
|
||||||
|
|
||||||
release-artifacts: asset gotty cross_compile targz shasums
|
|
||||||
|
|
||||||
release:
|
|
||||||
ghr -draft -prerelease ${VERSION} ${OUTPUT_DIR}/dist # -c ${GIT_COMMIT} --delete --prerelease -u sorenisanerd -r gotty ${VERSION}
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -fr gotty builds bindata server/asset.go js/dist
|
|
11
NEWS.md
11
NEWS.md
@ -1,11 +0,0 @@
|
|||||||
# GoTTY releases
|
|
||||||
|
|
||||||
## v2.1.0 (unreleased)
|
|
||||||
|
|
||||||
* Use Go modules and update cli module import path (Thanks, @svanellewee
|
|
||||||
* Fix typos (Thanks, @0xflotus, @RealCyGuy, @ygit, @Jason-Cooke and @fredster33!)
|
|
||||||
* Fix printing of ipv6 addresses (Thanks, @Felixoid!)
|
|
||||||
* Add Progressive Web App support (Thanks, @sehaas!)
|
|
||||||
* Add instructions for GNU screen (Thanks, @Immortalin!)
|
|
||||||
* Add Solaris support (Thanks, @fazalmajid!)
|
|
||||||
* New maintainer: @sorenisanerd
|
|
237
README.md
237
README.md
@ -1,237 +0,0 @@
|
|||||||
# ![](https://raw.githubusercontent.com/sorenisanerd/gotty/master/resources/favicon.png) GoTTY - Share your terminal as a web application
|
|
||||||
|
|
||||||
[![GitHub release](http://img.shields.io/github/release/sorenisanerd/gotty.svg?style=flat-square)][release]
|
|
||||||
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)][license]
|
|
||||||
|
|
||||||
[release]: https://github.com/sorenisanerd/gotty/releases
|
|
||||||
[license]: https://github.com/sorenisanerd/gotty/blob/master/LICENSE
|
|
||||||
|
|
||||||
GoTTY is a simple command line tool that turns your CLI tools into web applications.
|
|
||||||
|
|
||||||
![Screenshot](https://raw.githubusercontent.com/sorenisanerd/gotty/master/screenshot.gif)
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
Download the latest stable binary file from the [Releases](https://github.com/sorenisanerd/gotty/releases) page. Note that the release marked `Pre-release` is built for testing purpose, which can include unstable or breaking changes. Download a release marked [Latest release](https://github.com/sorenisanerd/gotty/releases/latest) for a stable build.
|
|
||||||
|
|
||||||
(Files named with `darwin_amd64` are for Mac OS X users)
|
|
||||||
|
|
||||||
## Homebrew Installation
|
|
||||||
|
|
||||||
You can install GoTTY with [Homebrew](http://brew.sh/) as well.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ brew install yudai/gotty/gotty
|
|
||||||
```
|
|
||||||
|
|
||||||
## `go get` Installation (Development)
|
|
||||||
|
|
||||||
If you have a Go language environment, you can install GoTTY with the `go get` command. However, this command builds a binary file from the latest master branch, which can include unstable or breaking changes. GoTTY requires go1.9 or later.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ go get github.com/sorenisanerd/gotty
|
|
||||||
```
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
Usage: gotty [options] <command> [<arguments...>]
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `gotty` with your preferred command as its arguments (e.g. `gotty top`).
|
|
||||||
|
|
||||||
By default, GoTTY starts a web server at port 8080. Open the URL on your web browser and you can see the running command as if it were running on your terminal.
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
```
|
|
||||||
--address value, -a value IP address to listen (default: "0.0.0.0") [$GOTTY_ADDRESS]
|
|
||||||
--port value, -p value Port number to listen (default: "8080") [$GOTTY_PORT]
|
|
||||||
--permit-write, -w Permit clients to write to the TTY (BE CAREFUL) [$GOTTY_PERMIT_WRITE]
|
|
||||||
--credential value, -c value Credential for Basic Authentication (ex: user:pass, default disabled) [$GOTTY_CREDENTIAL]
|
|
||||||
--random-url, -r Add a random string to the URL [$GOTTY_RANDOM_URL]
|
|
||||||
--random-url-length value Random URL length (default: 8) [$GOTTY_RANDOM_URL_LENGTH]
|
|
||||||
--tls, -t Enable TLS/SSL [$GOTTY_TLS]
|
|
||||||
--tls-crt value TLS/SSL certificate file path (default: "~/.gotty.crt") [$GOTTY_TLS_CRT]
|
|
||||||
--tls-key value TLS/SSL key file path (default: "~/.gotty.key") [$GOTTY_TLS_KEY]
|
|
||||||
--tls-ca-crt value TLS/SSL CA certificate file for client certifications (default: "~/.gotty.ca.crt") [$GOTTY_TLS_CA_CRT]
|
|
||||||
--index value Custom index.html file [$GOTTY_INDEX]
|
|
||||||
--title-format value Title format of browser window (default: "{{ .command }}@{{ .hostname }}") [$GOTTY_TITLE_FORMAT]
|
|
||||||
--reconnect Enable reconnection [$GOTTY_RECONNECT]
|
|
||||||
--reconnect-time value Time to reconnect (default: 10) [$GOTTY_RECONNECT_TIME]
|
|
||||||
--max-connection value Maximum connection to gotty (default: 0) [$GOTTY_MAX_CONNECTION]
|
|
||||||
--once Accept only one client and exit on disconnection [$GOTTY_ONCE]
|
|
||||||
--timeout value Timeout seconds for waiting a client(0 to disable) (default: 0) [$GOTTY_TIMEOUT]
|
|
||||||
--permit-arguments Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB) [$GOTTY_PERMIT_ARGUMENTS]
|
|
||||||
--width value Static width of the screen, 0(default) means dynamically resize (default: 0) [$GOTTY_WIDTH]
|
|
||||||
--height value Static height of the screen, 0(default) means dynamically resize (default: 0) [$GOTTY_HEIGHT]
|
|
||||||
--ws-origin value A regular expression that matches origin URLs to be accepted by WebSocket. No cross origin requests are acceptable by default [$GOTTY_WS_ORIGIN]
|
|
||||||
--term value Terminal name to use on the browser, one of xterm or hterm. (default: "xterm") [$GOTTY_TERM]
|
|
||||||
--close-signal value Signal sent to the command process when gotty close it (default: SIGHUP) (default: 1) [$GOTTY_CLOSE_SIGNAL]
|
|
||||||
--close-timeout value Time in seconds to force kill process after client is disconnected (default: -1) (default: -1) [$GOTTY_CLOSE_TIMEOUT]
|
|
||||||
--config value Config file path (default: "~/.gotty") [$GOTTY_CONFIG]
|
|
||||||
--version, -v print the version
|
|
||||||
```
|
|
||||||
|
|
||||||
### Config File
|
|
||||||
|
|
||||||
You can customize default options and your terminal (hterm) by providing a config file to the `gotty` command. GoTTY loads a profile file at `~/.gotty` by default when it exists.
|
|
||||||
|
|
||||||
```
|
|
||||||
// Listen at port 9000 by default
|
|
||||||
port = "9000"
|
|
||||||
|
|
||||||
// Enable TSL/SSL by default
|
|
||||||
enable_tls = true
|
|
||||||
|
|
||||||
// hterm preferences
|
|
||||||
// Smaller font and a little bit bluer background color
|
|
||||||
preferences {
|
|
||||||
font_size = 5
|
|
||||||
background_color = "rgb(16, 16, 32)"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [`.gotty`](https://github.com/sorenisanerd/gotty/blob/master/.gotty) file in this repository for the list of configuration options.
|
|
||||||
|
|
||||||
### Security Options
|
|
||||||
|
|
||||||
By default, GoTTY doesn't allow clients to send any keystrokes or commands except terminal window resizing. When you want to permit clients to write input to the TTY, add the `-w` option. However, accepting input from remote clients is dangerous for most commands. When you need interaction with the TTY for some reasons, consider starting GoTTY with tmux or GNU Screen and run your command on it (see "Sharing with Multiple Clients" section for detail).
|
|
||||||
|
|
||||||
To restrict client access, you can use the `-c` option to enable the basic authentication. With this option, clients need to input the specified username and password to connect to the GoTTY server. Note that the credentials will be transmitted between the server and clients in plain text. For more strict authentication, consider the SSL/TLS client certificate authentication described below.
|
|
||||||
|
|
||||||
The `-r` option is a little bit casualer way to restrict access. With this option, GoTTY generates a random URL so that only people who know the URL can get access to the server.
|
|
||||||
|
|
||||||
All traffic between the server and clients are NOT encrypted by default. When you send secret information through GoTTY, we strongly recommend you use the `-t` option which enables TLS/SSL on the session. By default, GoTTY loads the crt and key files placed at `~/.gotty.crt` and `~/.gotty.key`. You can overwrite these file paths with the `--tls-crt` and `--tls-key` options. When you need to generate a self-signed certification file, you can use the `openssl` command.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
openssl req -x509 -nodes -days 9999 -newkey rsa:2048 -keyout ~/.gotty.key -out ~/.gotty.crt
|
|
||||||
```
|
|
||||||
|
|
||||||
(NOTE: For Safari uses, see [how to enable self-signed certificates for WebSockets](http://blog.marcon.me/post/24874118286/secure-websockets-safari) when use self-signed certificates)
|
|
||||||
|
|
||||||
For additional security, you can use the SSL/TLS client certificate authentication by providing a CA certificate file to the `--tls-ca-crt` option (this option requires the `-t` or `--tls` to be set). This option requires all clients to send valid client certificates that are signed by the specified certification authority.
|
|
||||||
|
|
||||||
## Sharing with Multiple Clients
|
|
||||||
|
|
||||||
GoTTY starts a new process with the given command when a new client connects to the server. This means users cannot share a single terminal with others by default. However, you can use terminal multiplexers for sharing a single process with multiple clients.
|
|
||||||
### Screen
|
|
||||||
After installing GNU screen, start a new session with `screen -S name-for-session` and connect to it with gotty in another terminal window/tab through `screen -x name-for-session`. All commands and activities being done in the first terminal tab/window will now be broadcasted by gotty.
|
|
||||||
### Tmux
|
|
||||||
For example, you can start a new tmux session named `gotty` with `top` command by the command below.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ gotty tmux new -A -s gotty top
|
|
||||||
```
|
|
||||||
|
|
||||||
This command doesn't allow clients to send keystrokes, however, you can attach the session from your local terminal and run operations like switching the mode of the `top` command. To connect to the tmux session from your terminal, you can use following command.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ tmux new -A -s gotty
|
|
||||||
```
|
|
||||||
|
|
||||||
By using terminal multiplexers, you can have the control of your terminal and allow clients to just see your screen.
|
|
||||||
|
|
||||||
### Quick Sharing on tmux
|
|
||||||
|
|
||||||
To share your current session with others by a shortcut key, you can add a line like below to your `.tmux.conf`.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Start GoTTY in a new window with C-t
|
|
||||||
bind-key C-t new-window "gotty tmux attach -t `tmux display -p '#S'`"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Playing with Docker
|
|
||||||
|
|
||||||
When you want to create a jailed environment for each client, you can use Docker containers like following:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ gotty -w docker run -it --rm busybox
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
You can build a binary using the following commands. Windows is not supported now. go1.9 is required.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Install tools
|
|
||||||
go get github.com/jteeuwen/go-bindata/...
|
|
||||||
go get github.com/tools/godep
|
|
||||||
|
|
||||||
# Build
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
To build the frontend part (JS files and other static files), you need `npm`.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
GoTTY uses [xterm.js](https://xtermjs.org/) and [hterm](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm) to run a JavaScript based terminal on web browsers. GoTTY itself provides a websocket server that simply relays output from the TTY to clients and receives input from clients and forwards it to the TTY. This hterm + websocket idea is inspired by [Wetty](https://github.com/krishnasrinivas/wetty).
|
|
||||||
|
|
||||||
## Alternatives
|
|
||||||
|
|
||||||
### Command line client
|
|
||||||
|
|
||||||
* [gotty-client](https://github.com/moul/gotty-client): If you want to connect to GoTTY server from your terminal
|
|
||||||
|
|
||||||
### Terminal/SSH on Web Browsers
|
|
||||||
|
|
||||||
* [Secure Shell (Chrome App)](https://chrome.google.com/webstore/detail/secure-shell/pnhechapfaindjhompbnflcldabbghjo): If you are a chrome user and need a "real" SSH client on your web browser, perhaps the Secure Shell app is what you want
|
|
||||||
* [Wetty](https://github.com/krishnasrinivas/wetty): Node based web terminal (SSH/login)
|
|
||||||
* [ttyd](https://tsl0922.github.io/ttyd): C port of GoTTY with CJK and IME support
|
|
||||||
|
|
||||||
### Terminal Sharing
|
|
||||||
|
|
||||||
* [tmate](http://tmate.io/): Forked-Tmux based Terminal-Terminal sharing
|
|
||||||
* [termshare](https://termsha.re): Terminal-Terminal sharing through a HTTP server
|
|
||||||
* [tmux](https://tmux.github.io/): Tmux itself also supports TTY sharing through SSH)
|
|
||||||
|
|
||||||
# License
|
|
||||||
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
# Contributors
|
|
||||||
|
|
||||||
## Original author
|
|
||||||
|
|
||||||
* Iwasaki Yudai
|
|
||||||
|
|
||||||
## Maintainer
|
|
||||||
|
|
||||||
* Søren L. Hansen
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
* 0xflotus
|
|
||||||
* Andrea Lusuardi - uovobw
|
|
||||||
* Andy Skelton
|
|
||||||
* Artem Medvedev
|
|
||||||
* Blake Jennings
|
|
||||||
* Christian Jensen
|
|
||||||
* Christopher Wilkinson
|
|
||||||
* Cyrus
|
|
||||||
* David Horsley
|
|
||||||
* Fazal Majid
|
|
||||||
* freakhill
|
|
||||||
* fredster33
|
|
||||||
* Jan-Willem Korver
|
|
||||||
* Jason Cooke
|
|
||||||
* Johan Gall
|
|
||||||
* Korenevskiy Denis
|
|
||||||
* Lin
|
|
||||||
* Manfred Touron
|
|
||||||
* Massimiliano Stucchi
|
|
||||||
* mattn
|
|
||||||
* Mikhail f. Shiryaev
|
|
||||||
* Quentin Perez
|
|
||||||
* Richard Metzler
|
|
||||||
* Robert Bittle
|
|
||||||
* Sebastian Haas
|
|
||||||
* shingt
|
|
||||||
* Shoji Ihara
|
|
||||||
* Shuanglei Tao
|
|
||||||
* Stephan van Ellewee
|
|
||||||
* The Gitter Badger
|
|
||||||
* Xinyun Zhou
|
|
||||||
* Yifa Zhang
|
|
||||||
* yogesh singh
|
|
||||||
* zlji
|
|
203
README.md.in
203
README.md.in
@ -1,203 +0,0 @@
|
|||||||
# ![](https://raw.githubusercontent.com/sorenisanerd/gotty/master/resources/favicon.png) GoTTY - Share your terminal as a web application
|
|
||||||
|
|
||||||
[![GitHub release](http://img.shields.io/github/release/sorenisanerd/gotty.svg?style=flat-square)][release]
|
|
||||||
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)][license]
|
|
||||||
|
|
||||||
[release]: https://github.com/sorenisanerd/gotty/releases
|
|
||||||
[license]: https://github.com/sorenisanerd/gotty/blob/master/LICENSE
|
|
||||||
|
|
||||||
GoTTY is a simple command line tool that turns your CLI tools into web applications.
|
|
||||||
|
|
||||||
![Screenshot](https://raw.githubusercontent.com/sorenisanerd/gotty/master/screenshot.gif)
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
Download the latest stable binary file from the [Releases](https://github.com/sorenisanerd/gotty/releases) page. Note that the release marked `Pre-release` is built for testing purpose, which can include unstable or breaking changes. Download a release marked [Latest release](https://github.com/sorenisanerd/gotty/releases/latest) for a stable build.
|
|
||||||
|
|
||||||
(Files named with `darwin_amd64` are for Mac OS X users)
|
|
||||||
|
|
||||||
## Homebrew Installation
|
|
||||||
|
|
||||||
You can install GoTTY with [Homebrew](http://brew.sh/) as well.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ brew install yudai/gotty/gotty
|
|
||||||
```
|
|
||||||
|
|
||||||
## `go get` Installation (Development)
|
|
||||||
|
|
||||||
If you have a Go language environment, you can install GoTTY with the `go get` command. However, this command builds a binary file from the latest master branch, which can include unstable or breaking changes. GoTTY requires go1.9 or later.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ go get github.com/sorenisanerd/gotty
|
|
||||||
```
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
Usage: gotty [options] <command> [<arguments...>]
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `gotty` with your preferred command as its arguments (e.g. `gotty top`).
|
|
||||||
|
|
||||||
By default, GoTTY starts a web server at port 8080. Open the URL on your web browser and you can see the running command as if it were running on your terminal.
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
```
|
|
||||||
--address value, -a value IP address to listen (default: "0.0.0.0") [$GOTTY_ADDRESS]
|
|
||||||
--port value, -p value Port number to listen (default: "8080") [$GOTTY_PORT]
|
|
||||||
--permit-write, -w Permit clients to write to the TTY (BE CAREFUL) [$GOTTY_PERMIT_WRITE]
|
|
||||||
--credential value, -c value Credential for Basic Authentication (ex: user:pass, default disabled) [$GOTTY_CREDENTIAL]
|
|
||||||
--random-url, -r Add a random string to the URL [$GOTTY_RANDOM_URL]
|
|
||||||
--random-url-length value Random URL length (default: 8) [$GOTTY_RANDOM_URL_LENGTH]
|
|
||||||
--tls, -t Enable TLS/SSL [$GOTTY_TLS]
|
|
||||||
--tls-crt value TLS/SSL certificate file path (default: "~/.gotty.crt") [$GOTTY_TLS_CRT]
|
|
||||||
--tls-key value TLS/SSL key file path (default: "~/.gotty.key") [$GOTTY_TLS_KEY]
|
|
||||||
--tls-ca-crt value TLS/SSL CA certificate file for client certifications (default: "~/.gotty.ca.crt") [$GOTTY_TLS_CA_CRT]
|
|
||||||
--index value Custom index.html file [$GOTTY_INDEX]
|
|
||||||
--title-format value Title format of browser window (default: "{{ .command }}@{{ .hostname }}") [$GOTTY_TITLE_FORMAT]
|
|
||||||
--reconnect Enable reconnection [$GOTTY_RECONNECT]
|
|
||||||
--reconnect-time value Time to reconnect (default: 10) [$GOTTY_RECONNECT_TIME]
|
|
||||||
--max-connection value Maximum connection to gotty (default: 0) [$GOTTY_MAX_CONNECTION]
|
|
||||||
--once Accept only one client and exit on disconnection [$GOTTY_ONCE]
|
|
||||||
--timeout value Timeout seconds for waiting a client(0 to disable) (default: 0) [$GOTTY_TIMEOUT]
|
|
||||||
--permit-arguments Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB) [$GOTTY_PERMIT_ARGUMENTS]
|
|
||||||
--width value Static width of the screen, 0(default) means dynamically resize (default: 0) [$GOTTY_WIDTH]
|
|
||||||
--height value Static height of the screen, 0(default) means dynamically resize (default: 0) [$GOTTY_HEIGHT]
|
|
||||||
--ws-origin value A regular expression that matches origin URLs to be accepted by WebSocket. No cross origin requests are acceptable by default [$GOTTY_WS_ORIGIN]
|
|
||||||
--term value Terminal name to use on the browser, one of xterm or hterm. (default: "xterm") [$GOTTY_TERM]
|
|
||||||
--close-signal value Signal sent to the command process when gotty close it (default: SIGHUP) (default: 1) [$GOTTY_CLOSE_SIGNAL]
|
|
||||||
--close-timeout value Time in seconds to force kill process after client is disconnected (default: -1) (default: -1) [$GOTTY_CLOSE_TIMEOUT]
|
|
||||||
--config value Config file path (default: "~/.gotty") [$GOTTY_CONFIG]
|
|
||||||
--version, -v print the version
|
|
||||||
```
|
|
||||||
|
|
||||||
### Config File
|
|
||||||
|
|
||||||
You can customize default options and your terminal (hterm) by providing a config file to the `gotty` command. GoTTY loads a profile file at `~/.gotty` by default when it exists.
|
|
||||||
|
|
||||||
```
|
|
||||||
// Listen at port 9000 by default
|
|
||||||
port = "9000"
|
|
||||||
|
|
||||||
// Enable TSL/SSL by default
|
|
||||||
enable_tls = true
|
|
||||||
|
|
||||||
// hterm preferences
|
|
||||||
// Smaller font and a little bit bluer background color
|
|
||||||
preferences {
|
|
||||||
font_size = 5
|
|
||||||
background_color = "rgb(16, 16, 32)"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [`.gotty`](https://github.com/sorenisanerd/gotty/blob/master/.gotty) file in this repository for the list of configuration options.
|
|
||||||
|
|
||||||
### Security Options
|
|
||||||
|
|
||||||
By default, GoTTY doesn't allow clients to send any keystrokes or commands except terminal window resizing. When you want to permit clients to write input to the TTY, add the `-w` option. However, accepting input from remote clients is dangerous for most commands. When you need interaction with the TTY for some reasons, consider starting GoTTY with tmux or GNU Screen and run your command on it (see "Sharing with Multiple Clients" section for detail).
|
|
||||||
|
|
||||||
To restrict client access, you can use the `-c` option to enable the basic authentication. With this option, clients need to input the specified username and password to connect to the GoTTY server. Note that the credentials will be transmitted between the server and clients in plain text. For more strict authentication, consider the SSL/TLS client certificate authentication described below.
|
|
||||||
|
|
||||||
The `-r` option is a little bit casualer way to restrict access. With this option, GoTTY generates a random URL so that only people who know the URL can get access to the server.
|
|
||||||
|
|
||||||
All traffic between the server and clients are NOT encrypted by default. When you send secret information through GoTTY, we strongly recommend you use the `-t` option which enables TLS/SSL on the session. By default, GoTTY loads the crt and key files placed at `~/.gotty.crt` and `~/.gotty.key`. You can overwrite these file paths with the `--tls-crt` and `--tls-key` options. When you need to generate a self-signed certification file, you can use the `openssl` command.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
openssl req -x509 -nodes -days 9999 -newkey rsa:2048 -keyout ~/.gotty.key -out ~/.gotty.crt
|
|
||||||
```
|
|
||||||
|
|
||||||
(NOTE: For Safari uses, see [how to enable self-signed certificates for WebSockets](http://blog.marcon.me/post/24874118286/secure-websockets-safari) when use self-signed certificates)
|
|
||||||
|
|
||||||
For additional security, you can use the SSL/TLS client certificate authentication by providing a CA certificate file to the `--tls-ca-crt` option (this option requires the `-t` or `--tls` to be set). This option requires all clients to send valid client certificates that are signed by the specified certification authority.
|
|
||||||
|
|
||||||
## Sharing with Multiple Clients
|
|
||||||
|
|
||||||
GoTTY starts a new process with the given command when a new client connects to the server. This means users cannot share a single terminal with others by default. However, you can use terminal multiplexers for sharing a single process with multiple clients.
|
|
||||||
### Screen
|
|
||||||
After installing GNU screen, start a new session with `screen -S name-for-session` and connect to it with gotty in another terminal window/tab through `screen -x name-for-session`. All commands and activities being done in the first terminal tab/window will now be broadcasted by gotty.
|
|
||||||
### Tmux
|
|
||||||
For example, you can start a new tmux session named `gotty` with `top` command by the command below.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ gotty tmux new -A -s gotty top
|
|
||||||
```
|
|
||||||
|
|
||||||
This command doesn't allow clients to send keystrokes, however, you can attach the session from your local terminal and run operations like switching the mode of the `top` command. To connect to the tmux session from your terminal, you can use following command.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ tmux new -A -s gotty
|
|
||||||
```
|
|
||||||
|
|
||||||
By using terminal multiplexers, you can have the control of your terminal and allow clients to just see your screen.
|
|
||||||
|
|
||||||
### Quick Sharing on tmux
|
|
||||||
|
|
||||||
To share your current session with others by a shortcut key, you can add a line like below to your `.tmux.conf`.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Start GoTTY in a new window with C-t
|
|
||||||
bind-key C-t new-window "gotty tmux attach -t `tmux display -p '#S'`"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Playing with Docker
|
|
||||||
|
|
||||||
When you want to create a jailed environment for each client, you can use Docker containers like following:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ gotty -w docker run -it --rm busybox
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
You can build a binary using the following commands. Windows is not supported now. go1.9 is required.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Install tools
|
|
||||||
go get github.com/jteeuwen/go-bindata/...
|
|
||||||
go get github.com/tools/godep
|
|
||||||
|
|
||||||
# Build
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
To build the frontend part (JS files and other static files), you need `npm`.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
GoTTY uses [xterm.js](https://xtermjs.org/) and [hterm](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm) to run a JavaScript based terminal on web browsers. GoTTY itself provides a websocket server that simply relays output from the TTY to clients and receives input from clients and forwards it to the TTY. This hterm + websocket idea is inspired by [Wetty](https://github.com/krishnasrinivas/wetty).
|
|
||||||
|
|
||||||
## Alternatives
|
|
||||||
|
|
||||||
### Command line client
|
|
||||||
|
|
||||||
* [gotty-client](https://github.com/moul/gotty-client): If you want to connect to GoTTY server from your terminal
|
|
||||||
|
|
||||||
### Terminal/SSH on Web Browsers
|
|
||||||
|
|
||||||
* [Secure Shell (Chrome App)](https://chrome.google.com/webstore/detail/secure-shell/pnhechapfaindjhompbnflcldabbghjo): If you are a chrome user and need a "real" SSH client on your web browser, perhaps the Secure Shell app is what you want
|
|
||||||
* [Wetty](https://github.com/krishnasrinivas/wetty): Node based web terminal (SSH/login)
|
|
||||||
* [ttyd](https://tsl0922.github.io/ttyd): C port of GoTTY with CJK and IME support
|
|
||||||
|
|
||||||
### Terminal Sharing
|
|
||||||
|
|
||||||
* [tmate](http://tmate.io/): Forked-Tmux based Terminal-Terminal sharing
|
|
||||||
* [termshare](https://termsha.re): Terminal-Terminal sharing through a HTTP server
|
|
||||||
* [tmux](https://tmux.github.io/): Tmux itself also supports TTY sharing through SSH)
|
|
||||||
|
|
||||||
# License
|
|
||||||
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
# Contributors
|
|
||||||
|
|
||||||
## Original author
|
|
||||||
|
|
||||||
* Iwasaki Yudai
|
|
||||||
|
|
||||||
## Maintainer
|
|
||||||
|
|
||||||
* Søren L. Hansen
|
|
||||||
|
|
||||||
## Contributors
|
|
@ -1 +0,0 @@
|
|||||||
package backend
|
|
@ -1,3 +0,0 @@
|
|||||||
// Package localcommand provides an implementation of webtty.Slave
|
|
||||||
// that launches a local command with a PTY.
|
|
||||||
package localcommand
|
|
@ -1,48 +0,0 @@
|
|||||||
package localcommand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sorenisanerd/gotty/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
CloseSignal int `hcl:"close_signal" flagName:"close-signal" flagSName:"" flagDescribe:"Signal sent to the command process when gotty close it (default: SIGHUP)" default:"1"`
|
|
||||||
CloseTimeout int `hcl:"close_timeout" flagName:"close-timeout" flagSName:"" flagDescribe:"Time in seconds to force kill process after client is disconnected (default: -1)" default:"-1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Factory struct {
|
|
||||||
command string
|
|
||||||
argv []string
|
|
||||||
options *Options
|
|
||||||
opts []Option
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFactory(command string, argv []string, options *Options) (*Factory, error) {
|
|
||||||
opts := []Option{WithCloseSignal(syscall.Signal(options.CloseSignal))}
|
|
||||||
if options.CloseTimeout >= 0 {
|
|
||||||
opts = append(opts, WithCloseTimeout(time.Duration(options.CloseTimeout)*time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Factory{
|
|
||||||
command: command,
|
|
||||||
argv: argv,
|
|
||||||
options: options,
|
|
||||||
opts: opts,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *Factory) Name() string {
|
|
||||||
return "local command"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *Factory) New(params map[string][]string) (server.Slave, error) {
|
|
||||||
argv := make([]string, len(factory.argv))
|
|
||||||
copy(argv, factory.argv)
|
|
||||||
if params["arg"] != nil && len(params["arg"]) > 0 {
|
|
||||||
argv = append(argv, params["arg"]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return New(factory.command, argv, factory.opts...)
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
package localcommand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/creack/pty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultCloseSignal = syscall.SIGINT
|
|
||||||
DefaultCloseTimeout = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
type LocalCommand struct {
|
|
||||||
command string
|
|
||||||
argv []string
|
|
||||||
|
|
||||||
closeSignal syscall.Signal
|
|
||||||
closeTimeout time.Duration
|
|
||||||
|
|
||||||
cmd *exec.Cmd
|
|
||||||
pty *os.File
|
|
||||||
ptyClosed chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(command string, argv []string, options ...Option) (*LocalCommand, error) {
|
|
||||||
cmd := exec.Command(command, argv...)
|
|
||||||
|
|
||||||
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
|
|
||||||
|
|
||||||
pty, err := pty.Start(cmd)
|
|
||||||
if err != nil {
|
|
||||||
// todo close cmd?
|
|
||||||
return nil, errors.Wrapf(err, "failed to start command `%s`", command)
|
|
||||||
}
|
|
||||||
ptyClosed := make(chan struct{})
|
|
||||||
|
|
||||||
lcmd := &LocalCommand{
|
|
||||||
command: command,
|
|
||||||
argv: argv,
|
|
||||||
|
|
||||||
closeSignal: DefaultCloseSignal,
|
|
||||||
closeTimeout: DefaultCloseTimeout,
|
|
||||||
|
|
||||||
cmd: cmd,
|
|
||||||
pty: pty,
|
|
||||||
ptyClosed: ptyClosed,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, option := range options {
|
|
||||||
option(lcmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the process is closed by the user,
|
|
||||||
// close pty so that Read() on the pty breaks with an EOF.
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
lcmd.pty.Close()
|
|
||||||
close(lcmd.ptyClosed)
|
|
||||||
}()
|
|
||||||
|
|
||||||
lcmd.cmd.Wait()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return lcmd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcmd *LocalCommand) Read(p []byte) (n int, err error) {
|
|
||||||
return lcmd.pty.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcmd *LocalCommand) Write(p []byte) (n int, err error) {
|
|
||||||
return lcmd.pty.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcmd *LocalCommand) Close() error {
|
|
||||||
if lcmd.cmd != nil && lcmd.cmd.Process != nil {
|
|
||||||
lcmd.cmd.Process.Signal(lcmd.closeSignal)
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-lcmd.ptyClosed:
|
|
||||||
return nil
|
|
||||||
case <-lcmd.closeTimeoutC():
|
|
||||||
lcmd.cmd.Process.Signal(syscall.SIGKILL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcmd *LocalCommand) WindowTitleVariables() map[string]interface{} {
|
|
||||||
return map[string]interface{}{
|
|
||||||
"command": lcmd.command,
|
|
||||||
"argv": lcmd.argv,
|
|
||||||
"pid": lcmd.cmd.Process.Pid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error {
|
|
||||||
window := pty.Winsize{
|
|
||||||
Rows: uint16(height),
|
|
||||||
Cols: uint16(width),
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
}
|
|
||||||
err := pty.Setsize(lcmd.pty, &window)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcmd *LocalCommand) closeTimeoutC() <-chan time.Time {
|
|
||||||
if lcmd.closeTimeout >= 0 {
|
|
||||||
return time.After(lcmd.closeTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
return make(chan time.Time)
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package localcommand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Option func(*LocalCommand)
|
|
||||||
|
|
||||||
func WithCloseSignal(signal syscall.Signal) Option {
|
|
||||||
return func(lcmd *LocalCommand) {
|
|
||||||
lcmd.closeSignal = signal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithCloseTimeout(timeout time.Duration) Option {
|
|
||||||
return func(lcmd *LocalCommand) {
|
|
||||||
lcmd.closeTimeout = timeout
|
|
||||||
}
|
|
||||||
}
|
|
BIN
favicon.psd
BIN
favicon.psd
Binary file not shown.
12
go.mod
12
go.mod
@ -2,16 +2,4 @@ module github.com/sorenisanerd/gotty/v2
|
|||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
|
||||||
github.com/creack/pty v1.1.11
|
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.1
|
|
||||||
github.com/fatih/structs v1.1.0
|
|
||||||
github.com/gorilla/websocket v1.4.2
|
|
||||||
github.com/pkg/errors v0.9.1
|
|
||||||
github.com/sorenisanerd/gotty v1.3.0
|
|
||||||
github.com/urfave/cli/v2 v2.3.0
|
|
||||||
github.com/yudai/hcl v0.0.0-20151013225006-5fa2393b3552
|
|
||||||
)
|
|
||||||
|
|
||||||
retract [v2.0.0, v2.1.1]
|
retract [v2.0.0, v2.1.1]
|
||||||
|
51
go.sum
51
go.sum
@ -1,51 +0,0 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
|
||||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
|
||||||
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
|
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
|
||||||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
|
||||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
|
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
|
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
|
||||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
|
||||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
|
||||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
|
||||||
github.com/yudai/hcl v0.0.0-20151013225006-5fa2393b3552 h1:tjsK9T2IA3d2FFNxzDP7AJf+EXhyuPd7PB4Z2HrtAoc=
|
|
||||||
github.com/yudai/hcl v0.0.0-20151013225006-5fa2393b3552/go.mod h1:hg0ZaCmQL3rze1cH8Fh2g0a9q8vQs0uN8ESpePEwSEw=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750 h1:ZBu6861dZq7xBnG1bn5SRU0vA8nx42at4+kP07FMTog=
|
|
||||||
golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
|
|
||||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
1467
js/package-lock.json
generated
1467
js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "gotty",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"private": true,
|
|
||||||
"devDependencies": {
|
|
||||||
"license-loader": "^0.5.0",
|
|
||||||
"ts-loader": "^8.1.0",
|
|
||||||
"typescript": "^4.2.4",
|
|
||||||
"webpack": "^5.33.2",
|
|
||||||
"webpack-cli": "^4.6.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"css-loader": "^5.2.1",
|
|
||||||
"libapps": "github:yudai/libapps#release-hterm-1.70",
|
|
||||||
"style-loader": "^2.0.0",
|
|
||||||
"xterm": "^2.7.0"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
import * as bare from "libapps";
|
|
||||||
|
|
||||||
export class Hterm {
|
|
||||||
elem: HTMLElement;
|
|
||||||
|
|
||||||
term: bare.hterm.Terminal;
|
|
||||||
io: bare.hterm.IO;
|
|
||||||
|
|
||||||
columns: number;
|
|
||||||
rows: number;
|
|
||||||
|
|
||||||
// to "show" the current message when removeMessage() is called
|
|
||||||
message: string;
|
|
||||||
|
|
||||||
constructor(elem: HTMLElement) {
|
|
||||||
this.elem = elem;
|
|
||||||
bare.hterm.defaultStorage = new bare.lib.Storage.Memory();
|
|
||||||
this.term = new bare.hterm.Terminal();
|
|
||||||
this.term.getPrefs().set("send-encoding", "raw");
|
|
||||||
this.term.decorate(this.elem);
|
|
||||||
|
|
||||||
this.io = this.term.io.push();
|
|
||||||
this.term.installKeyboard();
|
|
||||||
};
|
|
||||||
|
|
||||||
info(): { columns: number, rows: number } {
|
|
||||||
return { columns: this.columns, rows: this.rows };
|
|
||||||
};
|
|
||||||
|
|
||||||
output(data: string) {
|
|
||||||
if (this.term.io != null) {
|
|
||||||
this.term.io.writeUTF8(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
showMessage(message: string, timeout: number) {
|
|
||||||
this.message = message;
|
|
||||||
if (timeout > 0) {
|
|
||||||
this.term.io.showOverlay(message, timeout);
|
|
||||||
} else {
|
|
||||||
this.term.io.showOverlay(message, null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
removeMessage(): void {
|
|
||||||
// there is no hideOverlay(), so show the same message with 0 sec
|
|
||||||
this.term.io.showOverlay(this.message, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
setWindowTitle(title: string) {
|
|
||||||
this.term.setWindowTitle(title);
|
|
||||||
};
|
|
||||||
|
|
||||||
setPreferences(value: object) {
|
|
||||||
Object.keys(value).forEach((key) => {
|
|
||||||
this.term.getPrefs().set(key, value[key]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onInput(callback: (input: string) => void) {
|
|
||||||
this.io.onVTKeystroke = (data) => {
|
|
||||||
callback(data);
|
|
||||||
};
|
|
||||||
this.io.sendString = (data) => {
|
|
||||||
callback(data);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
onResize(callback: (colmuns: number, rows: number) => void) {
|
|
||||||
this.io.onTerminalResize = (columns: number, rows: number) => {
|
|
||||||
this.columns = columns;
|
|
||||||
this.rows = rows;
|
|
||||||
callback(columns, rows);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
deactivate(): void {
|
|
||||||
this.io.onVTKeystroke = function(){};
|
|
||||||
this.io.sendString = function(){};
|
|
||||||
this.io.onTerminalResize = function(){};
|
|
||||||
this.term.uninstallKeyboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.removeMessage();
|
|
||||||
this.term.installKeyboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
close(): void {
|
|
||||||
this.term.uninstallKeyboard();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { Hterm } from "./hterm";
|
|
||||||
import { Xterm } from "./xterm";
|
|
||||||
import { Terminal, WebTTY, protocols } from "./webtty";
|
|
||||||
import { ConnectionFactory } from "./websocket";
|
|
||||||
|
|
||||||
// @TODO remove these
|
|
||||||
declare var gotty_auth_token: string;
|
|
||||||
declare var gotty_term: string;
|
|
||||||
|
|
||||||
const elem = document.getElementById("terminal")
|
|
||||||
|
|
||||||
if (elem !== null) {
|
|
||||||
var term: Terminal;
|
|
||||||
if (gotty_term == "hterm") {
|
|
||||||
term = new Hterm(elem);
|
|
||||||
} else {
|
|
||||||
term = new Xterm(elem);
|
|
||||||
}
|
|
||||||
const httpsEnabled = window.location.protocol == "https:";
|
|
||||||
const url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws';
|
|
||||||
const args = window.location.search;
|
|
||||||
const factory = new ConnectionFactory(url, protocols);
|
|
||||||
const wt = new WebTTY(term, factory, args, gotty_auth_token);
|
|
||||||
const closer = wt.open();
|
|
||||||
|
|
||||||
window.addEventListener("unload", () => {
|
|
||||||
closer();
|
|
||||||
term.close();
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,60 +0,0 @@
|
|||||||
export class ConnectionFactory {
|
|
||||||
url: string;
|
|
||||||
protocols: string[];
|
|
||||||
|
|
||||||
constructor(url: string, protocols: string[]) {
|
|
||||||
this.url = url;
|
|
||||||
this.protocols = protocols;
|
|
||||||
};
|
|
||||||
|
|
||||||
create(): Connection {
|
|
||||||
return new Connection(this.url, this.protocols);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Connection {
|
|
||||||
bare: WebSocket;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(url: string, protocols: string[]) {
|
|
||||||
this.bare = new WebSocket(url, protocols);
|
|
||||||
}
|
|
||||||
|
|
||||||
open() {
|
|
||||||
// nothing todo for websocket
|
|
||||||
};
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.bare.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
send(data: string) {
|
|
||||||
this.bare.send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
isOpen(): boolean {
|
|
||||||
if (this.bare.readyState == WebSocket.CONNECTING ||
|
|
||||||
this.bare.readyState == WebSocket.OPEN) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpen(callback: () => void) {
|
|
||||||
this.bare.onopen = (event) => {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onReceive(callback: (data: string) => void) {
|
|
||||||
this.bare.onmessage = (event) => {
|
|
||||||
callback(event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onClose(callback: () => void) {
|
|
||||||
this.bare.onclose = (event) => {
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
162
js/src/webtty.ts
162
js/src/webtty.ts
@ -1,162 +0,0 @@
|
|||||||
export const protocols = ["webtty"];
|
|
||||||
|
|
||||||
export const msgInputUnknown = '0';
|
|
||||||
export const msgInput = '1';
|
|
||||||
export const msgPing = '2';
|
|
||||||
export const msgResizeTerminal = '3';
|
|
||||||
|
|
||||||
export const msgUnknownOutput = '0';
|
|
||||||
export const msgOutput = '1';
|
|
||||||
export const msgPong = '2';
|
|
||||||
export const msgSetWindowTitle = '3';
|
|
||||||
export const msgSetPreferences = '4';
|
|
||||||
export const msgSetReconnect = '5';
|
|
||||||
export const msgSetBufferSize = '6';
|
|
||||||
|
|
||||||
|
|
||||||
export interface Terminal {
|
|
||||||
info(): { columns: number, rows: number };
|
|
||||||
output(data: string): void;
|
|
||||||
showMessage(message: string, timeout: number): void;
|
|
||||||
removeMessage(): void;
|
|
||||||
setWindowTitle(title: string): void;
|
|
||||||
setPreferences(value: object): void;
|
|
||||||
onInput(callback: (input: string) => void): void;
|
|
||||||
onResize(callback: (colmuns: number, rows: number) => void): void;
|
|
||||||
reset(): void;
|
|
||||||
deactivate(): void;
|
|
||||||
close(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Connection {
|
|
||||||
open(): void;
|
|
||||||
close(): void;
|
|
||||||
send(data: string): void;
|
|
||||||
isOpen(): boolean;
|
|
||||||
onOpen(callback: () => void): void;
|
|
||||||
onReceive(callback: (data: string) => void): void;
|
|
||||||
onClose(callback: () => void): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConnectionFactory {
|
|
||||||
create(): Connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class WebTTY {
|
|
||||||
term: Terminal;
|
|
||||||
connectionFactory: ConnectionFactory;
|
|
||||||
args: string;
|
|
||||||
authToken: string;
|
|
||||||
reconnect: number;
|
|
||||||
bufSize: number;
|
|
||||||
|
|
||||||
constructor(term: Terminal, connectionFactory: ConnectionFactory, args: string, authToken: string) {
|
|
||||||
this.term = term;
|
|
||||||
this.connectionFactory = connectionFactory;
|
|
||||||
this.args = args;
|
|
||||||
this.authToken = authToken;
|
|
||||||
this.reconnect = -1;
|
|
||||||
this.bufSize = 1024;
|
|
||||||
};
|
|
||||||
|
|
||||||
open() {
|
|
||||||
let connection = this.connectionFactory.create();
|
|
||||||
let pingTimer: NodeJS.Timeout;
|
|
||||||
let reconnectTimeout: NodeJS.Timeout;
|
|
||||||
|
|
||||||
const setup = () => {
|
|
||||||
connection.onOpen(() => {
|
|
||||||
const termInfo = this.term.info();
|
|
||||||
|
|
||||||
connection.send(JSON.stringify(
|
|
||||||
{
|
|
||||||
Arguments: this.args,
|
|
||||||
AuthToken: this.authToken,
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
|
|
||||||
const resizeHandler = (colmuns: number, rows: number) => {
|
|
||||||
connection.send(
|
|
||||||
msgResizeTerminal + JSON.stringify(
|
|
||||||
{
|
|
||||||
columns: colmuns,
|
|
||||||
rows: rows
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.term.onResize(resizeHandler);
|
|
||||||
resizeHandler(termInfo.columns, termInfo.rows);
|
|
||||||
|
|
||||||
this.term.onInput(
|
|
||||||
(input: string) => {
|
|
||||||
// Leave room for message type id
|
|
||||||
let effectiveBufferSize = this.bufSize - 1;
|
|
||||||
|
|
||||||
// Split input into buffer sized chunks
|
|
||||||
for (let i = 0; i < Math.ceil(input.length/effectiveBufferSize); i++) {
|
|
||||||
let inputChunk = input.substring(i*effectiveBufferSize, Math.min((i+1)*effectiveBufferSize, input.length))
|
|
||||||
connection.send(msgInput + inputChunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
pingTimer = setInterval(() => {
|
|
||||||
connection.send(msgPing)
|
|
||||||
}, 30 * 1000);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.onReceive((data) => {
|
|
||||||
const payload = data.slice(1);
|
|
||||||
switch (data[0]) {
|
|
||||||
case msgOutput:
|
|
||||||
this.term.output(atob(payload));
|
|
||||||
break;
|
|
||||||
case msgPong:
|
|
||||||
break;
|
|
||||||
case msgSetWindowTitle:
|
|
||||||
this.term.setWindowTitle(payload);
|
|
||||||
break;
|
|
||||||
case msgSetPreferences:
|
|
||||||
const preferences = JSON.parse(payload);
|
|
||||||
this.term.setPreferences(preferences);
|
|
||||||
break;
|
|
||||||
case msgSetReconnect:
|
|
||||||
const autoReconnect = JSON.parse(payload);
|
|
||||||
console.log("Enabling reconnect: " + autoReconnect + " seconds")
|
|
||||||
this.reconnect = autoReconnect;
|
|
||||||
break;
|
|
||||||
case msgSetBufferSize:
|
|
||||||
const bufSize = JSON.parse(payload);
|
|
||||||
this.bufSize = bufSize;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.onClose(() => {
|
|
||||||
clearInterval(pingTimer);
|
|
||||||
this.term.deactivate();
|
|
||||||
this.term.showMessage("Connection Closed", 0);
|
|
||||||
if (this.reconnect > 0) {
|
|
||||||
reconnectTimeout = setTimeout(() => {
|
|
||||||
connection = this.connectionFactory.create();
|
|
||||||
this.term.reset();
|
|
||||||
setup();
|
|
||||||
}, this.reconnect * 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
setup();
|
|
||||||
return () => {
|
|
||||||
clearTimeout(reconnectTimeout);
|
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
105
js/src/xterm.ts
105
js/src/xterm.ts
@ -1,105 +0,0 @@
|
|||||||
import * as bare from "xterm";
|
|
||||||
import { lib } from "libapps"
|
|
||||||
|
|
||||||
|
|
||||||
bare.loadAddon("fit");
|
|
||||||
|
|
||||||
export class Xterm {
|
|
||||||
elem: HTMLElement;
|
|
||||||
term: bare;
|
|
||||||
resizeListener: () => void;
|
|
||||||
decoder: lib.UTF8Decoder;
|
|
||||||
|
|
||||||
message: HTMLElement;
|
|
||||||
messageTimeout: number;
|
|
||||||
messageTimer: NodeJS.Timeout;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(elem: HTMLElement) {
|
|
||||||
this.elem = elem;
|
|
||||||
this.term = new bare();
|
|
||||||
|
|
||||||
this.message = elem.ownerDocument.createElement("div");
|
|
||||||
this.message.className = "xterm-overlay";
|
|
||||||
this.messageTimeout = 2000;
|
|
||||||
|
|
||||||
this.resizeListener = () => {
|
|
||||||
this.term.fit();
|
|
||||||
this.term.scrollToBottom();
|
|
||||||
this.showMessage(String(this.term.cols) + "x" + String(this.term.rows), this.messageTimeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.term.on("open", () => {
|
|
||||||
this.resizeListener();
|
|
||||||
window.addEventListener("resize", () => { this.resizeListener(); });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.term.open(elem, true);
|
|
||||||
|
|
||||||
this.decoder = new lib.UTF8Decoder()
|
|
||||||
};
|
|
||||||
|
|
||||||
info(): { columns: number, rows: number } {
|
|
||||||
return { columns: this.term.cols, rows: this.term.rows };
|
|
||||||
};
|
|
||||||
|
|
||||||
output(data: string) {
|
|
||||||
this.term.write(this.decoder.decode(data));
|
|
||||||
};
|
|
||||||
|
|
||||||
showMessage(message: string, timeout: number) {
|
|
||||||
this.message.textContent = message;
|
|
||||||
this.elem.appendChild(this.message);
|
|
||||||
|
|
||||||
if (this.messageTimer) {
|
|
||||||
clearTimeout(this.messageTimer);
|
|
||||||
}
|
|
||||||
if (timeout > 0) {
|
|
||||||
this.messageTimer = setTimeout(() => {
|
|
||||||
this.elem.removeChild(this.message);
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
removeMessage(): void {
|
|
||||||
if (this.message.parentNode == this.elem) {
|
|
||||||
this.elem.removeChild(this.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setWindowTitle(title: string) {
|
|
||||||
document.title = title;
|
|
||||||
};
|
|
||||||
|
|
||||||
setPreferences(value: object) {
|
|
||||||
};
|
|
||||||
|
|
||||||
onInput(callback: (input: string) => void) {
|
|
||||||
this.term.on("data", (data) => {
|
|
||||||
callback(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
onResize(callback: (colmuns: number, rows: number) => void) {
|
|
||||||
this.term.on("resize", (data) => {
|
|
||||||
callback(data.cols, data.rows);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
deactivate(): void {
|
|
||||||
this.term.off("data");
|
|
||||||
this.term.off("resize");
|
|
||||||
this.term.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.removeMessage();
|
|
||||||
this.term.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
close(): void {
|
|
||||||
window.removeEventListener("resize", this.resizeListener);
|
|
||||||
this.term.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist/",
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"noUnusedLocals" : true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"outDir": "./dist/",
|
|
||||||
"declaration": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"target": "es5",
|
|
||||||
"module": "commonJS",
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"*": ["./typings/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
51
js/typings/libapps/index.d.ts
vendored
51
js/typings/libapps/index.d.ts
vendored
@ -1,51 +0,0 @@
|
|||||||
export declare namespace hterm {
|
|
||||||
export class Terminal {
|
|
||||||
io: IO;
|
|
||||||
onTerminalReady: () => void;
|
|
||||||
|
|
||||||
constructor();
|
|
||||||
getPrefs(): Prefs;
|
|
||||||
decorate(HTMLElement);
|
|
||||||
installKeyboard(): void;
|
|
||||||
uninstallKeyboard(): void;
|
|
||||||
setWindowTitle(title: string): void;
|
|
||||||
reset(): void;
|
|
||||||
softReset(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IO {
|
|
||||||
writeUTF8: ((data: string) => void);
|
|
||||||
writeUTF16: ((data: string) => void);
|
|
||||||
onVTKeystroke: ((data: string) => void) | null;
|
|
||||||
sendString: ((data: string) => void) | null;
|
|
||||||
onTerminalResize: ((columns: number, rows: number) => void) | null;
|
|
||||||
|
|
||||||
push(): IO;
|
|
||||||
writeUTF(data: string);
|
|
||||||
showOverlay(message: string, timeout: number | null);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Prefs {
|
|
||||||
set(key: string, value: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export var defaultStorage: lib.Storage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace lib {
|
|
||||||
export interface Storage {
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Memory {
|
|
||||||
new (): Storage;
|
|
||||||
Memory(): Storage
|
|
||||||
}
|
|
||||||
|
|
||||||
export var Storage: {
|
|
||||||
Memory: Memory
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UTF8Decoder {
|
|
||||||
decode(str: string)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: "./src/main.ts",
|
|
||||||
entry: {
|
|
||||||
"gotty": "./src/main.ts",
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, '../bindata/static/js/'),
|
|
||||||
},
|
|
||||||
devtool: "source-map",
|
|
||||||
resolve: {
|
|
||||||
extensions: [".ts", ".tsx", ".js"],
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
loader: "ts-loader",
|
|
||||||
exclude: /node_modules/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: ["style-loader", "css-loader"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
include: /node_modules/,
|
|
||||||
loader: 'license-loader'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
148
main.go
148
main.go
@ -1,152 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
cli "github.com/urfave/cli/v2"
|
|
||||||
|
|
||||||
"github.com/sorenisanerd/gotty/backend/localcommand"
|
|
||||||
"github.com/sorenisanerd/gotty/pkg/homedir"
|
|
||||||
"github.com/sorenisanerd/gotty/server"
|
|
||||||
"github.com/sorenisanerd/gotty/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
fmt.Println("Deprecated. Use v1")
|
||||||
app.Name = "gotty"
|
|
||||||
app.Version = Version + "+" + CommitID
|
|
||||||
app.Usage = "Share your terminal as a web application"
|
|
||||||
app.HideHelpCommand = true
|
|
||||||
appOptions := &server.Options{}
|
|
||||||
|
|
||||||
if err := utils.ApplyDefaultValues(appOptions); err != nil {
|
|
||||||
exit(err, 1)
|
|
||||||
}
|
|
||||||
backendOptions := &localcommand.Options{}
|
|
||||||
if err := utils.ApplyDefaultValues(backendOptions); err != nil {
|
|
||||||
exit(err, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
cliFlags, flagMappings, err := utils.GenerateFlags(appOptions, backendOptions)
|
|
||||||
if err != nil {
|
|
||||||
exit(err, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Flags = append(
|
|
||||||
cliFlags,
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "config",
|
|
||||||
Value: "~/.gotty",
|
|
||||||
Usage: "Config file path",
|
|
||||||
EnvVars: []string{"GOTTY_CONFIG"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
app.Action = func(c *cli.Context) error {
|
|
||||||
if c.NArg() == 0 {
|
|
||||||
msg := "Error: No command given."
|
|
||||||
cli.ShowAppHelp(c)
|
|
||||||
exit(fmt.Errorf(msg), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
configFile := c.String("config")
|
|
||||||
_, err := os.Stat(homedir.Expand(configFile))
|
|
||||||
if configFile != "~/.gotty" || !os.IsNotExist(err) {
|
|
||||||
if err := utils.ApplyConfigFile(configFile, appOptions, backendOptions); err != nil {
|
|
||||||
exit(err, 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ApplyFlags(cliFlags, flagMappings, c, appOptions, backendOptions)
|
|
||||||
|
|
||||||
appOptions.EnableBasicAuth = c.IsSet("credential")
|
|
||||||
appOptions.EnableTLSClientAuth = c.IsSet("tls-ca-crt")
|
|
||||||
|
|
||||||
err = appOptions.Validate()
|
|
||||||
if err != nil {
|
|
||||||
exit(err, 6)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := c.Args()
|
|
||||||
factory, err := localcommand.NewFactory(args.First(), args.Tail(), backendOptions)
|
|
||||||
if err != nil {
|
|
||||||
exit(err, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
appOptions.TitleVariables = map[string]interface{}{
|
|
||||||
"command": args.First(),
|
|
||||||
"argv": args.Tail(),
|
|
||||||
"hostname": hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
srv, err := server.New(factory, appOptions)
|
|
||||||
if err != nil {
|
|
||||||
exit(err, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
gCtx, gCancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
log.Printf("GoTTY is starting with command: %s", strings.Join(args.Slice(), " "))
|
|
||||||
|
|
||||||
errs := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
errs <- srv.Run(ctx, server.WithGracefullContext(gCtx))
|
|
||||||
}()
|
|
||||||
err = waitSignals(errs, cancel, gCancel)
|
|
||||||
|
|
||||||
if err != nil && err != context.Canceled {
|
|
||||||
fmt.Printf("Error: %s\n", err)
|
|
||||||
exit(err, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
app.Run(os.Args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func exit(err error, code int) {
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
os.Exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitSignals(errs chan error, cancel context.CancelFunc, gracefullCancel context.CancelFunc) error {
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(
|
|
||||||
sigChan,
|
|
||||||
syscall.SIGINT,
|
|
||||||
syscall.SIGTERM,
|
|
||||||
)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-errs:
|
|
||||||
return err
|
|
||||||
|
|
||||||
case s := <-sigChan:
|
|
||||||
switch s {
|
|
||||||
case syscall.SIGINT:
|
|
||||||
gracefullCancel()
|
|
||||||
fmt.Println("C-C to force close")
|
|
||||||
select {
|
|
||||||
case err := <-errs:
|
|
||||||
return err
|
|
||||||
case <-sigChan:
|
|
||||||
fmt.Println("Force closing...")
|
|
||||||
cancel()
|
|
||||||
return <-errs
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
cancel()
|
|
||||||
return <-errs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
package homedir
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Expand(path string) string {
|
|
||||||
if path[0:2] == "~/" {
|
|
||||||
return os.Getenv("HOME") + path[1:]
|
|
||||||
} else {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package randomstring
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"math/big"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Generate(length int) string {
|
|
||||||
const base = 36
|
|
||||||
size := big.NewInt(base)
|
|
||||||
n := make([]byte, length)
|
|
||||||
for i, _ := range n {
|
|
||||||
c, _ := rand.Int(rand.Reader, size)
|
|
||||||
n[i] = strconv.FormatInt(c.Int64(), base)[0]
|
|
||||||
}
|
|
||||||
return string(n)
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 863 B |
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
@ -1,7 +0,0 @@
|
|||||||
html, body, #terminal {
|
|
||||||
background: black;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0%;
|
|
||||||
margin: 0%;
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>{{ .title }}</title>
|
|
||||||
<link rel="manifest" href="manifest.json">
|
|
||||||
<link rel="icon" type="image/png" href="favicon.png">
|
|
||||||
<link rel="stylesheet" href="./css/index.css" />
|
|
||||||
<link rel="stylesheet" href="./css/xterm.css" />
|
|
||||||
<link rel="stylesheet" href="./css/xterm_customize.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="terminal"></div>
|
|
||||||
<script src="./auth_token.js"></script>
|
|
||||||
<script src="./config.js"></script>
|
|
||||||
<script src="./js/gotty.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "{{ .title }}",
|
|
||||||
"name": "{{ .title }}",
|
|
||||||
"start_url": "./",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "./icon_192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"display": "minimal-ui",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#000000"
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
.terminal {
|
|
||||||
font-family: "DejaVu Sans Mono", "Everson Mono", FreeMono, Menlo, Terminal, monospace, "Apple Symbols";
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-overlay {
|
|
||||||
font-family: "DejaVu Sans Mono", "Everson Mono", FreeMono, Menlo, Terminal, monospace, "Apple Symbols";
|
|
||||||
border-radius: 15px;
|
|
||||||
font-size: xx-large;
|
|
||||||
color: black;
|
|
||||||
background: white;
|
|
||||||
opacity: 0.75;
|
|
||||||
padding: 0.2em 0.5em 0.2em 0.5em;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
user-select: none;
|
|
||||||
transition: opacity 180ms ease-in;
|
|
||||||
}
|
|
BIN
screenshot.gif
BIN
screenshot.gif
Binary file not shown.
Before Width: | Height: | Size: 2.1 MiB |
@ -1,70 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type counter struct {
|
|
||||||
duration time.Duration
|
|
||||||
zeroTimer *time.Timer
|
|
||||||
wg sync.WaitGroup
|
|
||||||
connections int
|
|
||||||
mutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCounter(duration time.Duration) *counter {
|
|
||||||
zeroTimer := time.NewTimer(duration)
|
|
||||||
|
|
||||||
// when duration is 0, drain the expire event here
|
|
||||||
// so that user will never get the event.
|
|
||||||
if duration == 0 {
|
|
||||||
<-zeroTimer.C
|
|
||||||
}
|
|
||||||
|
|
||||||
return &counter{
|
|
||||||
duration: duration,
|
|
||||||
zeroTimer: zeroTimer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (counter *counter) add(n int) int {
|
|
||||||
counter.mutex.Lock()
|
|
||||||
defer counter.mutex.Unlock()
|
|
||||||
|
|
||||||
if counter.duration > 0 {
|
|
||||||
counter.zeroTimer.Stop()
|
|
||||||
}
|
|
||||||
counter.wg.Add(n)
|
|
||||||
counter.connections += n
|
|
||||||
|
|
||||||
return counter.connections
|
|
||||||
}
|
|
||||||
|
|
||||||
func (counter *counter) done() int {
|
|
||||||
counter.mutex.Lock()
|
|
||||||
defer counter.mutex.Unlock()
|
|
||||||
|
|
||||||
counter.connections--
|
|
||||||
counter.wg.Done()
|
|
||||||
if counter.connections == 0 && counter.duration > 0 {
|
|
||||||
counter.zeroTimer.Reset(counter.duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
return counter.connections
|
|
||||||
}
|
|
||||||
|
|
||||||
func (counter *counter) count() int {
|
|
||||||
counter.mutex.Lock()
|
|
||||||
defer counter.mutex.Unlock()
|
|
||||||
|
|
||||||
return counter.connections
|
|
||||||
}
|
|
||||||
|
|
||||||
func (counter *counter) wait() {
|
|
||||||
counter.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (counter *counter) timer() *time.Timer {
|
|
||||||
return counter.zeroTimer
|
|
||||||
}
|
|
@ -1,260 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/sorenisanerd/gotty/webtty"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (server *Server) generateHandleWS(ctx context.Context, cancel context.CancelFunc, counter *counter) http.HandlerFunc {
|
|
||||||
once := new(int64)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-counter.timer().C:
|
|
||||||
cancel()
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if server.options.Once {
|
|
||||||
success := atomic.CompareAndSwapInt64(once, 0, 1)
|
|
||||||
if !success {
|
|
||||||
http.Error(w, "Server is shutting down", http.StatusServiceUnavailable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
num := counter.add(1)
|
|
||||||
closeReason := "unknown reason"
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
num := counter.done()
|
|
||||||
log.Printf(
|
|
||||||
"Connection closed by %s: %s, connections: %d/%d",
|
|
||||||
closeReason, r.RemoteAddr, num, server.options.MaxConnection,
|
|
||||||
)
|
|
||||||
|
|
||||||
if server.options.Once {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if int64(server.options.MaxConnection) != 0 {
|
|
||||||
if num > server.options.MaxConnection {
|
|
||||||
closeReason = "exceeding max number of connections"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("New client connected: %s, connections: %d/%d", r.RemoteAddr, num, server.options.MaxConnection)
|
|
||||||
|
|
||||||
if r.Method != "GET" {
|
|
||||||
http.Error(w, "Method not allowed", 405)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := server.upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
closeReason = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
err = server.processWSConn(ctx, conn)
|
|
||||||
|
|
||||||
switch err {
|
|
||||||
case ctx.Err():
|
|
||||||
closeReason = "cancelation"
|
|
||||||
case webtty.ErrSlaveClosed:
|
|
||||||
closeReason = server.factory.Name()
|
|
||||||
case webtty.ErrMasterClosed:
|
|
||||||
closeReason = "client"
|
|
||||||
default:
|
|
||||||
closeReason = fmt.Sprintf("an error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) processWSConn(ctx context.Context, conn *websocket.Conn) error {
|
|
||||||
typ, initLine, err := conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to authenticate websocket connection")
|
|
||||||
}
|
|
||||||
if typ != websocket.TextMessage {
|
|
||||||
return errors.New("failed to authenticate websocket connection: invalid message type")
|
|
||||||
}
|
|
||||||
|
|
||||||
var init InitMessage
|
|
||||||
err = json.Unmarshal(initLine, &init)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to authenticate websocket connection")
|
|
||||||
}
|
|
||||||
if init.AuthToken != server.options.Credential {
|
|
||||||
return errors.New("failed to authenticate websocket connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
queryPath := "?"
|
|
||||||
if server.options.PermitArguments && init.Arguments != "" {
|
|
||||||
queryPath = init.Arguments
|
|
||||||
}
|
|
||||||
|
|
||||||
query, err := url.Parse(queryPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to parse arguments")
|
|
||||||
}
|
|
||||||
params := query.Query()
|
|
||||||
var slave Slave
|
|
||||||
slave, err = server.factory.New(params)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to create backend")
|
|
||||||
}
|
|
||||||
defer slave.Close()
|
|
||||||
|
|
||||||
titleVars := server.titleVariables(
|
|
||||||
[]string{"server", "master", "slave"},
|
|
||||||
map[string]map[string]interface{}{
|
|
||||||
"server": server.options.TitleVariables,
|
|
||||||
"master": map[string]interface{}{
|
|
||||||
"remote_addr": conn.RemoteAddr(),
|
|
||||||
},
|
|
||||||
"slave": slave.WindowTitleVariables(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
titleBuf := new(bytes.Buffer)
|
|
||||||
err = server.titleTemplate.Execute(titleBuf, titleVars)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to fill window title template")
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := []webtty.Option{
|
|
||||||
webtty.WithWindowTitle(titleBuf.Bytes()),
|
|
||||||
}
|
|
||||||
if server.options.PermitWrite {
|
|
||||||
opts = append(opts, webtty.WithPermitWrite())
|
|
||||||
}
|
|
||||||
if server.options.EnableReconnect {
|
|
||||||
opts = append(opts, webtty.WithReconnect(server.options.ReconnectTime))
|
|
||||||
}
|
|
||||||
if server.options.Width > 0 {
|
|
||||||
opts = append(opts, webtty.WithFixedColumns(server.options.Width))
|
|
||||||
}
|
|
||||||
if server.options.Height > 0 {
|
|
||||||
opts = append(opts, webtty.WithFixedRows(server.options.Height))
|
|
||||||
}
|
|
||||||
if server.options.Preferences != nil {
|
|
||||||
opts = append(opts, webtty.WithMasterPreferences(server.options.Preferences))
|
|
||||||
}
|
|
||||||
|
|
||||||
tty, err := webtty.New(&wsWrapper{conn}, slave, opts...)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to create webtty")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tty.Run(ctx)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
||||||
indexVars, err := server.indexVariables(r)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal Server Error", 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
indexBuf := new(bytes.Buffer)
|
|
||||||
err = server.indexTemplate.Execute(indexBuf, indexVars)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal Server Error", 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write(indexBuf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) handleManifest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
indexVars, err := server.indexVariables(r)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal Server Error", 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
indexBuf := new(bytes.Buffer)
|
|
||||||
err = server.manifestTemplate.Execute(indexBuf, indexVars)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal Server Error", 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write(indexBuf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) indexVariables(r *http.Request) (map[string]interface{}, error) {
|
|
||||||
titleVars := server.titleVariables(
|
|
||||||
[]string{"server", "master"},
|
|
||||||
map[string]map[string]interface{}{
|
|
||||||
"server": server.options.TitleVariables,
|
|
||||||
"master": map[string]interface{}{
|
|
||||||
"remote_addr": r.RemoteAddr,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
titleBuf := new(bytes.Buffer)
|
|
||||||
err := server.titleTemplate.Execute(titleBuf, titleVars)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
indexVars := map[string]interface{}{
|
|
||||||
"title": titleBuf.String(),
|
|
||||||
}
|
|
||||||
return indexVars, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) handleAuthToken(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
|
||||||
// @TODO hashing?
|
|
||||||
w.Write([]byte("var gotty_auth_token = '" + server.options.Credential + "';"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
|
||||||
w.Write([]byte("var gotty_term = '" + server.options.Term + "';"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// titleVariables merges maps in a specified order.
|
|
||||||
// varUnits are name-keyed maps, whose names will be iterated using order.
|
|
||||||
func (server *Server) titleVariables(order []string, varUnits map[string]map[string]interface{}) map[string]interface{} {
|
|
||||||
titleVars := map[string]interface{}{}
|
|
||||||
|
|
||||||
for _, name := range order {
|
|
||||||
vars, ok := varUnits[name]
|
|
||||||
if !ok {
|
|
||||||
panic("title variable name error")
|
|
||||||
}
|
|
||||||
for key, val := range vars {
|
|
||||||
titleVars[key] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// safe net for conflicted keys
|
|
||||||
for _, name := range order {
|
|
||||||
titleVars[name] = varUnits[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
return titleVars
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
type InitMessage struct {
|
|
||||||
Arguments string `json:"Arguments,omitempty"`
|
|
||||||
AuthToken string `json:"AuthToken,omitempty"`
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func listAddresses() (addresses []string) {
|
|
||||||
ifaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses = make([]string, 0, len(ifaces))
|
|
||||||
|
|
||||||
for _, iface := range ifaces {
|
|
||||||
ifAddrs, _ := iface.Addrs()
|
|
||||||
for _, ifAddr := range ifAddrs {
|
|
||||||
switch v := ifAddr.(type) {
|
|
||||||
case *net.IPNet:
|
|
||||||
addresses = append(addresses, v.IP.String())
|
|
||||||
case *net.IPAddr:
|
|
||||||
addresses = append(addresses, v.IP.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return addresses
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type logResponseWriter struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
status int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *logResponseWriter) WriteHeader(status int) {
|
|
||||||
w.status = status
|
|
||||||
w.ResponseWriter.WriteHeader(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *logResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
||||||
hj, _ := w.ResponseWriter.(http.Hijacker)
|
|
||||||
w.status = http.StatusSwitchingProtocols
|
|
||||||
return hj.Hijack()
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (server *Server) wrapLogger(handler http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
rw := &logResponseWriter{w, 200}
|
|
||||||
handler.ServeHTTP(rw, r)
|
|
||||||
log.Printf("%s %d %s %s", r.RemoteAddr, rw.status, r.Method, r.URL.Path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) wrapHeaders(handler http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// todo add version
|
|
||||||
w.Header().Set("Server", "GoTTY")
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) wrapBasicAuth(handler http.Handler, credential string) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
token := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
|
|
||||||
|
|
||||||
if len(token) != 2 || strings.ToLower(token[0]) != "basic" {
|
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="GoTTY"`)
|
|
||||||
http.Error(w, "Bad Request", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := base64.StdEncoding.DecodeString(token[1])
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if credential != string(payload) {
|
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="GoTTY"`)
|
|
||||||
http.Error(w, "authorization failed", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Basic Authentication Succeeded: %s", r.RemoteAddr)
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
Address string `hcl:"address" flagName:"address" flagSName:"a" flagDescribe:"IP address to listen" default:"0.0.0.0"`
|
|
||||||
Port string `hcl:"port" flagName:"port" flagSName:"p" flagDescribe:"Port number to liten" default:"8080"`
|
|
||||||
PermitWrite bool `hcl:"permit_write" flagName:"permit-write" flagSName:"w" flagDescribe:"Permit clients to write to the TTY (BE CAREFUL)" default:"false"`
|
|
||||||
EnableBasicAuth bool `hcl:"enable_basic_auth" default:"false"`
|
|
||||||
Credential string `hcl:"credential" flagName:"credential" flagSName:"c" flagDescribe:"Credential for Basic Authentication (ex: user:pass, default disabled)" default:""`
|
|
||||||
EnableRandomUrl bool `hcl:"enable_random_url" flagName:"random-url" flagSName:"r" flagDescribe:"Add a random string to the URL" default:"false"`
|
|
||||||
RandomUrlLength int `hcl:"random_url_length" flagName:"random-url-length" flagDescribe:"Random URL length" default:"8"`
|
|
||||||
EnableTLS bool `hcl:"enable_tls" flagName:"tls" flagSName:"t" flagDescribe:"Enable TLS/SSL" default:"false"`
|
|
||||||
TLSCrtFile string `hcl:"tls_crt_file" flagName:"tls-crt" flagDescribe:"TLS/SSL certificate file path" default:"~/.gotty.crt"`
|
|
||||||
TLSKeyFile string `hcl:"tls_key_file" flagName:"tls-key" flagDescribe:"TLS/SSL key file path" default:"~/.gotty.key"`
|
|
||||||
EnableTLSClientAuth bool `hcl:"enable_tls_client_auth" default:"false"`
|
|
||||||
TLSCACrtFile string `hcl:"tls_ca_crt_file" flagName:"tls-ca-crt" flagDescribe:"TLS/SSL CA certificate file for client certifications" default:"~/.gotty.ca.crt"`
|
|
||||||
IndexFile string `hcl:"index_file" flagName:"index" flagDescribe:"Custom index.html file" default:""`
|
|
||||||
TitleFormat string `hcl:"title_format" flagName:"title-format" flagSName:"" flagDescribe:"Title format of browser window" default:"{{ .command }}@{{ .hostname }}"`
|
|
||||||
EnableReconnect bool `hcl:"enable_reconnect" flagName:"reconnect" flagDescribe:"Enable reconnection" default:"false"`
|
|
||||||
ReconnectTime int `hcl:"reconnect_time" flagName:"reconnect-time" flagDescribe:"Time to reconnect" default:"10"`
|
|
||||||
MaxConnection int `hcl:"max_connection" flagName:"max-connection" flagDescribe:"Maximum connection to gotty" default:"0"`
|
|
||||||
Once bool `hcl:"once" flagName:"once" flagDescribe:"Accept only one client and exit on disconnection" default:"false"`
|
|
||||||
Timeout int `hcl:"timeout" flagName:"timeout" flagDescribe:"Timeout seconds for waiting a client(0 to disable)" default:"0"`
|
|
||||||
PermitArguments bool `hcl:"permit_arguments" flagName:"permit-arguments" flagDescribe:"Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB)" default:"true"`
|
|
||||||
Preferences *HtermPrefernces `hcl:"preferences"`
|
|
||||||
Width int `hcl:"width" flagName:"width" flagDescribe:"Static width of the screen, 0(default) means dynamically resize" default:"0"`
|
|
||||||
Height int `hcl:"height" flagName:"height" flagDescribe:"Static height of the screen, 0(default) means dynamically resize" default:"0"`
|
|
||||||
WSOrigin string `hcl:"ws_origin" flagName:"ws-origin" flagDescribe:"A regular expression that matches origin URLs to be accepted by WebSocket. No cross origin requests are acceptable by default" default:""`
|
|
||||||
Term string `hcl:"term" flagName:"term" flagDescribe:"Terminal name to use on the browser, one of xterm or hterm." default:"hterm"`
|
|
||||||
|
|
||||||
TitleVariables map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (options *Options) Validate() error {
|
|
||||||
if options.EnableTLSClientAuth && !options.EnableTLS {
|
|
||||||
return errors.New("TLS client authentication is enabled, but TLS is not enabled")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type HtermPrefernces struct {
|
|
||||||
AltGrMode *string `hcl:"alt_gr_mode" json:"alt-gr-mode,omitempty"`
|
|
||||||
AltBackspaceIsMetaBackspace bool `hcl:"alt_backspace_is_meta_backspace" json:"alt-backspace-is-meta-backspace,omitempty"`
|
|
||||||
AltIsMeta bool `hcl:"alt_is_meta" json:"alt-is-meta,omitempty"`
|
|
||||||
AltSendsWhat string `hcl:"alt_sends_what" json:"alt-sends-what,omitempty"`
|
|
||||||
AudibleBellSound string `hcl:"audible_bell_sound" json:"audible-bell-sound,omitempty"`
|
|
||||||
DesktopNotificationBell bool `hcl:"desktop_notification_bell" json:"desktop-notification-bell,omitempty"`
|
|
||||||
BackgroundColor string `hcl:"background_color" json:"background-color,omitempty"`
|
|
||||||
BackgroundImage string `hcl:"background_image" json:"background-image,omitempty"`
|
|
||||||
BackgroundSize string `hcl:"background_size" json:"background-size,omitempty"`
|
|
||||||
BackgroundPosition string `hcl:"background_position" json:"background-position,omitempty"`
|
|
||||||
BackspaceSendsBackspace bool `hcl:"backspace_sends_backspace" json:"backspace-sends-backspace,omitempty"`
|
|
||||||
CharacterMapOverrides map[string]map[string]string `hcl:"character_map_overrides" json:"character-map-overrides,omitempty"`
|
|
||||||
CloseOnExit bool `hcl:"close_on_exit" json:"close-on-exit,omitempty"`
|
|
||||||
CursorBlink bool `hcl:"cursor_blink" json:"cursor-blink,omitempty"`
|
|
||||||
CursorBlinkCycle [2]int `hcl:"cursor_blink_cycle" json:"cursor-blink-cycle,omitempty"`
|
|
||||||
CursorColor string `hcl:"cursor_color" json:"cursor-color,omitempty"`
|
|
||||||
ColorPaletteOverrides []*string `hcl:"color_palette_overrides" json:"color-palette-overrides,omitempty"`
|
|
||||||
CopyOnSelect bool `hcl:"copy_on_select" json:"copy-on-select,omitempty"`
|
|
||||||
UseDefaultWindowCopy bool `hcl:"use_default_window_copy" json:"use-default-window-copy,omitempty"`
|
|
||||||
ClearSelectionAfterCopy bool `hcl:"clear_selection_after_copy" json:"clear-selection-after-copy,omitempty"`
|
|
||||||
CtrlPlusMinusZeroZoom bool `hcl:"ctrl_plus_minus_zero_zoom" json:"ctrl-plus-minus-zero-zoom,omitempty"`
|
|
||||||
CtrlCCopy bool `hcl:"ctrl_c_copy" json:"ctrl-c-copy,omitempty"`
|
|
||||||
CtrlVPaste bool `hcl:"ctrl_v_paste" json:"ctrl-v-paste,omitempty"`
|
|
||||||
EastAsianAmbiguousAsTwoColumn bool `hcl:"east_asian_ambiguous_as_two_column" json:"east-asian-ambiguous-as-two-column,omitempty"`
|
|
||||||
Enable8BitControl *bool `hcl:"enable_8_bit_control" json:"enable-8-bit-control,omitempty"`
|
|
||||||
EnableBold *bool `hcl:"enable_bold" json:"enable-bold,omitempty"`
|
|
||||||
EnableBoldAsBright bool `hcl:"enable_bold_as_bright" json:"enable-bold-as-bright,omitempty"`
|
|
||||||
EnableClipboardNotice bool `hcl:"enable_clipboard_notice" json:"enable-clipboard-notice,omitempty"`
|
|
||||||
EnableClipboardWrite bool `hcl:"enable_clipboard_write" json:"enable-clipboard-write,omitempty"`
|
|
||||||
EnableDec12 bool `hcl:"enable_dec12" json:"enable-dec12,omitempty"`
|
|
||||||
Environment map[string]string `hcl:"environment" json:"environment,omitempty"`
|
|
||||||
FontFamily string `hcl:"font_family" json:"font-family,omitempty"`
|
|
||||||
FontSize int `hcl:"font_size" json:"font-size,omitempty"`
|
|
||||||
FontSmoothing string `hcl:"font_smoothing" json:"font-smoothing,omitempty"`
|
|
||||||
ForegroundColor string `hcl:"foreground_color" json:"foreground-color,omitempty"`
|
|
||||||
HomeKeysScroll bool `hcl:"home_keys_scroll" json:"home-keys-scroll,omitempty"`
|
|
||||||
Keybindings map[string]string `hcl:"keybindings" json:"keybindings,omitempty"`
|
|
||||||
MaxStringSequence int `hcl:"max_string_sequence" json:"max-string-sequence,omitempty"`
|
|
||||||
MediaKeysAreFkeys bool `hcl:"media_keys_are_fkeys" json:"media-keys-are-fkeys,omitempty"`
|
|
||||||
MetaSendsEscape bool `hcl:"meta_sends_escape" json:"meta-sends-escape,omitempty"`
|
|
||||||
MousePasteButton *int `hcl:"mouse_paste_button" json:"mouse-paste-button,omitempty"`
|
|
||||||
PageKeysScroll bool `hcl:"page_keys_scroll" json:"page-keys-scroll,omitempty"`
|
|
||||||
PassAltNumber *bool `hcl:"pass_alt_number" json:"pass-alt-number,omitempty"`
|
|
||||||
PassCtrlNumber *bool `hcl:"pass_ctrl_number" json:"pass-ctrl-number,omitempty"`
|
|
||||||
PassMetaNumber *bool `hcl:"pass_meta_number" json:"pass-meta-number,omitempty"`
|
|
||||||
PassMetaV bool `hcl:"pass_meta_v" json:"pass-meta-v,omitempty"`
|
|
||||||
ReceiveEncoding string `hcl:"receive_encoding" json:"receive-encoding,omitempty"`
|
|
||||||
ScrollOnKeystroke bool `hcl:"scroll_on_keystroke" json:"scroll-on-keystroke,omitempty"`
|
|
||||||
ScrollOnOutput bool `hcl:"scroll_on_output" json:"scroll-on-output,omitempty"`
|
|
||||||
ScrollbarVisible bool `hcl:"scrollbar_visible" json:"scrollbar-visible,omitempty"`
|
|
||||||
ScrollWheelMoveMultiplier int `hcl:"scroll_wheel_move_multiplier" json:"scroll-wheel-move-multiplier,omitempty"`
|
|
||||||
SendEncoding string `hcl:"send_encoding" json:"send-encoding,omitempty"`
|
|
||||||
ShiftInsertPaste bool `hcl:"shift_insert_paste" json:"shift-insert-paste,omitempty"`
|
|
||||||
UserCss string `hcl:"user_css" json:"user-css,omitempty"`
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RunOptions holds a set of configurations for Server.Run().
|
|
||||||
type RunOptions struct {
|
|
||||||
gracefullCtx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunOption is an option of Server.Run().
|
|
||||||
type RunOption func(*RunOptions)
|
|
||||||
|
|
||||||
// WithGracefullContext accepts a context to shutdown a Server
|
|
||||||
// with care for existing client connections.
|
|
||||||
func WithGracefullContext(ctx context.Context) RunOption {
|
|
||||||
return func(options *RunOptions) {
|
|
||||||
options.gracefullCtx = ctx
|
|
||||||
}
|
|
||||||
}
|
|
259
server/server.go
259
server/server.go
@ -1,259 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
noesctmpl "text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/NYTimes/gziphandler"
|
|
||||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/sorenisanerd/gotty/pkg/homedir"
|
|
||||||
"github.com/sorenisanerd/gotty/pkg/randomstring"
|
|
||||||
"github.com/sorenisanerd/gotty/webtty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Server provides a webtty HTTP endpoint.
|
|
||||||
type Server struct {
|
|
||||||
factory Factory
|
|
||||||
options *Options
|
|
||||||
|
|
||||||
upgrader *websocket.Upgrader
|
|
||||||
indexTemplate *template.Template
|
|
||||||
titleTemplate *noesctmpl.Template
|
|
||||||
manifestTemplate *template.Template
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new instance of Server.
|
|
||||||
// Server will use the New() of the factory provided to handle each request.
|
|
||||||
func New(factory Factory, options *Options) (*Server, error) {
|
|
||||||
indexData, err := Asset("static/index.html")
|
|
||||||
if err != nil {
|
|
||||||
panic("index not found") // must be in bindata
|
|
||||||
}
|
|
||||||
if options.IndexFile != "" {
|
|
||||||
path := homedir.Expand(options.IndexFile)
|
|
||||||
indexData, err = ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to read custom index file at `%s`", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
indexTemplate, err := template.New("index").Parse(string(indexData))
|
|
||||||
if err != nil {
|
|
||||||
panic("index template parse failed") // must be valid
|
|
||||||
}
|
|
||||||
|
|
||||||
manifestData, err := Asset("static/manifest.json")
|
|
||||||
if err != nil {
|
|
||||||
panic("manifest not found") // must be in bindata
|
|
||||||
}
|
|
||||||
manifestTemplate, err := template.New("manifest").Parse(string(manifestData))
|
|
||||||
if err != nil {
|
|
||||||
panic("manifest template parse failed") // must be valid
|
|
||||||
}
|
|
||||||
|
|
||||||
titleTemplate, err := noesctmpl.New("title").Parse(options.TitleFormat)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to parse window title format `%s`", options.TitleFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
var originChekcer func(r *http.Request) bool
|
|
||||||
if options.WSOrigin != "" {
|
|
||||||
matcher, err := regexp.Compile(options.WSOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to compile regular expression of Websocket Origin: %s", options.WSOrigin)
|
|
||||||
}
|
|
||||||
originChekcer = func(r *http.Request) bool {
|
|
||||||
return matcher.MatchString(r.Header.Get("Origin"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Server{
|
|
||||||
factory: factory,
|
|
||||||
options: options,
|
|
||||||
|
|
||||||
upgrader: &websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
Subprotocols: webtty.Protocols,
|
|
||||||
CheckOrigin: originChekcer,
|
|
||||||
},
|
|
||||||
indexTemplate: indexTemplate,
|
|
||||||
titleTemplate: titleTemplate,
|
|
||||||
manifestTemplate: manifestTemplate,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run starts the main process of the Server.
|
|
||||||
// The cancelation of ctx will shutdown the server immediately with aborting
|
|
||||||
// existing connections. Use WithGracefullContext() to support gracefull shutdown.
|
|
||||||
func (server *Server) Run(ctx context.Context, options ...RunOption) error {
|
|
||||||
cctx, cancel := context.WithCancel(ctx)
|
|
||||||
opts := &RunOptions{gracefullCtx: context.Background()}
|
|
||||||
for _, opt := range options {
|
|
||||||
opt(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
counter := newCounter(time.Duration(server.options.Timeout) * time.Second)
|
|
||||||
|
|
||||||
path := "/"
|
|
||||||
if server.options.EnableRandomUrl {
|
|
||||||
path = "/" + randomstring.Generate(server.options.RandomUrlLength) + "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
handlers := server.setupHandlers(cctx, cancel, path, counter)
|
|
||||||
srv, err := server.setupHTTPServer(handlers)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to setup an HTTP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
if server.options.PermitWrite {
|
|
||||||
log.Printf("Permitting clients to write input to the PTY.")
|
|
||||||
}
|
|
||||||
if server.options.Once {
|
|
||||||
log.Printf("Once option is provided, accepting only one client")
|
|
||||||
}
|
|
||||||
|
|
||||||
if server.options.Port == "0" {
|
|
||||||
log.Printf("Port number configured to `0`, choosing a random port")
|
|
||||||
}
|
|
||||||
hostPort := net.JoinHostPort(server.options.Address, server.options.Port)
|
|
||||||
listener, err := net.Listen("tcp", hostPort)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to listen at `%s`", hostPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
scheme := "http"
|
|
||||||
if server.options.EnableTLS {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
host, port, _ := net.SplitHostPort(listener.Addr().String())
|
|
||||||
log.Printf("HTTP server is listening at: %s", scheme+"://"+net.JoinHostPort(host, port)+path)
|
|
||||||
if server.options.Address == "0.0.0.0" {
|
|
||||||
for _, address := range listAddresses() {
|
|
||||||
log.Printf("Alternative URL: %s", scheme+"://"+net.JoinHostPort(address, port)+path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
srvErr := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
if server.options.EnableTLS {
|
|
||||||
crtFile := homedir.Expand(server.options.TLSCrtFile)
|
|
||||||
keyFile := homedir.Expand(server.options.TLSKeyFile)
|
|
||||||
log.Printf("TLS crt file: " + crtFile)
|
|
||||||
log.Printf("TLS key file: " + keyFile)
|
|
||||||
|
|
||||||
err = srv.ServeTLS(listener, crtFile, keyFile)
|
|
||||||
} else {
|
|
||||||
err = srv.Serve(listener)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
srvErr <- err
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-opts.gracefullCtx.Done():
|
|
||||||
srv.Shutdown(context.Background())
|
|
||||||
case <-cctx.Done():
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err = <-srvErr:
|
|
||||||
if err == http.ErrServerClosed { // by gracefull ctx
|
|
||||||
err = nil
|
|
||||||
} else {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
case <-cctx.Done():
|
|
||||||
srv.Close()
|
|
||||||
err = cctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := counter.count()
|
|
||||||
if conn > 0 {
|
|
||||||
log.Printf("Waiting for %d connections to be closed", conn)
|
|
||||||
}
|
|
||||||
counter.wait()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) setupHandlers(ctx context.Context, cancel context.CancelFunc, pathPrefix string, counter *counter) http.Handler {
|
|
||||||
staticFileHandler := http.FileServer(
|
|
||||||
&assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "static"},
|
|
||||||
)
|
|
||||||
|
|
||||||
var siteMux = http.NewServeMux()
|
|
||||||
siteMux.HandleFunc(pathPrefix, server.handleIndex)
|
|
||||||
siteMux.Handle(pathPrefix+"js/", http.StripPrefix(pathPrefix, staticFileHandler))
|
|
||||||
siteMux.Handle(pathPrefix+"favicon.png", http.StripPrefix(pathPrefix, staticFileHandler))
|
|
||||||
siteMux.Handle(pathPrefix+"css/", http.StripPrefix(pathPrefix, staticFileHandler))
|
|
||||||
siteMux.Handle(pathPrefix+"icon_192.png", http.StripPrefix(pathPrefix, staticFileHandler))
|
|
||||||
|
|
||||||
siteMux.HandleFunc(pathPrefix+"manifest.json", server.handleManifest)
|
|
||||||
siteMux.HandleFunc(pathPrefix+"auth_token.js", server.handleAuthToken)
|
|
||||||
siteMux.HandleFunc(pathPrefix+"config.js", server.handleConfig)
|
|
||||||
|
|
||||||
siteHandler := http.Handler(siteMux)
|
|
||||||
|
|
||||||
if server.options.EnableBasicAuth {
|
|
||||||
log.Printf("Using Basic Authentication")
|
|
||||||
siteHandler = server.wrapBasicAuth(siteHandler, server.options.Credential)
|
|
||||||
}
|
|
||||||
|
|
||||||
withGz := gziphandler.GzipHandler(server.wrapHeaders(siteHandler))
|
|
||||||
siteHandler = server.wrapLogger(withGz)
|
|
||||||
|
|
||||||
wsMux := http.NewServeMux()
|
|
||||||
wsMux.Handle("/", siteHandler)
|
|
||||||
wsMux.HandleFunc(pathPrefix+"ws", server.generateHandleWS(ctx, cancel, counter))
|
|
||||||
siteHandler = http.Handler(wsMux)
|
|
||||||
|
|
||||||
return siteHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) setupHTTPServer(handler http.Handler) (*http.Server, error) {
|
|
||||||
srv := &http.Server{
|
|
||||||
Handler: handler,
|
|
||||||
}
|
|
||||||
|
|
||||||
if server.options.EnableTLSClientAuth {
|
|
||||||
tlsConfig, err := server.tlsConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to setup TLS configuration")
|
|
||||||
}
|
|
||||||
srv.TLSConfig = tlsConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
return srv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) tlsConfig() (*tls.Config, error) {
|
|
||||||
caFile := homedir.Expand(server.options.TLSCACrtFile)
|
|
||||||
caCert, err := ioutil.ReadFile(caFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("could not open CA crt file " + caFile)
|
|
||||||
}
|
|
||||||
caCertPool := x509.NewCertPool()
|
|
||||||
if !caCertPool.AppendCertsFromPEM(caCert) {
|
|
||||||
return nil, errors.New("could not parse CA crt file data in " + caFile)
|
|
||||||
}
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
ClientCAs: caCertPool,
|
|
||||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
||||||
}
|
|
||||||
return tlsConfig, nil
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sorenisanerd/gotty/webtty"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Slave is webtty.Slave with some additional methods.
|
|
||||||
type Slave interface {
|
|
||||||
webtty.Slave
|
|
||||||
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Factory interface {
|
|
||||||
Name() string
|
|
||||||
New(params map[string][]string) (Slave, error)
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
type wsWrapper struct {
|
|
||||||
*websocket.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wsw *wsWrapper) Write(p []byte) (n int, err error) {
|
|
||||||
writer, err := wsw.Conn.NextWriter(websocket.TextMessage)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer writer.Close()
|
|
||||||
return writer.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wsw *wsWrapper) Read(p []byte) (n int, err error) {
|
|
||||||
for {
|
|
||||||
msgType, reader, err := wsw.Conn.NextReader()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if msgType != websocket.TextMessage {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader.Read(p)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/fatih/structs"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ApplyDefaultValues(struct_ interface{}) (err error) {
|
|
||||||
o := structs.New(struct_)
|
|
||||||
|
|
||||||
for _, field := range o.Fields() {
|
|
||||||
defaultValue := field.Tag("default")
|
|
||||||
if defaultValue == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var val interface{}
|
|
||||||
switch field.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
val = defaultValue
|
|
||||||
case reflect.Bool:
|
|
||||||
if defaultValue == "true" {
|
|
||||||
val = true
|
|
||||||
} else if defaultValue == "false" {
|
|
||||||
val = false
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("invalid bool expression: %v, use true/false", defaultValue)
|
|
||||||
}
|
|
||||||
case reflect.Int:
|
|
||||||
val, err = strconv.Atoi(defaultValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
val = field.Value()
|
|
||||||
}
|
|
||||||
field.Set(val)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
128
utils/flags.go
128
utils/flags.go
@ -1,128 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/structs"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"github.com/yudai/hcl"
|
|
||||||
|
|
||||||
"github.com/sorenisanerd/gotty/pkg/homedir"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GenerateFlags(options ...interface{}) (flags []cli.Flag, mappings map[string]string, err error) {
|
|
||||||
mappings = make(map[string]string)
|
|
||||||
|
|
||||||
for _, struct_ := range options {
|
|
||||||
o := structs.New(struct_)
|
|
||||||
for _, field := range o.Fields() {
|
|
||||||
flagName := field.Tag("flagName")
|
|
||||||
if flagName == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
envName := "GOTTY_" + strings.ToUpper(strings.Join(strings.Split(flagName, "-"), "_"))
|
|
||||||
mappings[flagName] = field.Name()
|
|
||||||
|
|
||||||
flagShortName := field.Tag("flagSName")
|
|
||||||
var aliases []string
|
|
||||||
if flagShortName != "" {
|
|
||||||
aliases = []string{flagShortName}
|
|
||||||
}
|
|
||||||
|
|
||||||
flagDescription := field.Tag("flagDescribe")
|
|
||||||
|
|
||||||
switch field.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
flags = append(flags, &cli.StringFlag{
|
|
||||||
Name: flagName,
|
|
||||||
Value: field.Value().(string),
|
|
||||||
Usage: flagDescription,
|
|
||||||
EnvVars: []string{envName},
|
|
||||||
Aliases: aliases,
|
|
||||||
})
|
|
||||||
case reflect.Bool:
|
|
||||||
flags = append(flags, &cli.BoolFlag{
|
|
||||||
Name: flagName,
|
|
||||||
Usage: flagDescription,
|
|
||||||
EnvVars: []string{envName},
|
|
||||||
Aliases: aliases,
|
|
||||||
})
|
|
||||||
case reflect.Int:
|
|
||||||
flags = append(flags, &cli.IntFlag{
|
|
||||||
Name: flagName,
|
|
||||||
Value: field.Value().(int),
|
|
||||||
Usage: flagDescription,
|
|
||||||
EnvVars: []string{envName},
|
|
||||||
Aliases: aliases,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApplyFlags(
|
|
||||||
flags []cli.Flag,
|
|
||||||
mappingHint map[string]string,
|
|
||||||
c *cli.Context,
|
|
||||||
options ...interface{},
|
|
||||||
) {
|
|
||||||
objects := make([]*structs.Struct, len(options))
|
|
||||||
for i, struct_ := range options {
|
|
||||||
objects[i] = structs.New(struct_)
|
|
||||||
}
|
|
||||||
|
|
||||||
for flagName, fieldName := range mappingHint {
|
|
||||||
if !c.IsSet(flagName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var field *structs.Field
|
|
||||||
var ok bool
|
|
||||||
for _, o := range objects {
|
|
||||||
field, ok = o.FieldOk(fieldName)
|
|
||||||
if ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if field == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var val interface{}
|
|
||||||
switch field.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
val = c.String(flagName)
|
|
||||||
case reflect.Bool:
|
|
||||||
val = c.Bool(flagName)
|
|
||||||
case reflect.Int:
|
|
||||||
val = c.Int(flagName)
|
|
||||||
}
|
|
||||||
field.Set(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApplyConfigFile(filePath string, options ...interface{}) error {
|
|
||||||
filePath = homedir.Expand(filePath)
|
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileString := []byte{}
|
|
||||||
log.Printf("Loading config file at: %s", filePath)
|
|
||||||
fileString, err := ioutil.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, object := range options {
|
|
||||||
if err := hcl.Decode(object, string(fileString)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
var Version = "unknown_version"
|
|
||||||
var CommitID = "unknown_commit"
|
|
@ -1,3 +0,0 @@
|
|||||||
// Package webtty provides a protocl and an implementation to
|
|
||||||
// controll terminals thorough networks.
|
|
||||||
package webtty
|
|
@ -1,13 +0,0 @@
|
|||||||
package webtty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrSlaveClosed indicates the function has exited by the slave
|
|
||||||
ErrSlaveClosed = errors.New("slave closed")
|
|
||||||
|
|
||||||
// ErrSlaveClosed is returned when the slave connection is closed.
|
|
||||||
ErrMasterClosed = errors.New("master closed")
|
|
||||||
)
|
|
@ -1,8 +0,0 @@
|
|||||||
package webtty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Master represents a PTY master, usually it's a websocket connection.
|
|
||||||
type Master io.ReadWriter
|
|
@ -1,33 +0,0 @@
|
|||||||
package webtty
|
|
||||||
|
|
||||||
// Protocols defines the name of this protocol,
|
|
||||||
// which is supposed to be used to the subprotocol of Websockt streams.
|
|
||||||
var Protocols = []string{"webtty"}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Unknown message type, maybe sent by a bug
|
|
||||||
UnknownInput = '0'
|
|
||||||
// User input typically from a keyboard
|
|
||||||
Input = '1'
|
|
||||||
// Ping to the server
|
|
||||||
Ping = '2'
|
|
||||||
// Notify that the browser size has been changed
|
|
||||||
ResizeTerminal = '3'
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Unknown message type, maybe set by a bug
|
|
||||||
UnknownOutput = '0'
|
|
||||||
// Normal output to the terminal
|
|
||||||
Output = '1'
|
|
||||||
// Pong to the browser
|
|
||||||
Pong = '2'
|
|
||||||
// Set window title of the terminal
|
|
||||||
SetWindowTitle = '3'
|
|
||||||
// Set terminal preference
|
|
||||||
SetPreferences = '4'
|
|
||||||
// Make terminal to reconnect
|
|
||||||
SetReconnect = '5'
|
|
||||||
// Set the input buffer size
|
|
||||||
SetBufferSize = '6'
|
|
||||||
)
|
|
@ -1,62 +0,0 @@
|
|||||||
package webtty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Option is an option for WebTTY.
|
|
||||||
type Option func(*WebTTY) error
|
|
||||||
|
|
||||||
// WithPermitWrite sets a WebTTY to accept input from slaves.
|
|
||||||
func WithPermitWrite() Option {
|
|
||||||
return func(wt *WebTTY) error {
|
|
||||||
wt.permitWrite = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFixedColumns sets a fixed width to TTY master.
|
|
||||||
func WithFixedColumns(columns int) Option {
|
|
||||||
return func(wt *WebTTY) error {
|
|
||||||
wt.columns = columns
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFixedRows sets a fixed height to TTY master.
|
|
||||||
func WithFixedRows(rows int) Option {
|
|
||||||
return func(wt *WebTTY) error {
|
|
||||||
wt.rows = rows
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithWindowTitle sets the default window title of the session
|
|
||||||
func WithWindowTitle(windowTitle []byte) Option {
|
|
||||||
return func(wt *WebTTY) error {
|
|
||||||
wt.windowTitle = windowTitle
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithReconnect enables reconnection on the master side.
|
|
||||||
func WithReconnect(timeInSeconds int) Option {
|
|
||||||
return func(wt *WebTTY) error {
|
|
||||||
wt.reconnect = timeInSeconds
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMasterPreferences sets an optional configuration of master.
|
|
||||||
func WithMasterPreferences(preferences interface{}) Option {
|
|
||||||
return func(wt *WebTTY) error {
|
|
||||||
prefs, err := json.Marshal(preferences)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to marshal preferences as JSON")
|
|
||||||
}
|
|
||||||
wt.masterPrefs = prefs
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package webtty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Slave represents a PTY slave, typically it's a local command.
|
|
||||||
type Slave interface {
|
|
||||||
io.ReadWriter
|
|
||||||
|
|
||||||
// WindowTitleVariables returns any values that can be used to fill out
|
|
||||||
// the title of a terminal.
|
|
||||||
WindowTitleVariables() map[string]interface{}
|
|
||||||
|
|
||||||
// ResizeTerminal sets a new size of the terminal.
|
|
||||||
ResizeTerminal(columns int, rows int) error
|
|
||||||
}
|
|
225
webtty/webtty.go
225
webtty/webtty.go
@ -1,225 +0,0 @@
|
|||||||
package webtty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WebTTY bridges a PTY slave and its PTY master.
|
|
||||||
// To support text-based streams and side channel commands such as
|
|
||||||
// terminal resizing, WebTTY uses an original protocol.
|
|
||||||
type WebTTY struct {
|
|
||||||
// PTY Master, which probably a connection to browser
|
|
||||||
masterConn Master
|
|
||||||
// PTY Slave
|
|
||||||
slave Slave
|
|
||||||
|
|
||||||
windowTitle []byte
|
|
||||||
permitWrite bool
|
|
||||||
columns int
|
|
||||||
rows int
|
|
||||||
reconnect int // in seconds
|
|
||||||
masterPrefs []byte
|
|
||||||
|
|
||||||
bufferSize int
|
|
||||||
writeMutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new instance of WebTTY.
|
|
||||||
// masterConn is a connection to the PTY master,
|
|
||||||
// typically it's a websocket connection to a client.
|
|
||||||
// slave is a PTY slave such as a local command with a PTY.
|
|
||||||
func New(masterConn Master, slave Slave, options ...Option) (*WebTTY, error) {
|
|
||||||
wt := &WebTTY{
|
|
||||||
masterConn: masterConn,
|
|
||||||
slave: slave,
|
|
||||||
|
|
||||||
permitWrite: false,
|
|
||||||
columns: 0,
|
|
||||||
rows: 0,
|
|
||||||
|
|
||||||
bufferSize: 1024,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, option := range options {
|
|
||||||
option(wt)
|
|
||||||
}
|
|
||||||
|
|
||||||
return wt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run starts the main process of the WebTTY.
|
|
||||||
// This method blocks until the context is canceled.
|
|
||||||
// Note that the master and slave are left intact even
|
|
||||||
// after the context is canceled. Closing them is caller's
|
|
||||||
// responsibility.
|
|
||||||
// If the connection to one end gets closed, returns ErrSlaveClosed or ErrMasterClosed.
|
|
||||||
func (wt *WebTTY) Run(ctx context.Context) error {
|
|
||||||
err := wt.sendInitializeMessage()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to send initializing message")
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make(chan error, 2)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
errs <- func() error {
|
|
||||||
buffer := make([]byte, wt.bufferSize)
|
|
||||||
for {
|
|
||||||
n, err := wt.slave.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return ErrSlaveClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wt.handleSlaveReadEvent(buffer[:n])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
errs <- func() error {
|
|
||||||
buffer := make([]byte, wt.bufferSize)
|
|
||||||
for {
|
|
||||||
n, err := wt.masterConn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return ErrMasterClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wt.handleMasterReadEvent(buffer[:n])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
err = ctx.Err()
|
|
||||||
case err = <-errs:
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wt *WebTTY) sendInitializeMessage() error {
|
|
||||||
err := wt.masterWrite(append([]byte{SetWindowTitle}, wt.windowTitle...))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to send window title")
|
|
||||||
}
|
|
||||||
|
|
||||||
bufSizeMsg, _ := json.Marshal(wt.bufferSize)
|
|
||||||
err = wt.masterWrite(append([]byte{SetBufferSize}, bufSizeMsg...))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to send buffer size")
|
|
||||||
}
|
|
||||||
|
|
||||||
if wt.reconnect > 0 {
|
|
||||||
reconnect, _ := json.Marshal(wt.reconnect)
|
|
||||||
err := wt.masterWrite(append([]byte{SetReconnect}, reconnect...))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to set reconnect")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if wt.masterPrefs != nil {
|
|
||||||
err := wt.masterWrite(append([]byte{SetPreferences}, wt.masterPrefs...))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to set preferences")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wt *WebTTY) handleSlaveReadEvent(data []byte) error {
|
|
||||||
safeMessage := base64.StdEncoding.EncodeToString(data)
|
|
||||||
err := wt.masterWrite(append([]byte{Output}, []byte(safeMessage)...))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to send message to master")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wt *WebTTY) masterWrite(data []byte) error {
|
|
||||||
wt.writeMutex.Lock()
|
|
||||||
defer wt.writeMutex.Unlock()
|
|
||||||
|
|
||||||
_, err := wt.masterConn.Write(data)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to write to master")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wt *WebTTY) handleMasterReadEvent(data []byte) error {
|
|
||||||
if len(data) == 0 {
|
|
||||||
return errors.New("unexpected zero length read from master")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch data[0] {
|
|
||||||
case Input:
|
|
||||||
if !wt.permitWrite {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) <= 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := wt.slave.Write(data[1:])
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to write received data to slave")
|
|
||||||
}
|
|
||||||
|
|
||||||
case Ping:
|
|
||||||
err := wt.masterWrite([]byte{Pong})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to return Pong message to master")
|
|
||||||
}
|
|
||||||
|
|
||||||
case ResizeTerminal:
|
|
||||||
if wt.columns != 0 && wt.rows != 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) <= 1 {
|
|
||||||
return errors.New("received malformed remote command for terminal resize: empty payload")
|
|
||||||
}
|
|
||||||
|
|
||||||
var args argResizeTerminal
|
|
||||||
err := json.Unmarshal(data[1:], &args)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "received malformed data for terminal resize")
|
|
||||||
}
|
|
||||||
rows := wt.rows
|
|
||||||
if rows == 0 {
|
|
||||||
rows = int(args.Rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
columns := wt.columns
|
|
||||||
if columns == 0 {
|
|
||||||
columns = int(args.Columns)
|
|
||||||
}
|
|
||||||
|
|
||||||
wt.slave.ResizeTerminal(columns, rows)
|
|
||||||
default:
|
|
||||||
return errors.Errorf("unknown message type `%c`", data[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type argResizeTerminal struct {
|
|
||||||
Columns float64
|
|
||||||
Rows float64
|
|
||||||
}
|
|
@ -1,139 +0,0 @@
|
|||||||
package webtty
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type pipePair struct {
|
|
||||||
*io.PipeReader
|
|
||||||
*io.PipeWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteFromPTY(t *testing.T) {
|
|
||||||
connInPipeReader, connInPipeWriter := io.Pipe() // in to conn
|
|
||||||
connOutPipeReader, _ := io.Pipe() // out from conn
|
|
||||||
|
|
||||||
conn := pipePair{
|
|
||||||
connOutPipeReader,
|
|
||||||
connInPipeWriter,
|
|
||||||
}
|
|
||||||
dt, err := New(conn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from New(): %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
wg.Done()
|
|
||||||
err := dt.Run(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from Run(): %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
message := []byte("foobar")
|
|
||||||
n, err := dt.TTY().Write(message)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from Write(): %s", err)
|
|
||||||
}
|
|
||||||
if n != len(message) {
|
|
||||||
t.Fatalf("Write() accepted `%d` for message `%s`", n, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, err = connInPipeReader.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from Read(): %s", err)
|
|
||||||
}
|
|
||||||
if buf[0] != Output {
|
|
||||||
t.Fatalf("Unexpected message type `%c`", buf[0])
|
|
||||||
}
|
|
||||||
decoded := make([]byte, 1024)
|
|
||||||
n, err = base64.StdEncoding.Decode(decoded, buf[1:n])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from Decode(): %s", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(decoded[:n], message) {
|
|
||||||
t.Fatalf("Unexpected message received: `%s`", decoded[:n])
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteFromConn(t *testing.T) {
|
|
||||||
connInPipeReader, connInPipeWriter := io.Pipe() // in to conn
|
|
||||||
connOutPipeReader, connOutPipeWriter := io.Pipe() // out from conn
|
|
||||||
|
|
||||||
conn := pipePair{
|
|
||||||
connOutPipeReader,
|
|
||||||
connInPipeWriter,
|
|
||||||
}
|
|
||||||
|
|
||||||
dt, err := New(conn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from New(): %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
wg.Done()
|
|
||||||
err := dt.Run(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from Run(): %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var (
|
|
||||||
message []byte
|
|
||||||
n int
|
|
||||||
)
|
|
||||||
readBuf := make([]byte, 1024)
|
|
||||||
|
|
||||||
// input
|
|
||||||
message = []byte("0hello\n") // line buffered canonical mode
|
|
||||||
n, err = connOutPipeWriter.Write(message)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from Write(): %s", err)
|
|
||||||
}
|
|
||||||
if n != len(message) {
|
|
||||||
t.Fatalf("Write() accepted `%d` for message `%s`", n, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err = dt.TTY().Read(readBuf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from Write(): %s", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(readBuf[:n], message[1:]) {
|
|
||||||
t.Fatalf("Unexpected message received: `%s`", readBuf[:n])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ping
|
|
||||||
message = []byte("1\n") // line buffered canonical mode
|
|
||||||
n, err = connOutPipeWriter.Write(message)
|
|
||||||
if n != len(message) {
|
|
||||||
t.Fatalf("Write() accepted `%d` for message `%s`", n, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err = connInPipeReader.Read(readBuf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error from Read(): %s", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(readBuf[:n], []byte{'1'}) {
|
|
||||||
t.Fatalf("Unexpected message received: `%s`", readBuf[:n])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: resize
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user