mirror of https://github.com/elisescu/tty-share
Separate the tty-share tool from the server side
parent
30fa0c825c
commit
a27cd48730
@ -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/
|
@ -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
|
@ -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 +0,0 @@
|
||||
Hi there
|
@ -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;
|
@ -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…
Reference in New Issue