Initial commit

pull/14/merge
Vasile Popescu 7 years ago
commit 9f0a1474f2

8
.gitignore vendored

@ -0,0 +1,8 @@
**/node_modules/
.DS_Store
bundle.js
playground/
.vscode/
tty_sender
tty_server
tmp-*

@ -0,0 +1,33 @@
TTY_SERVER=tty_server
TTY_SENDER=tty_sender
TTY_SERVER_SRC=$(wildcard ./tty-server/*.go)
TTY_SENDER_SRC=$(wildcard ./tty-sender/*.go)
EXTRA_BUILD_DEPS=$(wildcard ./common/*go)
all: $(TTY_SERVER) $(TTY_SENDER)
@echo "All done"
$(TTY_SERVER): $(TTY_SERVER_SRC) $(EXTRA_BUILD_DEPS)
go build -o $@ $(TTY_SERVER_SRC)
$(TTY_SENDER): $(TTY_SENDER_SRC) $(EXTRA_BUILD_DEPS)
go build -o $@ $(TTY_SENDER_SRC)
frontend: FORCE
cd frontend && npm run build && cd -
clean:
@rm -f $(TTY_SERVER) $(TTY_SENDER)
@echo "Cleaned"
runs: $(TTY_SERVER)
@./$(TTY_SERVER) --url http://localhost:9090 --web_address :9090 --sender_address :7654
runc: $(TTY_SENDER)
@./$(TTY_SENDER) --logfile output.log
test:
@go test github.com/elisescu/tty-share/testing -v
FORCE:

@ -0,0 +1,24 @@
TTY Share
========
A small tool to allow sharing a terminal command with others via Internet.
Shortly, the user can start a command in the terminal and then others can watch that command via
Internet in the browser.
More info to come.
Run the code
===========
* Build the frontend
```
cd tty-share/frontend
npm install
npm run build
```
* Run the server
```
cd tty-share
make run
```

@ -0,0 +1,132 @@
package common
import (
"encoding/json"
"errors"
"fmt"
"io"
)
type ProtocolMessageIDType string
const (
MsgIDSenderInitRequest = "SenderInitRequest"
MsgIDSenderInitReply = "SenderInitReply"
MsgIDReceiverInitRequest = "ReceiverInitRequest"
MsgIDReceiverInitReply = "ReceiverInitReply"
MsgIDWrite = "Write"
MsgIDWinSize = "WinSize"
)
type MsgAll struct {
Type ProtocolMessageIDType
Data []byte
}
type MsgTTYSenderInitRequest struct {
Salt string
PasswordVerifierA string
}
type MsgTTYSenderInitReply struct {
ReceiverURLWebReadWrite string
}
type MsgTTYReceiverInitRequest struct {
ChallengeReply string
}
type MsgTTYReceiverInitReply struct {
}
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)
}
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,118 @@
package common
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,58 @@
## Overview of the architecture
```
B
A +-------------+
+-----------------+ | | C
| TTYSender(cmd) | <+-> | TTYProxy | +-------------------+
+-----------------+ | Server | <-+->| TTYReceiver(web) |
| | +-------------------+
| |
| | D
| | +-------------------+
| | <-+->| TTYReceiver(ssh) |
| | +-------------------+
| |
M | | N
+-----------------+ | | +-------------------+
| TTYSender(cmd) | <+-> | | <-+->| TTYREceiver(web) |
+-----------------+ +-------------+ +-------------------+
```
##
```
A <-> C, D
M <-> N
```
### A
Where A is the TTYSender, which will be used by the user Alice to share her terminal session. She will start it in the command line, with something like:
```
tty-share bash
```
If everything is successful, A will output to stdout 3 URLs, which, something like:
```
1. read-only: https://tty-share.io/s/0ENHQGjqaB
2. write: https://tty-share.io/s/4HGFN8jahg
3. terminal: ssh://0ENHQGjqaB@tty-share.io.com -p1234
4. admin: http://localhost:5456/admin
```
Url number 1. will provide read-only access to the command shared. Which means the user will not be able to interact with the terminal.
Url number 2. will allow the user to interact with the terminale.
Url number 3. ssh access, to follow the remote command from a remote terminal.
Url number 4. provides an interface to control various options related to sharing.
### B
B is the TTYProxyServer, which will be publicly accessible and to which the TTYSender will connect to. On the TTYProxyServer will be created te sessions (read-only and write), and URLs will be returned back to A. Whent the command that A started exits, the session will end, so C should know.
### C
C is the browser via which user Chris will receive the terminal which Alice has shared.
### Corner cases
Corner cases to test for:
* AB connection cannot be done
* AB is established, but CB can't be done
* AB connection can go down
* CB connection can go down:
- The websocket connection can go down
- The browser refreshed. Command is still running, so the session is still valid
* All users from the C side close their connection
* The commmand finishes

@ -0,0 +1,2 @@
# Sat Oct 28 16:42:04 CEST 2017
Got the first end-to-end communication between the tty-share command and the

@ -0,0 +1,45 @@
- process reads input
- key shortcuts . same as input?
- process writes to output?
Notes:
- background programs are suspended when they try to run to the terminal
- user input redirected to foreground program only
- UART driver, line discipline instance and TTY driver compose a TTY device
- job = process group (jobs, fg, bg)
- session, session leader (shell - talks to the kernel through signals and system calls)
```
cat &
ls | sort
```
As you can see in the diagram above, several processes have /dev/pts/0 attached to their standard input.
But only the foreground job (the ls | sort pipeline) will receive input from the TTY.
Likewise, only the foreground job will be allowed to write to the TTY device (in the default configuration).
If the cat process were to attempt to write to the TTY, the kernel would suspend it using a signal.
End-to-end encryption:
======================
* sender:
- generate salt, and the shared key from the password
- connect to the server sending the session start info:
SessionStart {
salt String
passwordVerifierA String
passwordVerifierB String
allowSSHNotEndToEnd bool
}
- gets one of the two replies:
SessionStartNotOk - should stop; or
SessionStartOK {
webTTYUrl string
sshTTYUrl string
}
* web-receiver:
- open the link: get the html page and try to open the WS connection.
- has the same salt as the sender, served in the web page
- asks the user for the password and checks the password and asks server to validate password verifier
* ssh-receiver:
- connects with ssh to some-random-string@tty-share.io

@ -0,0 +1,2 @@
7.9.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,30 @@
{
"name": "static",
"version": "1.0.0",
"description": "",
"main": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"watch": "webpack --watch --hot"
},
"author": "",
"license": "",
"dependencies": {
"babel-core": "6.26.0",
"babel-loader": "7.1.2",
"babel-preset-env": "1.6.0",
"babel-preset-react": "6.24.1",
"css-loader": "0.28.7",
"ignore-loader": "0.1.2",
"material-ui": "0.19.4",
"react": "16.2.0",
"react-bootstrap": "0.31.3",
"react-dom": "16.2.0",
"source-map-loader": "0.2.2",
"style-loader": "0.19.0",
"webpack": "3.7.1",
"webpack-dev-server": "2.9.1",
"xterm": "2.9.2"
}
}

@ -0,0 +1,24 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import MuiThemePro from 'material-ui/styles/MuiThemeProvider';
import RaisedButton from 'material-ui/RaisedButton';
import TextField from 'material-ui/TextField';
class App extends Component {
constructor(props) {
super(props);
}
render() {
return (<div> </div>);
return (
<MuiThemePro>
<TextField
hintText="Password Field"
floatingLabelText="Password"
type="password"
/>
</MuiThemePro>
);
}
}
export default App;

@ -0,0 +1,147 @@
/**
*
* 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;
}
}
export default Base64;

@ -0,0 +1,62 @@
import 'xterm/dist/xterm.css';
import Terminal from 'xterm';
import pbkdf2 from 'pbkdf2';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
import base64 from './base64'
ReactDOM.render(
<App />,
document.querySelector('#settings')
);
var term = new Terminal({
cursorBlink: true,
});
var derivedKey = pbkdf2.pbkdf2Sync('password', 'salt', 4096, 32, 'sha256');
console.log(derivedKey);
var wsAddress = 'ws://' + window.location.host + window.ttyInitialData.wsPath;
var connection = new WebSocket(wsAddress);
term.open(document.getElementById('terminal'), true);
//term.attach(connection);
term.write("$");
connection.onclose = function(evt) {
console.log("Got the WS closed !!");
term.write("disconnected");
}
connection.onmessage = function(evt) {
let message = JSON.parse(evt.data)
let msgData = base64.decode(message.Data)
if (message.Type === "Write") {
let writeMsg = JSON.parse(msgData)
term.write(base64.decode(writeMsg.Data))
}
if (message.Type == "WinSize") {
let winSizeMsg = JSON.parse(msgData)
term.resize(winSizeMsg.Cols, winSizeMsg.Rows)
}
}
term.on('data', function (data) {
//console.log('TERM->WS:', data);
let writeMessage = {
Type: "Write",
Data: base64.encode(JSON.stringify({ Size: data.length, Data: base64.encode(data)})),
}
let dataToSend = JSON.stringify(writeMessage)
//console.log("Sending : ", dataToSend)
connection.send(dataToSend);
})

@ -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/bundle.js"></script>
</body>
</html>

@ -0,0 +1,51 @@
var path = require('path');
module.exports = {
entry: './src/main.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test:/\.(js|jsx)$/,
use: [{
loader: 'babel-loader',
options: {
babelrc: false,
presets: ['env', 'react'],
},
}],
},
{
test: /\.(tsx|ts)?$/,
use: ['awesome-typescript-loader']
},
{
test: /node_modules.+xterm.+\.map$/,
use: ['ignore-loader']
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)/,
use: ['url-loader']
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: ['url-loader', 'image-webpack-loader']
},
{
test: /\.js\.map$/,
use: ['source-map-loader']
}
]
}
};

@ -0,0 +1,117 @@
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
}

@ -0,0 +1,240 @@
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")
}
}

@ -0,0 +1,37 @@
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)
}

@ -0,0 +1,59 @@
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)
}

@ -0,0 +1,102 @@
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io"
"net"
"os"
"strings"
"github.com/elisescu/tty-share/common"
logrus "github.com/sirupsen/logrus"
)
var log = logrus.New()
func main() {
commandName := flag.String("command", "bash", "The command to run")
commandArgs := flag.String("args", "", "The command arguments")
logFileName := flag.String("logfile", "-", "The name of the file to log")
server := flag.String("server", "localhost:7654", "tty-proxyserver address")
flag.Parse()
log.Level = logrus.ErrorLevel
if *logFileName != "-" {
fmt.Printf("Writing logs to: %s\n", *logFileName)
logFile, err := os.Create(*logFileName)
if err != nil {
fmt.Printf("Can't open %s for writing logs\n", *logFileName)
}
log.Level = logrus.DebugLevel
log.Out = logFile
}
// TODO: check we are running inside a tty environment, and exit if not
tcpConn, err := net.Dial("tcp", *server)
if err != nil {
fmt.Printf("Cannot connect to the server (%s): %s", *server, err.Error())
return
}
serverConnection := common.NewTTYProtocolConn(tcpConn)
reply, err := serverConnection.InitSender(common.SenderSessionInfo{
Salt: "salt",
PasswordVerifierA: "PV_A",
})
log.Infof("Web terminal: %s", reply.URLWebReadWrite)
// Display the session information to the user, before showing any output from the command.
// Wait until the user presses Enter
fmt.Printf("Web terminal: %s. Press Enter to continue. \n\r", reply.URLWebReadWrite)
bufio.NewReader(os.Stdin).ReadBytes('\n')
//TODO: if the user on the remote side presses keys, and so messages are sent back to the
// tty_sender, they will be delivered all at once, after Enter has been pressed. Fix that.
ptyMaster := ptyMasterNew()
ptyMaster.Start(*commandName, strings.Fields(*commandArgs), func(cols, rows int) {
log.Infof("New window size: %dx%d", cols, rows)
serverConnection.SetWinSize(cols, rows)
})
if cols, rows, e := ptyMaster.GetWinSize(); e == nil {
serverConnection.SetWinSize(cols, rows)
}
allWriter := io.MultiWriter(os.Stdout, serverConnection)
go func() {
_, err := io.Copy(allWriter, ptyMaster)
if err != nil {
log.Error("Lost connection with the server.\n")
ptyMaster.Stop()
}
}()
go func() {
for {
msg, err := serverConnection.ReadMessage()
if err != nil {
fmt.Printf(" -- Finishing the server connection with error: %s", err.Error())
break
}
if msg.Type == common.MsgIDWrite {
var msgWrite common.MsgTTYWrite
json.Unmarshal(msg.Data, &msgWrite)
ptyMaster.Write(msgWrite.Data[:msgWrite.Size])
}
}
}()
go func() {
io.Copy(ptyMaster, os.Stdin)
}()
ptyMaster.Wait()
}

@ -0,0 +1,121 @@
package main
import (
"os"
"os/exec"
"os/signal"
"syscall"
ptyDevice "github.com/elisescu/pty"
"golang.org/x/crypto/ssh/terminal"
)
type onWindowChangesCB func(int, int)
// This defines a PTY Master whih will encapsulate the command we want to run, and provide simple
// access to the command, to write and read IO, but also to control the window size.
type ptyMaster struct {
ptyFile *os.File
command *exec.Cmd
windowChangedCB onWindowChangesCB
terminalInitState *terminal.State
}
func ptyMasterNew() *ptyMaster {
return &ptyMaster{}
}
func (pty *ptyMaster) Start(command string, args []string, winChangedCB onWindowChangesCB) (err error) {
pty.windowChangedCB = winChangedCB
// Save the initial state of the terminal, before making it RAW. Note that this terminal is the
// terminal under which the tty_sender command has been started, and it's identified via the
// stdin file descriptor (0 in this case)
// We need to make this terminal RAW so that when the command (passed here as a string, a shell
// usually), is receiving all the input, including the special characters:
// so no SIGINT for Ctrl-C, but the RAW character data, so no line discipline.
// Read more here: https://www.linusakesson.net/programming/tty/
pty.terminalInitState, err = terminal.MakeRaw(0)
pty.command = exec.Command(command, args...)
pty.ptyFile, err = ptyDevice.Start(pty.command)
if err != nil {
return
}
// Start listening for window changes
go onWindowChanges(func(cols, rows int) {
// TODO:policy: should the server decide here if we care about the size and set it
// right here?
pty.SetWinSize(rows, cols)
// Notify the ptyMaster user of the window changes, to be sent to the remote side
pty.windowChangedCB(cols, rows)
})
// Set the initial window size
cols, rows, err := terminal.GetSize(0)
pty.SetWinSize(rows, cols)
return
}
func (pty *ptyMaster) GetWinSize() (int, int, error) {
return terminal.GetSize(0)
}
func (pty *ptyMaster) Write(b []byte) (int, error) {
return pty.ptyFile.Write(b)
}
func (pty *ptyMaster) Read(b []byte) (int, error) {
return pty.ptyFile.Read(b)
}
func (pty *ptyMaster) SetWinSize(rows, cols int) {
ptyDevice.Setsize(pty.ptyFile, rows, cols)
}
func (pty *ptyMaster) Wait() (err error) {
err = pty.command.Wait()
// The terminal has to be restored from the RAW state, to its initial state
terminal.Restore(0, pty.terminalInitState)
return
}
func (pty *ptyMaster) Stop() (err error) {
signal.Ignore(syscall.SIGWINCH)
pty.command.Process.Signal(syscall.SIGTERM)
// TODO: Find a proper wai to close the running command. Perhaps have a timeout after which,
// if the command hasn't reacted to SIGTERM, then send a SIGKILL
// (bash for example doesn't finish if only a SIGTERM has been sent)
pty.command.Process.Signal(syscall.SIGKILL)
return
}
func onWindowChanges(winChangedCb func(cols, rows int)) {
winChangedSig := make(chan os.Signal, 1)
signal.Notify(winChangedSig, syscall.SIGWINCH)
// The interface for getting window changes from the pty slave to its process, is via signals.
// In our case here, the tty_sender command (built in this project) is the client, which should
// get notified if the terminal window in which it runs has changed. To get that, it needs to
// register for SIGWINCH signal, which is used by the kernel to tell process that the window
// has changed its dimentions.
// Read more here: https://www.linusakesson.net/programming/tty/
// Shortly, ioctl calls are used to communicate from the process to the pty slave device,
// and signals are used for the communiation in the reverse direction: from the pty slave
// device to the process.
for {
select {
case <-winChangedSig:
cols, rows, err := terminal.GetSize(0)
if err == nil {
winChangedCb(cols, rows)
} else {
log.Warnf("Can't get window size: %s", err.Error())
}
}
}
}

@ -0,0 +1,231 @@
package main
import (
"errors"
"fmt"
"html/template"
"net"
"net/http"
"sync"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)
var log = MainLogger
// SessionTemplateModel used for templating
type SessionTemplateModel struct {
SessionID string
Salt string
WSPath string
}
// TTYProxyServerConfig is used to configure the proxy server before it is started
type TTYProxyServerConfig 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
}
// TTYProxyServer represents the instance of a proxy server
type TTYProxyServer struct {
httpServer *http.Server
ttySendersListener net.Listener
config TTYProxyServerConfig
activeSessions map[string]*ttyShareSession
activeSessionsRWLock sync.RWMutex
}
// NewTTYProxyServer creates a new instance
func NewTTYProxyServer(config TTYProxyServerConfig) (server *TTYProxyServer) {
server = &TTYProxyServer{
config: config,
}
server.httpServer = &http.Server{
Addr: config.WebAddress,
}
routesHandler := mux.NewRouter()
routesHandler.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("frontend"))))
routesHandler.HandleFunc("/", defaultHandler)
routesHandler.HandleFunc("/s/{sessionID}", func(w http.ResponseWriter, r *http.Request) {
sessionsHandler(server, w, r)
})
routesHandler.HandleFunc("/ws/{sessionID}", func(w http.ResponseWriter, r *http.Request) {
websocketHandler(server, w, r)
})
server.activeSessions = make(map[string]*ttyShareSession)
server.httpServer.Handler = routesHandler
return server
}
func getWSPath(sessionID string) string {
return "/ws/" + sessionID
}
func websocketHandler(server *TTYProxyServer, 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.StatusMethodNotAllowed)
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.")
// TODO: Create a proper way to communicate with the remote WS end, so that the server can send
// control messages or data messages to go directly to the terminal.
conn.WriteMessage(websocket.TextMessage, []byte("$ access denied."))
return
}
session.HandleReceiver(newWSConnection(conn))
}
func defaultHandler(http.ResponseWriter, *http.Request) {
log.Debug("Default handler ")
}
func sessionsHandler(server *TTYProxyServer, w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionID := vars["sessionID"]
log.Debug("Handling web TTYReceiver session: ", sessionID)
session := getSession(server, sessionID)
if session == nil {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
t, err := template.ParseFiles("./frontend/templates/index.html")
if err != nil {
fmt.Fprintf(w, err.Error())
return
}
templateModel := SessionTemplateModel{
SessionID: sessionID,
Salt: "salt&pepper",
WSPath: getWSPath(sessionID),
}
t.Execute(w, templateModel)
}
func addNewSession(server *TTYProxyServer, session *ttyShareSession) {
server.activeSessionsRWLock.Lock()
server.activeSessions[session.GetID()] = session
server.activeSessionsRWLock.Unlock()
}
func removeSession(server *TTYProxyServer, session *ttyShareSession) {
server.activeSessionsRWLock.Lock()
delete(server.activeSessions, session.GetID())
server.activeSessionsRWLock.Unlock()
}
func getSession(server *TTYProxyServer, 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 *TTYProxyServer, 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 *TTYProxyServer) 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()
}()
// 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 *TTYProxyServer) 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,45 @@
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")
senderAddress := flag.String("sender_address", ":6543", "The bind address for the tty_sender connections")
url := flag.String("url", "http://localhost", "The public web URL the server will be accessible at")
flag.Parse()
log := MainLogger
log.SetLevel(logrus.DebugLevel)
config := TTYProxyServerConfig{
WebAddress: *webAddress,
TTYSenderAddress: *senderAddress,
ServerURL: *url,
}
server := NewTTYProxyServer(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,184 @@
package main
import (
"container/list"
"encoding/json"
"fmt"
"net"
"sync"
"time"
. "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 {
// TODO: replace this with a proper way of generating secret session IDs
return fmt.Sprintf("%x", time.Now().UnixNano())
}
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)
// 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,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