Separate the tty-share tool from the server side

pull/14/merge
Vasile Popescu 4 years ago
parent 30fa0c825c
commit a27cd48730

9
.gitignore vendored

@ -1,12 +1,5 @@
frontend/public/
output.log
**/node_modules/
.DS_Store
bundle.js
playground/
.vscode/
tty_sender*
tty_server*
tty-share
tmp-*
tty-server/assets_bundle.go
out/

@ -3,9 +3,4 @@ language: go
go:
- 1.12.x
node_js:
- 12.4.0
# make frontend all won't work. The frontend assets have to be already built at the time of
# evaluating the Makefile first time, so for now I do it in two turns.
script: make frontend && make all
script: go build

@ -1,69 +0,0 @@
DEPS=github.com/elisescu/pty github.com/sirupsen/logrus golang.org/x/crypto/ssh/terminal github.com/gorilla/mux github.com/gorilla/websocket github.com/go-bindata/go-bindata/...
DEST_DIR=./out
TTY_SERVER=$(DEST_DIR)/tty-server
TTY_SHARE=$(DEST_DIR)/tty-share
# We need to make sure the assets_bundle is in the list only onces in both these two special cases:
# a) first time, when the assets_bundle.go is generated, and b) when it's already existing there,
# but it has to be re-generated.
# Unfortunately, the assets_bundle.go seems to have to be in the same folder as the rest of the
# server sources, so that's why all this mess
TTY_SERVER_SRC=$(filter-out ./tty-server/assets_bundle.go, $(wildcard ./tty-server/*.go)) ./tty-server/assets_bundle.go
TTY_SHARE_SRC=$(wildcard ./tty-share/*.go)
COMMON_SRC=$(wildcard ./common/*go)
TTY_SERVER_ASSETS=$(wildcard frontend/public/*)
## Keep this as the first and default target, so no need to mess up with building the frontend&rest if the server side is not needed
$(TTY_SHARE): get-deps $(TTY_SHARE_SRC) $(COMMON_SRC)
go build -o $@ $(TTY_SHARE_SRC)
## Build both the server and the tty-share
all: get-deps $(TTY_SHARE) $(TTY_SERVER)
@echo "All done"
get-deps:
go get $(DEPS)
# Building the server and tty-share
$(TTY_SERVER): get-deps $(TTY_SERVER_SRC) $(COMMON_SRC)
go build -o $@ $(TTY_SERVER_SRC)
tty-server/assets_bundle.go: $(TTY_SERVER_ASSETS)
go-bindata --prefix frontend/public/ -o $@ $^
%.zip: %
zip $@ $^
frontend: force
cd frontend && npm install && npm run build && cd -
force:
# Other different targets
## tty-share release binaries for Linux and OSX
# tty-share: $(OUT_DIR)/tty-share.osx $(OUT_DIR)/tty-share.linux
release: $(TTY_SHARE).osx.zip $(TTY_SHARE).lin.zip
@echo "Done: " $@
$(TTY_SHARE).lin: $(TTY_SHARE_SRC) $(COMMON_SRC)
GOOS=linux go build -o $@ $(TTY_SHARE_SRC)
$(TTY_SHARE).osx: $(TTY_SHARE_SRC) $(COMMON_SRC)
GOOS=darwin go build -o $@ $(TTY_SHARE_SRC)
clean:
rm -fr out/
rm -fr frontend/public
@echo "Cleaned"
## Development helper targets
### Runs the server, without TLS/HTTPS (no need for localhost testing)
runs: $(TTY_SERVER)
$(TTY_SERVER) --url http://localhost:9090 --web_address :9090 --sender_address :7654 -frontend_path ./frontend/public
### Runs the sender, without TLS (no need for localhost testing)
runc: $(TTY_SHARE)
$(TTY_SHARE) --useTLS=false --server localhost:7654
test:
@go test github.com/elisescu/tty-share/testing -v

@ -6,11 +6,11 @@ It is a very simple command line tool that gives remote access to a UNIX termina
The most important part about it is that it requires **no setup** on the remote end. All I need to give remote access to the terminal (a bash/shell session) is the binary tool, and the remote person only needs to open a secret URL in their browser.
The project consists of two command line utilities: `tty-share` and `tty-server`.
The project consists of two command line utilities: `tty-share` and `tty-server`. The server side has been moved to [github.com/elisescu/tty-server](https://github.com/elisescu/tty-server) repo, so it's easier to build the `tty-share` tool separately.
The `tty-share` is used on the machine that wants to share the terminal, and it connects to the server to generate a secret URL, over which the terminal can be viewed in the browser.
The server runs at [tty-share.com](https://tty-share.com), so you only need the `tty-server` binary if you want to host it yourself.
An instance of the server runs at [tty-share.com](https://tty-share.com), so you only need the `tty-server` binary if you want to host it yourself.
![demo](doc/demo.gif)
@ -34,54 +34,16 @@ bash$
If you want to just build the tool that shares your terminal, and not the server, then simply do a
```
make out/tty-share
go get github.com/elisescu/tty-share
```
This way you don't have to bother about the server side, nor about building the frontend, and you will get only the `tty-share` cmd line tool, inside `out` folder.
For cross-compilation you can use the GO building [environment variables](https://golang.org/doc/install/source#environment). For example, to build the `tty-share` for raspberrypi, you can do `GOOS=linux GOARCH=arm GOARM=6 make out/tty-share` (you can check your raspberrypi arch with `uname -a`).
## Building and running everything
For an easy deployment, the `tty-server` is by bundling by default all frontend resources inside the final binary. So in the end, there will be only one file to be copied and deployed. However, the frontend resources can also be served from a local folder, with a command line flag.
### Build all
```
cd frontend
nvm use
npm install
npm run build # builds the frontend
cd -
make all # builds both the sender and server. Check the Makefile for more details
```
### Run a development server
```
make runs
```
Will run the server on the localhost.
### Run a development sender
```
make runc
```
Will run the sender and connect it to the server running on the local host (so the above command has
to be ran first).
For more info, on how to run, see the Makefile, or the help of the two binaries (`tty-share` and `tty_receiver`)
The project didn't follow the typical way of building go applications, because everything is put in one single project and package, for the ease of development, and also because the bundle containing the frontend has to be built as well. Perhaps there's a better way, but this works for now.
## TLS and HTTPS
At the moment the `tty-share` supports connecting over a TLS connection to the server, but the server doesn't have that implemented yet. However, the server can easily run behind a proxy which can take care of encrypting the connections from the senders and receivers (doing both TLS and HTTPS), without the server knowing about it.
## Security
The server at [tty-share](https://tty-share.com) is using both TLS and https for both sides, relying on nginx reverse proxy.
`tty-share` connects over a TLS connection to the server, which uses a proxy for the SSL termination, and the browser terminal is served over HTTPS. The communication on both sides is encrypted and secured, in the same way as other similar tools are doing it (e.g. tmate, VSC, etc).
However, the `tty-server` should maybe also have native support for being able to listen on TLS connections from the sender as well. This can easily be added in the future.
However, end-to-end encryption is still desired, so nothing but the sender and receiver can decrypt the data passed around.
## TODO

@ -1,2 +0,0 @@
12.4.0

@ -1,9 +0,0 @@
# Readme #
## Building
The frontend uses webpack to build everything in a bundle file. Run:
```
npm install
webpack
```

File diff suppressed because it is too large Load Diff

@ -1,22 +0,0 @@
{
"name": "tty-share",
"version": "1.0.0",
"description": "",
"main": "",
"scripts": {
"watch": "TTY_SHARE_ENV=development webpack --watch",
"build": "TTY_SHARE_ENV=production webpack"
},
"author": "elisescu",
"license": "elisescu",
"dependencies": {
"copy-webpack-plugin": "4.5.1",
"css-loader": "^3.0.0",
"style-loader": "^0.23.1",
"ts-loader": "^6.0.2",
"typescript": "^3.5.2",
"webpack": "^4.34.0",
"webpack-cli": "^3.3.4",
"xterm": "3.14.4"
}
}

@ -1,147 +0,0 @@
@import url(https://fonts.googleapis.com/css?family=Raleway:300,700);
body {
width:100%;
height:100%;
background:#0B486B;
font-family: 'Raleway', sans-serif;
font-weight:300;
margin:0;
padding:0;
}
#title {
text-align:center;
font-size:40px;
margin-top:40px;
margin-bottom:-40px;
position:relative;
color:#fff;
}
.circles:after {
content:'';
display:inline-block;
width:100%;
height:100px;
background:#fff;
position:absolute;
top:-50px;
left:0;
transform:skewY(-4deg);
-webkit-transform:skewY(-4deg);
}
.circles {
background:#fff;
text-align: center;
position: relative;
margin-top:-60px;
box-shadow:inset -1px -4px 4px rgba(0,0,0,0.2);
}
.circles p {
font-size: 240px;
color: #fff;
padding-top: 60px;
position: relative;
z-index: 9;
line-height: 100%;
}
.circles p small {
font-size: 40px;
line-height: 100%;
vertical-align: top;
}
.circles .circle.small {
width: 140px;
height: 140px;
border-radius: 50%;
background: #0B486B;
position: absolute;
z-index: 1;
top: 80px;
left: 50%;
animation: 7s smallmove infinite cubic-bezier(1,.22,.71,.98);
-webkit-animation: 7s smallmove infinite cubic-bezier(1,.22,.71,.98);
animation-delay: 1.2s;
-webkit-animation-delay: 1.2s;
}
.circles .circle.med {
width: 200px;
height: 200px;
border-radius: 50%;
background: #0B486B;
position: absolute;
z-index: 1;
top: 0;
left: 10%;
animation: 7s medmove infinite cubic-bezier(.32,.04,.15,.75);
-webkit-animation: 7s medmove infinite cubic-bezier(.32,.04,.15,.75);
animation-delay: 0.4s;
-webkit-animation-delay: 0.4s;
}
.circles .circle.big {
width: 400px;
height: 400px;
border-radius: 50%;
background: #0B486B;
position: absolute;
z-index: 1;
top: 200px;
right: 0;
animation: 8s bigmove infinite;
-webkit-animation: 8s bigmove infinite;
animation-delay: 3s;
-webkit-animation-delay: 1s;
}
@-webkit-keyframes smallmove {
0% { top: 10px; left: 45%; opacity: 1; }
25% { top: 300px; left: 40%; opacity:0.7; }
50% { top: 240px; left: 55%; opacity:0.4; }
75% { top: 100px; left: 40%; opacity:0.6; }
100% { top: 10px; left: 45%; opacity: 1; }
}
@keyframes smallmove {
0% { top: 10px; left: 45%; opacity: 1; }
25% { top: 300px; left: 40%; opacity:0.7; }
50% { top: 240px; left: 55%; opacity:0.4; }
75% { top: 100px; left: 40%; opacity:0.6; }
100% { top: 10px; left: 45%; opacity: 1; }
}
@-webkit-keyframes medmove {
0% { top: 0px; left: 20%; opacity: 1; }
25% { top: 300px; left: 80%; opacity:0.7; }
50% { top: 240px; left: 55%; opacity:0.4; }
75% { top: 100px; left: 40%; opacity:0.6; }
100% { top: 0px; left: 20%; opacity: 1; }
}
@keyframes medmove {
0% { top: 0px; left: 20%; opacity: 1; }
25% { top: 300px; left: 80%; opacity:0.7; }
50% { top: 240px; left: 55%; opacity:0.4; }
75% { top: 100px; left: 40%; opacity:0.6; }
100% { top: 0px; left: 20%; opacity: 1; }
}
@-webkit-keyframes bigmove {
0% { top: 0px; right: 4%; opacity: 0.5; }
25% { top: 100px; right: 40%; opacity:0.4; }
50% { top: 240px; right: 45%; opacity:0.8; }
75% { top: 100px; right: 35%; opacity:0.6; }
100% { top: 0px; right: 4%; opacity: 0.5; }
}
@keyframes bigmove {
0% { top: 0px; right: 4%; opacity: 0.5; }
25% { top: 100px; right: 40%; opacity:0.4; }
50% { top: 240px; right: 45%; opacity:0.8; }
75% { top: 100px; right: 35%; opacity:0.6; }
100% { top: 0px; right: 4%; opacity: 0.5; }
}

@ -1,23 +0,0 @@
<!-- This is taken from: https://colorlib.com/wp/free-404-error-page-templates/ -->
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="/static/404.css">
</head>
<body>
<section id="not-found">
<div id="title">Something's wrong</div>
<div class="circles">
<p>404
<br>
<small>Page not found</small>
</p>
<span class="circle big"></span>
<span class="circle med"></span>
<span class="circle small"></span>
</div>
</section>
</body>
</html>

File diff suppressed because one or more lines are too long

@ -1,22 +0,0 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="/static/bootstrap.min.css">
</head>
<style>
.jumbotron {
background-color: #0B486B;
color: #ffffff;
font-family: 'Raleway', sans-serif;
}
</style>
<body>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-4">Invalid session</h1>
<p class="lead">This session doesn't exist, or has already ended.</p>
</div>
</div>
</body>
</html>

@ -1,21 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Terminal</title>
</head>
<body>
<div id="terminal"></div>
<div id="settings"></div>
<script type="text/javascript">
window.ttyInitialData = {
sessionID: {{.SessionID}},
salt: {{.Salt}},
wsPath: {{.WSPath}}
}
console.log("Initial data", window.ttyInitialData)
</script>
<script src="/static/tty-receiver.js"></script>
</body>
</html>

@ -1,10 +0,0 @@
{
"compilerOptions": {
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}

@ -1,156 +0,0 @@
/**
*
* Base64 encode / decode
* http://www.webtoolkit.info/
*
**/
var Base64 = {
// private property
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
// public method for encoding
encode: function (input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = Base64._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
}
return output;
},
// public method for decoding
decode: function (input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = Base64._utf8_decode(output);
return output;
},
// private method for UTF-8 encoding
_utf8_encode: function (string) {
string = string.replace(/\r\n/g, "\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if ((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
},
// private method for UTF-8 decoding
_utf8_decode: function (utftext) {
let string = "";
let i = 0;
let c = 0;
let c1 = 0;
let c2 = 0;
let c3 = 0;
while (i < utftext.length) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
}
else if ((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
}
else {
c2 = utftext.charCodeAt(i + 1);
c3 = utftext.charCodeAt(i + 2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
},
base64ToArrayBuffer: function (input) {
var binary_string = window.atob(input);
var len = binary_string.length;
var bytes = new Uint8Array( len );
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes;
}
}
export default Base64;

@ -1,13 +0,0 @@
html, body {
width: 100%;
height: 100%;
margin: 0;
}
#terminal {
position: fixed;
top: 0;
width: 100%;
height: 100%;
}

@ -1,28 +0,0 @@
import 'xterm/dist/xterm.css';
import './main.css';
import { Terminal } from 'xterm';
import * as pbkdf2 from 'pbkdf2';
import { TTYReceiver } from './tty-receiver';
const term = new Terminal({
cursorBlink: true,
macOptionIsMeta: true,
});
const derivedKey = pbkdf2.pbkdf2Sync('password', 'salt', 4096, 32, 'sha256');
console.log(derivedKey);
let wsAddress = "";
if (window.location.protocol === "https:") {
wsAddress = 'wss://';
} else {
wsAddress = "ws://";
}
let ttyWindow = window as any;
wsAddress += ttyWindow.location.host + ttyWindow.ttyInitialData.wsPath;
const ttyReceiver = new TTYReceiver(wsAddress, document.getElementById('terminal') as HTMLDivElement);

@ -1,114 +0,0 @@
import { Terminal, IEvent, IDisposable } from "xterm";
import base64 from './base64';
interface IRectSize {
width: number;
height: number;
}
class TTYReceiver {
private xterminal: Terminal;
private containerElement: HTMLElement;
constructor(wsAddress: string, container: HTMLDivElement) {
const connection = new WebSocket(wsAddress);
this.xterminal = new Terminal({
cursorBlink: true,
macOptionIsMeta: true,
scrollback: 0,
fontSize: 12,
letterSpacing: 0,
});
this.containerElement = container;
this.xterminal.open(container);
connection.onclose = (evt: CloseEvent) => {
this.xterminal.blur();
this.xterminal.setOption('cursorBlink', false);
this.xterminal.clear();
this.xterminal.write('Session closed');
}
this.xterminal.focus();
const containerPixSize = this.getElementPixelsSize(container);
const newFontSize = this.guessNewFontSize(this.xterminal.cols, this.xterminal.rows, containerPixSize.width, containerPixSize.height);
this.xterminal.setOption('fontSize', newFontSize);
connection.onmessage = (ev: MessageEvent) => {
let message = JSON.parse(ev.data)
let msgData = base64.decode(message.Data)
if (message.Type === "Write") {
let writeMsg = JSON.parse(msgData)
this.xterminal.writeUtf8(base64.base64ToArrayBuffer(writeMsg.Data));
}
if (message.Type == "WinSize") {
let winSizeMsg = JSON.parse(msgData)
const containerPixSize = this.getElementPixelsSize(container);
const newFontSize = this.guessNewFontSize(winSizeMsg.Cols, winSizeMsg.Rows, containerPixSize.width, containerPixSize.height);
this.xterminal.setOption('fontSize', newFontSize);
// Now set the new size.
this.xterminal.resize(winSizeMsg.Cols, winSizeMsg.Rows)
}
}
// TODO: .on() is deprecated. Should be replaced.
this.xterminal.on('data', function (data) {
let writeMessage = {
Type: "Write",
Data: base64.encode(JSON.stringify({ Size: data.length, Data: base64.encode(data)})),
}
let dataToSend = JSON.stringify(writeMessage)
connection.send(dataToSend);
});
}
// Get the pixels size of the element, after all CSS was applied. This will be used in an ugly
// hack to guess what fontSize to set on the xterm object. Horrible hack, but I feel less bad
// about it seeing that VSV does it too:
// https://github.com/microsoft/vscode/blob/d14ee7613fcead91c5c3c2bddbf288c0462be876/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts#L363
private getElementPixelsSize(element: HTMLElement): IRectSize {
const defView = this.containerElement.ownerDocument.defaultView;
let width = parseInt(defView.getComputedStyle(element).getPropertyValue('width').replace('px', ''), 10);
let height = parseInt(defView.getComputedStyle(element).getPropertyValue('height').replace('px', ''), 10);
return {
width,
height,
}
}
// Tries to guess the new font size, for the new terminal size, so that the rendered terminal
// will have the newWidth and newHeight dimensions
private guessNewFontSize(newCols: number, newRows: number, targetWidth: number, targetHeight: number): number {
const cols = this.xterminal.cols;
const rows = this.xterminal.rows;
const fontSize = this.xterminal.getOption('fontSize');
const xtermPixelsSize = this.getElementPixelsSize(this.containerElement.querySelector(".xterm-screen"));
const newHFontSizeMultiplier = (cols / newCols) * (targetWidth / xtermPixelsSize.width);
const newVFontSizeMultiplier = (rows / newRows) * (targetHeight / xtermPixelsSize.height);
let newFontSize;
if (newHFontSizeMultiplier > newVFontSizeMultiplier) {
newFontSize = Math.floor(fontSize * newVFontSizeMultiplier);
} else {
newFontSize = Math.floor(fontSize * newHFontSizeMultiplier);
}
return newFontSize;
}
}
export {
TTYReceiver
}

@ -1,49 +0,0 @@
const webpack = require("webpack");
const copyWebpackPlugin = require('copy-webpack-plugin')
const develBuild = process.env.TTY_SHARE_ENV === 'development';
let mainConfig = {
entry: {
'tty-receiver': './tty-receiver/main.ts',
},
output: {
path: __dirname + '/public/',
filename: '[name].js',
},
mode: develBuild ? 'development' : 'production',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /node_modules.+xterm.+\.map$/,
use: ['ignore-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
plugins: [
new copyWebpackPlugin([
'static',
'templates',
], {
debug: 'info',
}),
],
};
if (develBuild) {
mainConfig.devtool = 'inline-source-map';
}
module.exports = mainConfig;

@ -11,7 +11,6 @@ import (
"os"
"strings"
"github.com/elisescu/tty-share/common"
logrus "github.com/sirupsen/logrus"
)
@ -59,8 +58,8 @@ func main() {
}
}
serverConnection := common.NewTTYProtocolConn(rawConnection)
reply, err := serverConnection.InitSender(common.SenderSessionInfo{
serverConnection := NewTTYProtocolConn(rawConnection)
reply, err := serverConnection.InitSender(SenderSessionInfo{
Salt: "salt",
PasswordVerifierA: "PV_A",
})
@ -107,13 +106,13 @@ func main() {
break
}
if msg.Type == common.MsgIDWrite {
var msgWrite common.MsgTTYWrite
if msg.Type == MsgIDWrite {
var msgWrite MsgTTYWrite
json.Unmarshal(msg.Data, &msgWrite)
ptyMaster.Write(msgWrite.Data[:msgWrite.Size])
}
if msg.Type == common.MsgIDSenderNewReceiverConnected {
var msgReceiverConnected common.MsgTTYSenderNewReceiverConnected
if msg.Type == MsgIDSenderNewReceiverConnected {
var msgReceiverConnected MsgTTYSenderNewReceiverConnected
json.Unmarshal(msg.Data, &msgReceiverConnected)
ptyMaster.Refresh()

@ -1,4 +1,4 @@
package common
package main
import (
"encoding/json"

@ -1,117 +0,0 @@
package testing
import (
"errors"
"fmt"
"io"
"net"
"time"
)
type fakeWriteCb func(io.Writer, []byte) (int, error)
// Use this carefully. Not thread safe
type fakeTCPConn struct {
readPipe *io.PipeReader
writePipe *io.PipeWriter
debug bool
writeCb fakeWriteCb
deadline time.Time
}
func NewFakeTCPConn(debug bool, writeCb fakeWriteCb) *fakeTCPConn {
ret := &fakeTCPConn{debug: debug}
ret.readPipe, ret.writePipe = io.Pipe()
ret.writeCb = writeCb
return ret
}
func (conn *fakeTCPConn) Write(b []byte) (int, error) {
if conn.debug {
fmt.Printf("fakeTCP.Write: %s\n", string(b))
}
if conn.writeCb != nil {
return conn.writeCb(conn.writePipe, b)
}
return conn.writePipe.Write(b)
}
// If Read times out, the connection can't be used anymore.
// TODO: maybe fix that
func (conn *fakeTCPConn) Read(b []byte) (int, error) {
c := make(chan int)
n := 0
err := error(nil)
doRead := func() {
n, err = conn.readPipe.Read(b)
if conn.debug {
fmt.Printf("fakeTCP.Read: %s\n", string(b))
}
}
// If we have no deadline, then let Read wait forever
var zeroTime time.Time
if conn.deadline == zeroTime {
doRead()
return n, err
}
// Otherwise, do the read in a go routine
go func() {
doRead()
close(c)
}()
select {
case <-c:
return n, err
case <-time.After(conn.deadline.Sub(time.Now())):
// TODO: we timed out. What to do? Close the pipe?
conn.writePipe.CloseWithError(errors.New("timeout"))
conn.readPipe.CloseWithError(errors.New("timeout"))
// don't return here - closing with error, will make the readPipe.Read return
// the above error, passed to ClosedWithError
}
return n, err
}
func (conn *fakeTCPConn) Close() (err error) {
err = conn.writePipe.Close()
if err != nil {
return
}
err = conn.readPipe.Close()
if err != nil {
return
}
return
}
func (conn *fakeTCPConn) LocalAddr() net.Addr {
panic("LocalAddr not implemented")
return nil
}
func (conn *fakeTCPConn) RemoteAddr() net.Addr {
panic("RemoteAddr not implemented")
return nil
}
func (conn *fakeTCPConn) SetDeadline(t time.Time) error {
conn.deadline = t
return nil
}
func (conn *fakeTCPConn) SetReadDeadline(t time.Time) error {
panic("SetReadDeadline not implemented")
return nil
}
func (conn *fakeTCPConn) SetWriteDeadline(t time.Time) error {
panic("SetWriteDeadline not implemented")
return nil
}

@ -1,240 +0,0 @@
package testing
import (
"fmt"
"io"
"sync"
"testing"
"time"
"github.com/elisescu/tty-share/common"
)
// Returns true if waiting for the wg timed out
func wgWaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
timeoutChan := make(chan int)
go func() {
wg.Wait()
timeoutChan <- 3
close(timeoutChan)
}()
select {
case <-timeoutChan:
return false
case <-time.After(timeout):
return true
}
}
func TestInitOk(t *testing.T) {
tcpConn := NewFakeTCPConn(false, nil)
ttyConn := common.NewTTYSenderConnection(tcpConn)
defer ttyConn.Close()
senderSessionInfo := common.SenderSessionInfo{
Salt: fmt.Sprintf("salt_%d", time.Now().UnixNano()),
PasswordVerifierA: fmt.Sprintf("pass_a_%d", time.Now().UnixNano()),
}
serverSessionInfo := common.ServerSessionInfo{
URLWebReadWrite: fmt.Sprintf("http://%x:", time.Now().UnixNano()),
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
err, res := ttyConn.InitSender(senderSessionInfo)
if err != nil {
panic(fmt.Sprintf("Can't initialise the sender side: %s", err.Error()))
}
if res.URLWebReadWrite != serverSessionInfo.URLWebReadWrite {
panic(fmt.Sprintf("Received URL different from expected: <%s> != <%s>",
res.URLWebReadWrite, serverSessionInfo.URLWebReadWrite))
}
}()
err, res := ttyConn.InitServer(serverSessionInfo)
if err != nil {
t.Fatalf("Can't Initialise the server side: %s", err.Error())
}
if res.PasswordVerifierA != senderSessionInfo.PasswordVerifierA || res.Salt != senderSessionInfo.Salt {
t.Fatalf("Received invalid sender session info: <%s> != <%s>, <%s> != <%s> ",
res.PasswordVerifierA, senderSessionInfo.PasswordVerifierA, res.Salt, senderSessionInfo.Salt)
}
if wgWaitTimeout(&wg, 10*time.Millisecond) {
t.Fatalf("Waiting for initialisation took too long")
}
}
func TestInitServerBrokenConnection(t *testing.T) {
tcpConn := NewFakeTCPConn(false, nil)
ttyConn := common.NewTTYSenderConnection(tcpConn)
defer ttyConn.Close()
serverSessionInfo := common.ServerSessionInfo{
URLWebReadWrite: fmt.Sprintf("http://%x:", time.Now().UnixNano()),
}
senderSessionInfo := common.SenderSessionInfo{
Salt: fmt.Sprintf("salt_%d", time.Now().UnixNano()),
PasswordVerifierA: fmt.Sprintf("pass_a_%d", time.Now().UnixNano()),
}
var wg sync.WaitGroup
wg.Add(2)
// This should make the InitServer and InitSender fail
tcpConn.Close()
go func() {
defer wg.Done()
err, _ := ttyConn.InitServer(serverSessionInfo)
if err == nil {
panic("Expected the connection to fail, but it didn't")
}
}()
go func() {
defer wg.Done()
err, _ := ttyConn.InitSender(senderSessionInfo)
if err == nil {
panic("Expected the connection to fail, but it didn't")
}
}()
// Timeout the test
if wgWaitTimeout(&wg, 500*time.Millisecond) {
t.Fatalf("Waiting for initialisation took too long")
}
}
func TestInitBrokenWrite(t *testing.T) {
brokenWrite := func(writer io.Writer, b []byte) (int, error) {
// Write just half of the bytes
return writer.Write(b[:len(b)/2])
}
tcpConn := NewFakeTCPConn(false, brokenWrite)
ttyConn := common.NewTTYSenderConnection(tcpConn)
ttyConn.SetDeadline(time.Now().Add(time.Second * 1))
defer ttyConn.Close()
senderSessionInfo := common.SenderSessionInfo{
Salt: fmt.Sprintf("salt_%d", time.Now().UnixNano()),
PasswordVerifierA: fmt.Sprintf("pass_a_%d", time.Now().UnixNano()),
}
serverSessionInfo := common.ServerSessionInfo{
URLWebReadWrite: fmt.Sprintf("http://%x:", time.Now().UnixNano()),
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
err, _ := ttyConn.InitSender(senderSessionInfo)
if err == nil {
panic("Expected error when InitSender, but got nil")
}
}()
go func() {
defer wg.Done()
err, _ := ttyConn.InitServer(serverSessionInfo)
if err == nil {
panic("Expected error when InitServer, but got nil")
}
}()
if wgWaitTimeout(&wg, 2*time.Second) {
t.Fatalf("Waiting for initialisation took too long")
}
}
func TestWriteOk(t *testing.T) {
client, server := NewDoubleNetConn(false)
ttyConnC := common.NewTTYSenderConnection(client)
ttyConnS := common.NewTTYSenderConnection(server)
defer ttyConnC.Close()
defer ttyConnS.Close()
var wg sync.WaitGroup
wg.Add(4)
go func() {
defer wg.Done()
go func() {
defer wg.Done()
// If HandleReceive() returns an error, it must be because of closing the connection.
// If not, it will be caught anyways when comparing the actual data.
for {
err := ttyConnC.HandleReceive()
if err != nil {
break
}
}
}()
buff := make([]byte, 1024)
for i := 0; i < 100; i++ {
data := fmt.Sprintf("Data %d", i)
nw, errw := ttyConnC.Write([]byte(data))
nr, errr := ttyConnC.Read(buff)
if errw != nil {
panic(fmt.Sprintf("Couldn't write: %s", errw.Error()))
}
if errr != nil {
panic(fmt.Sprintf("Couldn't read: %s", errr.Error()))
}
if nr != nw {
panic(fmt.Sprintf("Unexpected number if bytes written and read: %d != %d", nw, nr))
}
rcvData := string(buff[:nr])
if data != rcvData {
panic(fmt.Sprintf("Unexpected data: expected vs expected: <%s> != <%s>", data, rcvData))
}
}
ttyConnC.Close()
}()
go func() {
defer wg.Done()
go func() {
defer wg.Done()
// If HandleReceive() returns an error, it must be because of closing the connection.
// If not, it will be caught anyways when comparing the actual data.
for {
err := ttyConnS.HandleReceive()
if err != nil {
break
}
}
}()
// Make the server echo back what it received
io.Copy(ttyConnS, ttyConnS)
}()
if wgWaitTimeout(&wg, 3*time.Second) {
t.Fatalf("Timed out")
}
}

@ -1,37 +0,0 @@
package testing
import (
"net"
"sync"
"github.com/elisescu/tty-share/common"
)
func NewDoubleNetConn(debug bool) (client net.Conn, server net.Conn) {
var wg sync.WaitGroup
wg.Add(1)
var err error
listener, err := net.Listen("tcp", "localhost:0")
defer listener.Close()
if err != nil {
panic(err.Error())
}
go func() {
server, err = listener.Accept()
if err != nil {
panic(err.Error())
}
wg.Done()
}()
client, err = net.Dial("tcp", listener.Addr().String())
if err != nil {
panic(err.Error())
}
wg.Wait()
return common.NewWrappedConn(client, debug), common.NewWrappedConn(server, debug)
}

@ -1,59 +0,0 @@
package common
import (
"fmt"
"net"
"time"
)
type wConn struct {
conn net.Conn
debug bool
}
func NewWrappedConn(conn net.Conn, debug bool) net.Conn {
return &wConn{
conn: conn,
debug: debug,
}
}
func (c *wConn) Read(b []byte) (n int, err error) {
n, err = c.conn.Read(b)
if c.debug {
fmt.Printf("%s.Read: <%s>, err %s\n", c.conn.LocalAddr().String(), string(b), err)
}
return n, err
}
func (c *wConn) Write(b []byte) (n int, err error) {
n, err = c.conn.Write(b)
if c.debug {
fmt.Printf("%s.Wrote: <%s>, err %s\n", c.conn.LocalAddr().String(), string(b), err)
}
return
}
func (c *wConn) Close() error {
return c.conn.Close()
}
func (c *wConn) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *wConn) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *wConn) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *wConn) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
func (c *wConn) SetWriteDeadline(t time.Time) error {
return c.conn.SetWriteDeadline(t)
}

@ -1,294 +0,0 @@
package main
import (
"errors"
"html/template"
"mime"
"net"
"net/http"
"os"
"path/filepath"
"sync"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)
const (
errorInvalidSession = iota
errorNotFound = iota
errorNotAllowed = iota
)
var log = MainLogger
// SessionTemplateModel used for templating
type SessionTemplateModel struct {
SessionID string
Salt string
WSPath string
}
// TTYServerConfig is used to configure the tty server before it is started
type TTYServerConfig struct {
WebAddress string
TTYSenderAddress string
ServerURL string
// The TLS Cert and Key can be null, if TLS should not be used
TLSCertFile string
TLSKeyFile string
FrontendPath string
}
// TTYServer represents the instance of a tty server
type TTYServer struct {
httpServer *http.Server
ttySendersListener net.Listener
config TTYServerConfig
activeSessions map[string]*ttyShareSession
activeSessionsRWLock sync.RWMutex
}
func (server *TTYServer) serveContent(w http.ResponseWriter, r *http.Request, name string) {
// If a path to the frontend resources was passed, serve from there, otherwise, serve from the
// builtin bundle
if server.config.FrontendPath == "" {
file, err := Asset(name)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
ctype := mime.TypeByExtension(filepath.Ext(name))
if ctype == "" {
ctype = http.DetectContentType(file)
}
w.Header().Set("Content-Type", ctype)
w.Write(file)
} else {
filePath := server.config.FrontendPath + string(os.PathSeparator) + name
_, err := os.Open(filePath)
if err != nil {
log.Errorf("Couldn't find resource: %s at %s", name, filePath)
w.WriteHeader(http.StatusNotFound)
return
}
log.Debugf("Serving %s from %s", name, filePath)
http.ServeFile(w, r, filePath)
}
}
// NewTTYServer creates a new instance
func NewTTYServer(config TTYServerConfig) (server *TTYServer) {
server = &TTYServer{
config: config,
}
server.httpServer = &http.Server{
Addr: config.WebAddress,
}
routesHandler := mux.NewRouter()
routesHandler.PathPrefix("/static/").Handler(http.StripPrefix("/static/",
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server.serveContent(w, r, r.URL.Path)
})))
routesHandler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://github.com/elisescu/tty-share", http.StatusMovedPermanently)
})
routesHandler.HandleFunc("/s/{sessionID}", func(w http.ResponseWriter, r *http.Request) {
server.handleSession(w, r)
})
routesHandler.HandleFunc("/ws/{sessionID}", func(w http.ResponseWriter, r *http.Request) {
server.handleWebsocket(w, r)
})
routesHandler.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server.serveContent(w, r, "404.html")
})
server.activeSessions = make(map[string]*ttyShareSession)
server.httpServer.Handler = routesHandler
return server
}
func getWSPath(sessionID string) string {
return "/ws/" + sessionID
}
func (server *TTYServer) handleWebsocket(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionID := vars["sessionID"]
defer log.Debug("Finished WS connection for ", sessionID)
// Validate incoming request.
if r.Method != "GET" {
w.WriteHeader(http.StatusForbidden)
return
}
// Upgrade to Websocket mode.
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error("Cannot create the WS connection for session ", sessionID, ". Error: ", err.Error())
return
}
session := getSession(server, sessionID)
if session == nil {
log.Error("WE connection for invalid sessionID: ", sessionID, ". Killing it.")
w.WriteHeader(http.StatusForbidden)
return
}
session.HandleReceiver(newWSConnection(conn))
}
func (server *TTYServer) handleSession(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionID := vars["sessionID"]
log.Debug("Handling web TTYReceiver session: ", sessionID)
session := getSession(server, sessionID)
// No valid session with this ID
if session == nil {
server.serveContent(w, r, "invalid-session.html")
return
}
var t *template.Template
var err error
if server.config.FrontendPath == "" {
templateDta, err := Asset("tty-receiver.in.html")
if err != nil {
panic("Cannot find the tty-receiver html template")
}
t = template.New("tty-receiver.html")
_, err = t.Parse(string(templateDta))
} else {
t, err = template.ParseFiles(server.config.FrontendPath + string(os.PathSeparator) + "tty-receiver.in.html")
}
if err != nil {
panic("Cannot parse the tty-receiver html template")
}
templateModel := SessionTemplateModel{
SessionID: sessionID,
Salt: "salt&pepper",
WSPath: getWSPath(sessionID),
}
err = t.Execute(w, templateModel)
if err != nil {
panic("Cannot execute the tty-receiver html template")
}
}
func addNewSession(server *TTYServer, session *ttyShareSession) {
server.activeSessionsRWLock.Lock()
server.activeSessions[session.GetID()] = session
server.activeSessionsRWLock.Unlock()
}
func removeSession(server *TTYServer, session *ttyShareSession) {
server.activeSessionsRWLock.Lock()
delete(server.activeSessions, session.GetID())
server.activeSessionsRWLock.Unlock()
}
func getSession(server *TTYServer, sessionID string) (session *ttyShareSession) {
// TODO: move this in a better place
server.activeSessionsRWLock.RLock()
session = server.activeSessions[sessionID]
server.activeSessionsRWLock.RUnlock()
return
}
func handleTTYSenderConnection(server *TTYServer, conn net.Conn) {
defer conn.Close()
session := newTTYShareSession(conn, server.config.ServerURL)
if err := session.InitSender(); err != nil {
log.Warnf("Cannot create session with %s. Error: %s", conn.RemoteAddr().String(), err.Error())
return
}
addNewSession(server, session)
session.HandleSenderConnection()
removeSession(server, session)
log.Debug("Finished session ", session.GetID(), ". Removing it.")
}
// Listen starts listening on connections
func (server *TTYServer) Listen() (err error) {
var wg sync.WaitGroup
runTLS := server.config.TLSCertFile != "" && server.config.TLSKeyFile != ""
// Start listening on the frontend side
wg.Add(1)
go func() {
if !runTLS {
err = server.httpServer.ListenAndServe()
} else {
err = server.httpServer.ListenAndServeTLS(server.config.TLSCertFile, server.config.TLSKeyFile)
}
// Just in case we are existing because of an error, close the other listener too
if server.ttySendersListener != nil {
server.ttySendersListener.Close()
}
wg.Done()
}()
// TODO: Add support for listening for connections over TLS
// Listen on connections on the tty sender side
server.ttySendersListener, err = net.Listen("tcp", server.config.TTYSenderAddress)
if err != nil {
log.Error("Cannot create the front server. Error: ", err.Error())
return
}
for {
connection, err := server.ttySendersListener.Accept()
if err == nil {
go handleTTYSenderConnection(server, connection)
} else {
break
}
}
// Close the http side too
if server.httpServer != nil {
server.httpServer.Close()
}
wg.Wait()
log.Debug("Server finished")
return
}
// Stop closes down the server
func (server *TTYServer) Stop() error {
log.Debug("Stopping the server")
err1 := server.httpServer.Close()
err2 := server.ttySendersListener.Close()
if err1 != nil || err2 != nil {
//TODO: do this nicer
return errors.New(err1.Error() + err2.Error())
}
return nil
}

@ -1,47 +0,0 @@
package main
import (
"flag"
"os"
"os/signal"
logrus "github.com/sirupsen/logrus"
)
// MainLogger is the logger that will be used across the whole main package. I whish I knew of a better way
var MainLogger = logrus.New()
func main() {
webAddress := flag.String("web_address", ":80", "The bind address for the web interface. This is the listening address for the web server that hosts the \"browser terminal\". You might want to change this if you don't want to use the port 80, or only bind the localhost.")
senderAddress := flag.String("sender_address", ":6543", "The bind address for the tty-share TLS connections. tty-share tool will connect to this address.")
url := flag.String("url", "http://localhost", "The public web URL the server will be accessible at. This will be sent back to the tty-share tool to display it to the user.")
frontendPath := flag.String("frontend_path", "", "The path to the frontend resources. By default, these resources are included in the server binary, so you only need this path if you don't want to use the bundled ones.")
flag.Parse()
log := MainLogger
log.SetLevel(logrus.DebugLevel)
config := TTYServerConfig{
WebAddress: *webAddress,
TTYSenderAddress: *senderAddress,
ServerURL: *url,
FrontendPath: *frontendPath,
}
server := NewTTYServer(config)
// Install a signal and wait until we get Ctrl-C
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
s := <-c
log.Debug("Received signal <", s, ">. Stopping the server")
server.Stop()
}()
log.Info("Listening on address: http://", config.WebAddress, ", and TCP://", config.TTYSenderAddress)
err := server.Listen()
log.Debug("Exiting. Error: ", err)
}

@ -1,200 +0,0 @@
package main
import (
"container/list"
"crypto/rand"
"encoding/base64"
"encoding/json"
"net"
"sync"
. "github.com/elisescu/tty-share/common"
)
type sessionInfo struct {
ID string
URLWebReadWrite string
}
type ttyShareSession struct {
sessionID string
serverURL string
mainRWLock sync.RWMutex
ttySenderConnection *TTYProtocolConn
ttyReceiverConnections *list.List
isAlive bool
lastWindowSizeMsg MsgAll
}
func generateNewSessionID() string {
binID := make([]byte, 32)
_, err := rand.Read(binID)
if err != nil {
panic(err)
}
return base64.URLEncoding.EncodeToString([]byte(binID))
}
func newTTYShareSession(conn net.Conn, serverURL string) *ttyShareSession {
sessionID := generateNewSessionID()
ttyShareSession := &ttyShareSession{
sessionID: sessionID,
serverURL: serverURL,
ttySenderConnection: NewTTYProtocolConn(conn),
ttyReceiverConnections: list.New(),
}
return ttyShareSession
}
func (session *ttyShareSession) InitSender() error {
_, err := session.ttySenderConnection.InitServer(ServerSessionInfo{
URLWebReadWrite: session.serverURL + "/s/" + session.GetID(),
})
return err
}
func (session *ttyShareSession) GetID() string {
return session.sessionID
}
func copyList(l *list.List) *list.List {
newList := list.New()
for e := l.Front(); e != nil; e = e.Next() {
newList.PushBack(e.Value)
}
return newList
}
func (session *ttyShareSession) handleSenderMessageLock(msg MsgAll) {
switch msg.Type {
case MsgIDWinSize:
// Save the last known size of the window so we pass it to new receivers, and then
// fallthrough. We save the WinSize message as we get it, since we send it anyways
// to the receivers, packed into the same protocol
session.mainRWLock.Lock()
session.lastWindowSizeMsg = msg
session.mainRWLock.Unlock()
fallthrough
case MsgIDWrite:
data, _ := json.Marshal(msg)
session.forEachReceiverLock(func(rcvConn *TTYProtocolConn) bool {
rcvConn.WriteRawData(data)
return true
})
}
}
// Will run on the ttySendeConnection go routine (e.g.: in the TCP connection routine)
func (session *ttyShareSession) HandleSenderConnection() {
session.mainRWLock.Lock()
session.isAlive = true
senderConnection := session.ttySenderConnection
session.mainRWLock.Unlock()
for {
msg, err := senderConnection.ReadMessage()
if err != nil {
log.Debugf("TTYSender connnection finished withs with error: %s", err.Error())
break
}
session.handleSenderMessageLock(msg)
}
// Close the connection to all the receivers
log.Debugf("Closing all receiver connection")
session.forEachReceiverLock(func(recvConn *TTYProtocolConn) bool {
log.Debugf("Closing receiver connection")
recvConn.Close()
return true
})
// TODO: clear here the list of receiver
session.mainRWLock.Lock()
session.isAlive = false
session.mainRWLock.Unlock()
}
// Runs the callback cb for each of the receivers in the list of the receivers, as it was when
// this function was called. Note that there might be receivers which might have lost
// the connection since this function was called.
// Return false in the callback to not continue for the rest of the receivers
func (session *ttyShareSession) forEachReceiverLock(cb func(rcvConn *TTYProtocolConn) bool) {
session.mainRWLock.RLock()
// TODO: Maybe find a better way?
rcvsCopy := copyList(session.ttyReceiverConnections)
session.mainRWLock.RUnlock()
for receiverE := rcvsCopy.Front(); receiverE != nil; receiverE = receiverE.Next() {
receiver := receiverE.Value.(*TTYProtocolConn)
if !cb(receiver) {
break
}
}
}
// Will run on the TTYReceiver connection go routine (e.g.: on the websockets connection routine)
// When HandleReceiver will exit, the connection to the TTYReceiver will be closed
func (session *ttyShareSession) HandleReceiver(rawConn *WSConnection) {
rcvProtoConn := NewTTYProtocolConn(rawConn)
session.mainRWLock.Lock()
if !session.isAlive {
log.Warnf("TTYReceiver tried to connect to a session that is not alive anymore. Rejecting it..")
session.mainRWLock.Unlock()
return
}
// Add the receiver to the list of receivers in the seesion, so we need to write-lock
rcvHandleEl := session.ttyReceiverConnections.PushBack(rcvProtoConn)
senderConn := session.ttySenderConnection
lastWindowSize, _ := json.Marshal(session.lastWindowSizeMsg)
session.mainRWLock.Unlock()
log.Debugf("Got new TTYReceiver connection (%s). Serving it..", rawConn.Address())
// Sending the initial size of the window, if we have one
rcvProtoConn.WriteRawData(lastWindowSize)
// Notify the tty-share that we got a new receiver connected
msgRcvConnected, err := MarshalMsg(MsgTTYSenderNewReceiverConnected{
Name: rawConn.Address(),
})
senderConn.WriteRawData(msgRcvConnected)
if err != nil {
log.Errorf("Cannot notify tty sender. Error: %s", err.Error())
}
// Wait until the TTYReceiver will close the connection on its end
for {
msg, err := rcvProtoConn.ReadMessage()
if err != nil {
log.Warnf("Finishing handling the TTYReceiver loop because: %s", err.Error())
break
}
switch msg.Type {
case MsgIDWinSize:
// Ignore these messages from the receiver. For now, the policy is that the sender
// decides on the window size.
case MsgIDWrite:
rawData, _ := json.Marshal(msg)
senderConn.WriteRawData(rawData)
default:
log.Warnf("Receiving unknown data from the receiver")
}
}
log.Debugf("Closing receiver connection")
rcvProtoConn.Close()
// Remove the recevier from the list of the receiver of this session, so we need to write-lock
session.mainRWLock.Lock()
session.ttyReceiverConnections.Remove(rcvHandleEl)
session.mainRWLock.Unlock()
}

@ -1,43 +0,0 @@
package main
import (
"github.com/gorilla/websocket"
)
type WSConnection struct {
connection *websocket.Conn
address string
}
func newWSConnection(conn *websocket.Conn) *WSConnection {
return &WSConnection{
connection: conn,
address: conn.RemoteAddr().String(),
}
}
func (handle *WSConnection) Write(data []byte) (n int, err error) {
w, err := handle.connection.NextWriter(websocket.TextMessage)
if err != nil {
return 0, err
}
n, err = w.Write(data)
w.Close()
return
}
func (handle *WSConnection) Close() (err error) {
return handle.connection.Close()
}
func (handle *WSConnection) Address() string {
return handle.address
}
func (handle *WSConnection) Read(data []byte) (int, error) {
_, r, err := handle.connection.NextReader()
if err != nil {
return 0, err
}
return r.Read(data)
}

@ -1,4 +1,4 @@
package common
package main
import (
"encoding/json"
Loading…
Cancel
Save