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
Vasile Popescu 4 years ago committed by Elis Popescu
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,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,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…
Cancel
Save