mirror of https://github.com/elisescu/tty-share
First step to adding support for local terminal sharing
Add the tty-server code to this repo as the first step for adding support to share the terminal in the local network. The public server should be involved only for public sessions.pull/25/head
parent
6fc776b924
commit
ba44046699
@ -0,0 +1,45 @@
|
||||
TTY_SERVER=./tty-server
|
||||
|
||||
TTY_SERVER_ASSETS=$(wildcard frontend/public/*) frontend/public/index.html
|
||||
TTY_SERVER_SRC=$(wildcard *.go) assets_bundle.go
|
||||
|
||||
.PHONY: all frontend clean cleanfront rebuild
|
||||
|
||||
all: $(TTY_SERVER)
|
||||
@echo "Done"
|
||||
|
||||
rebuild: clean all
|
||||
|
||||
# Building the server and tty-share
|
||||
$(TTY_SERVER): $(TTY_SERVER_SRC)
|
||||
go build -o $@
|
||||
|
||||
assets_bundle.go: $(TTY_SERVER_ASSETS)
|
||||
go get github.com/go-bindata/go-bindata/...
|
||||
go-bindata --prefix frontend/public/ -o $@ frontend/public/*
|
||||
|
||||
%.zip: %
|
||||
zip $@ $^
|
||||
|
||||
frontend: cleanfront frontend/public/index.html
|
||||
|
||||
frontend/public/index.html:
|
||||
cd frontend && npm install && npm run build && cd -
|
||||
|
||||
cleanfront:
|
||||
rm -fr frontend/public
|
||||
|
||||
clean: cleanfront
|
||||
rm -fr tty-server assets_bundle.go
|
||||
@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 --useTLS=false --server localhost:7654
|
||||
|
||||
test:
|
||||
@go test github.com/elisescu/tty-share/testing -v
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,2 @@
|
||||
12.4.0
|
||||
|
@ -0,0 +1,9 @@
|
||||
# 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
@ -0,0 +1,22 @@
|
||||
{
|
||||
"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": "^6.0.0",
|
||||
"css-loader": "^3.6.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^6.2.2",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"xterm": "^4.9.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
@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; }
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<!-- 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
@ -0,0 +1 @@
|
||||
Hi there
|
@ -0,0 +1,22 @@
|
||||
<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>
|
@ -0,0 +1,21 @@
|
||||
<!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>
|
||||
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"jsx": "react",
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
/**
|
||||
*
|
||||
* 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;
|
@ -0,0 +1,13 @@
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#terminal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
import 'xterm/css/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);
|
@ -0,0 +1,113 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
this.xterminal.onData(function (data:string) {
|
||||
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
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
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({
|
||||
patterns: [
|
||||
{from: 'static', },
|
||||
{from: 'templates',},
|
||||
]}),
|
||||
],
|
||||
};
|
||||
|
||||
if (develBuild) {
|
||||
mainConfig.devtool = 'inline-source-map';
|
||||
}
|
||||
|
||||
module.exports = mainConfig;
|
@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type ProtocolMessageIDType string
|
||||
|
||||
const (
|
||||
MsgIDSenderInitRequest = "SenderInitRequest"
|
||||
MsgIDSenderInitReply = "SenderInitReply"
|
||||
MsgIDSenderNewReceiverConnected = "SenderNewReceiverConnected"
|
||||
MsgIDReceiverInitRequest = "ReceiverInitRequest"
|
||||
MsgIDReceiverInitReply = "ReceiverInitReply"
|
||||
MsgIDWrite = "Write"
|
||||
MsgIDWinSize = "WinSize"
|
||||
)
|
||||
|
||||
// Message used to encapsulate the rest of the bessages bellow
|
||||
type MsgAll struct {
|
||||
Type ProtocolMessageIDType
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// These messages are used between the server and the sender/receiver
|
||||
type MsgTTYSenderInitRequest struct {
|
||||
Salt string
|
||||
PasswordVerifierA string
|
||||
}
|
||||
|
||||
type MsgTTYSenderInitReply struct {
|
||||
ReceiverURLWebReadWrite string
|
||||
}
|
||||
|
||||
type MsgTTYSenderNewReceiverConnected struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type MsgTTYReceiverInitRequest struct {
|
||||
ChallengeReply string
|
||||
}
|
||||
|
||||
type MsgTTYReceiverInitReply struct {
|
||||
}
|
||||
|
||||
// These messages are not intended for the server, so they are just forwarded by it to the remote
|
||||
// side.
|
||||
type MsgTTYWrite struct {
|
||||
Data []byte
|
||||
Size int
|
||||
}
|
||||
|
||||
type MsgTTYWinSize struct {
|
||||
Cols int
|
||||
Rows int
|
||||
}
|
||||
|
||||
func ReadAndUnmarshalMsg(reader io.Reader, aMessage interface{}) (err error) {
|
||||
var wrapperMsg MsgAll
|
||||
// Wait here for the right message to come
|
||||
dec := json.NewDecoder(reader)
|
||||
err = dec.Decode(&wrapperMsg)
|
||||
|
||||
if err != nil {
|
||||
return errors.New("Cannot decode message: " + err.Error())
|
||||
}
|
||||
|
||||
err = json.Unmarshal(wrapperMsg.Data, aMessage)
|
||||
|
||||
if err != nil {
|
||||
return errors.New("Cannot decode message: " + err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func MarshalMsg(aMessage interface{}) (_ []byte, err error) {
|
||||
var msg MsgAll
|
||||
|
||||
if initRequestMsg, ok := aMessage.(MsgTTYSenderInitRequest); ok {
|
||||
msg.Type = MsgIDSenderInitRequest
|
||||
msg.Data, err = json.Marshal(initRequestMsg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
if initReplyMsg, ok := aMessage.(MsgTTYSenderInitReply); ok {
|
||||
msg.Type = MsgIDSenderInitReply
|
||||
msg.Data, err = json.Marshal(initReplyMsg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
if writeMsg, ok := aMessage.(MsgTTYWrite); ok {
|
||||
msg.Type = MsgIDWrite
|
||||
msg.Data, err = json.Marshal(writeMsg)
|
||||
//fmt.Printf("Sent write message %s\n", string(writeMsg.Data))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
if winChangedMsg, ok := aMessage.(MsgTTYWinSize); ok {
|
||||
msg.Type = MsgIDWinSize
|
||||
msg.Data, err = json.Marshal(winChangedMsg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
if newRcvMsg, ok := aMessage.(MsgTTYSenderNewReceiverConnected); ok {
|
||||
msg.Type = MsgIDSenderNewReceiverConnected
|
||||
msg.Data, err = json.Marshal(newRcvMsg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func MarshalAndWriteMsg(writer io.Writer, aMessage interface{}) (err error) {
|
||||
b, err := MarshalMsg(aMessage)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n, err := writer.Write(b)
|
||||
|
||||
if n != len(b) {
|
||||
err = fmt.Errorf("Unable to write : wrote %d out of %d bytes", n, len(b))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@ -0,0 +1,294 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type ServerSessionInfo struct {
|
||||
URLWebReadWrite string
|
||||
}
|
||||
|
||||
type ReceiverSessionInfo struct {
|
||||
}
|
||||
|
||||
type SenderSessionInfo struct {
|
||||
Salt string
|
||||
PasswordVerifierA string
|
||||
}
|
||||
|
||||
// TTYProtocolConn is the interface used to communicate with the sending (master) side of the TTY session
|
||||
type TTYProtocolConn struct {
|
||||
netConnection io.ReadWriteCloser
|
||||
jsonDecoder *json.Decoder
|
||||
}
|
||||
|
||||
func NewTTYProtocolConn(conn io.ReadWriteCloser) *TTYProtocolConn {
|
||||
return &TTYProtocolConn{
|
||||
netConnection: conn,
|
||||
jsonDecoder: json.NewDecoder(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (protoConn *TTYProtocolConn) ReadMessage() (msg MsgAll, err error) {
|
||||
// TODO: perhaps read here the error, and transform it to something that's understandable
|
||||
// from the outside in the context of this object
|
||||
err = protoConn.jsonDecoder.Decode(&msg)
|
||||
return
|
||||
}
|
||||
|
||||
func (protoConn *TTYProtocolConn) SetWinSize(cols, rows int) error {
|
||||
msgWinChanged := MsgTTYWinSize{
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
}
|
||||
return MarshalAndWriteMsg(protoConn.netConnection, msgWinChanged)
|
||||
}
|
||||
|
||||
func (protoConn *TTYProtocolConn) Close() error {
|
||||
return protoConn.netConnection.Close()
|
||||
}
|
||||
|
||||
// Function to send data from one the sender to the server and the other way around.
|
||||
func (protoConn *TTYProtocolConn) Write(buff []byte) (int, error) {
|
||||
msgWrite := MsgTTYWrite{
|
||||
Data: buff,
|
||||
Size: len(buff),
|
||||
}
|
||||
return len(buff), MarshalAndWriteMsg(protoConn.netConnection, msgWrite)
|
||||
}
|
||||
|
||||
func (protoConn *TTYProtocolConn) WriteRawData(buff []byte) (int, error) {
|
||||
return protoConn.netConnection.Write(buff)
|
||||
}
|
||||
|
||||
// Function to be called on the sender side, and which blocks until the protocol has been
|
||||
// initialised
|
||||
func (protoConn *TTYProtocolConn) InitSender(senderInfo SenderSessionInfo) (serverInfo ServerSessionInfo, err error) {
|
||||
var replyMsg MsgTTYSenderInitReply
|
||||
|
||||
msgInitReq := MsgTTYSenderInitRequest{
|
||||
Salt: senderInfo.Salt,
|
||||
PasswordVerifierA: senderInfo.PasswordVerifierA,
|
||||
}
|
||||
|
||||
// Send the InitRequest message
|
||||
if err = MarshalAndWriteMsg(protoConn.netConnection, msgInitReq); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Wait here for the InitReply message
|
||||
if err = ReadAndUnmarshalMsg(protoConn.netConnection, &replyMsg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
serverInfo = ServerSessionInfo{
|
||||
URLWebReadWrite: replyMsg.ReceiverURLWebReadWrite,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (protoConn *TTYProtocolConn) InitServer(serverInfo ServerSessionInfo) (senderInfo SenderSessionInfo, err error) {
|
||||
var requestMsg MsgTTYSenderInitRequest
|
||||
|
||||
// Wait here and expect a InitRequest message
|
||||
if err = ReadAndUnmarshalMsg(protoConn.netConnection, &requestMsg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Send back a InitReply message
|
||||
if err = MarshalAndWriteMsg(protoConn.netConnection, MsgTTYSenderInitReply{
|
||||
ReceiverURLWebReadWrite: serverInfo.URLWebReadWrite}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
senderInfo = SenderSessionInfo{
|
||||
Salt: requestMsg.Salt,
|
||||
PasswordVerifierA: requestMsg.PasswordVerifierA,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (protoConn *TTYProtocolConn) InitServerReceiverConn(serverInfo ServerSessionInfo) (receiverInfo ReceiverSessionInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (protoConn *TTYProtocolConn) InitReceiverServerConn(receiverInfo ReceiverSessionInfo) (serverInfo ServerSessionInfo, err error) {
|
||||
return
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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)
|
||||
}
|
Loading…
Reference in New Issue