mirror of https://github.com/elisescu/tty-share
Initial commit
commit
9f0a1474f2
@ -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…
Reference in New Issue