Created
August 11, 2016 17:53
-
-
Save AndrienkoAleksandr/d425959f9ec657c66074d6a69f1224ab to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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