Skip to content

Instantly share code, notes, and snippets.

@AndrienkoAleksandr
Created August 11, 2016 17:53
Show Gist options
  • Save AndrienkoAleksandr/d425959f9ec657c66074d6a69f1224ab to your computer and use it in GitHub Desktop.
Save AndrienkoAleksandr/d425959f9ec657c66074d6a69f1224ab to your computer and use it in GitHub Desktop.
package main
/*
* websocket/pty proxy server:
* This program wires a websocket to a pty master.
*
* Usage:
* go build -o ws-pty-proxy server.go
* ./websocket-terminal -cmd /bin/bash -addr :9000 -static $HOME/src/websocket-terminal
* ./websocket-terminal -cmd /bin/bash -- -i
*
* TODO:
* * make more things configurable
* * switch back to binary encoding after fixing term.js (see index.html)
* * make errors return proper codes to the web client
*
* Copyright 2014 Al Tobey [email protected]
* MIT License, see the LICENSE file
*/
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"flag"
"github.com/eclipse/che-lib/pty"
"github.com/eclipse/che-lib/websocket"
"io"
"log"
"net"
"net/http"
"os"
"os/exec"
"unicode/utf8"
"fmt"
"syscall"
)
var addrFlag, cmdFlag, staticFlag string
type WebSocketMessage struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1,
WriteBufferSize: 1,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type wsPty struct {
Cmd *exec.Cmd // pty builds on os.exec
PtyFile *os.File // a pty is simply an os.File
}
func StartPty() (*wsPty, error) {
// TODO consider whether these args are needed at all
cmd := exec.Command(cmdFlag, flag.Args()...)
cmd.Env = append(os.Environ(), "TERM=xterm")
f, err := pty.Start(cmd)
if err != nil {
return nil, err
}
//Set the size of the pty
pty.Setsize(f, 60, 200)
return &wsPty{
PtyFile: f,
Cmd: cmd,
}, nil
}
func (wp *wsPty) Stop() {
wp.PtyFile.Close();
syscall.Kill(wp.Cmd.Process.Pid, syscall.SIGBUS)
//syscall.Kill(wp.Cmd.Process.Pid, syscall.SIGCHLD)
wp.Cmd.Wait()
}
func isNormalWsError(err error) bool {
closeErr, ok := err.(*websocket.CloseError)
if ok && (closeErr.Code == websocket.CloseGoingAway || closeErr.Code == websocket.CloseNormalClosure) {
return true
}
_, ok = err.(*net.OpError)
return ok
}
func isNormalPtyError(err error) bool {
if err == io.EOF {
return true
}
pathErr, ok := err.(*os.PathError)
return ok &&
pathErr.Op == "read" &&
pathErr.Path == "/dev/ptmx" &&
pathErr.Err.Error() == "input/output error"
}
// read from the web socket, copying to the pty master
// messages are expected to be text and base64 encoded
func sendConnectionInputToPty(f *os.File, conn *websocket.Conn, done chan bool) {
defer func() {
fmt.Println("complete sendConnectionInputToPty")
done <- true;
}()
for {
mt, payload, err := conn.ReadMessage()
if err != nil {
if !isNormalWsError(err) {
log.Printf("conn.ReadMessage failed: %s, %T\n", err, err)
}
return
}
var msg WebSocketMessage
switch mt {
case websocket.BinaryMessage:
log.Printf("Ignoring binary message: %q\n", payload)
case websocket.TextMessage:
if err := json.Unmarshal(payload, &msg); err != nil {
log.Printf("Invalid message %s\n", err)
continue
}
if errMsg := handleMessage(msg, f); errMsg != nil {
log.Printf(errMsg.Error())
return
}
default:
log.Printf("Invalid websocket message type %d\n", mt)
return
}
}
}
func handleMessage(msg WebSocketMessage, ptyFile *os.File) error {
switch msg.Type {
case "resize":
var size []float64
if err := json.Unmarshal(msg.Data, &size); err != nil {
log.Printf("Invalid resize message: %s\n", err)
} else {
pty.Setsize(ptyFile, uint16(size[1]), uint16(size[0]))
}
case "data":
var dat string
if err := json.Unmarshal(msg.Data, &dat); err != nil {
log.Printf("Invalid data message %s\n", err)
} else {
ptyFile.Write([]byte(dat))
}
default:
return errors.New("Invalid field message type: " + msg.Type + "\n")
}
return nil
}
//read byte array as Unicode code points (rune in go)
func normalizeBuffer(normalizedBuf *bytes.Buffer, buf []byte, n int) (int, error) {
bufferBytes := normalizedBuf.Bytes()
runeReader := bufio.NewReader(bytes.NewReader(append(bufferBytes[:], buf[:n]...)))
normalizedBuf.Reset()
i := 0
for i < n {
char, charLen, err := runeReader.ReadRune()
if err != nil {
return i, err
}
if char == utf8.RuneError {
runeReader.UnreadRune()
return i, nil
}
i += charLen
if _, err := normalizedBuf.WriteRune(char); err != nil {
return i, err
}
}
return i, nil
}
// copy everything from the pty master to the websocket
// using base64 encoding for now due to limitations in term.js
func sendPtyOutputToConnection(f *os.File, conn *websocket.Conn, done chan bool) {
defer func() {
fmt.Println("complete sendPtyOutputToConnection")
done <- true
}()
buf := make([]byte, 8192)
reader := bufio.NewReader(f)
var buffer bytes.Buffer
// TODO: more graceful exit on socket close / process exit
for {
n, err := reader.Read(buf)
if err != nil {
if !isNormalPtyError(err) {
log.Printf("Failed to read from pty: %s", err)
}
return
}
i, err := normalizeBuffer(&buffer, buf, n)
if err != nil {
log.Printf("Cound't normalize byte buffer to UTF-8 sequence, due to an error: %s", err.Error())
return
}
if err = conn.WriteMessage(websocket.TextMessage, buffer.Bytes()); err != nil {
log.Printf("Failed to send websocket message: %s, due to occurred error %s", string(buffer.Bytes()), err.Error())
return
}
buffer.Reset()
if i < n {
buffer.Write(buf[i:n])
}
}
}
func ptyHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatalf("Websocket upgrade failed: %s\n", err)
}
defer conn.Close()
wp, err := StartPty()
if err != nil {
log.Printf("Failed to start command: %s\n", err)
http.Error(w, err.Error(), 500)
return
}
done := make(chan bool)
go sendPtyOutputToConnection(wp.PtyFile, conn, done)
go sendConnectionInputToPty(wp.PtyFile, conn, done)
// Block until any routine finishes its work
<-done
// Close the pty file and kill the process after
// any of the routines finished its work, which enforces another
// go routine to complete and exit
wp.Stop()
}
func init() {
cwd, _ := os.Getwd()
flag.StringVar(&addrFlag, "addr", ":9000", "IP:PORT or :PORT address to listen on")
flag.StringVar(&cmdFlag, "cmd", "/bin/bash", "command to execute on slave side of the pty")
flag.StringVar(&staticFlag, "static", cwd, "path to static content")
// TODO: make sure paths exist and have correct permissions
}
func main() {
flag.Parse()
http.HandleFunc("/pty", ptyHandler)
// serve html & javascript
http.Handle("/", http.FileServer(http.Dir(staticFlag)))
err := http.ListenAndServe(addrFlag, nil)
if err != nil {
log.Fatalf("net.http could not listen on address '%s': %s\n", addrFlag, err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment