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