package main import ( "flag" "io" "log" "net/http" "os" "os/exec" "time" "github.com/creack/pty" "github.com/gorilla/websocket" ) var ( addr = flag.String("addr", ":8080", "http service address") ) const ( // Time allowed to write a message to the peer. writeWait = 10 * time.Second // Maximum message size allowed from peer. maxMessageSize = 8192 // Time allowed to read the next pong message from the peer. pongWait = 60 * time.Second // Send pings to peer with this period. Must be less than pongWait. pingPeriod = (pongWait * 9) / 10 // Time to wait before force close on connection. closeGracePeriod = 10 * time.Second ) var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, // Allow all origins for now - we'll restrict this in production CheckOrigin: func(r *http.Request) bool { return true }, } type terminal struct { cmd *exec.Cmd pty *os.File } func newTerminal() (*terminal, error) { // Start a new shell cmd := exec.Command("bash") ptmx, err := pty.Start(cmd) if err != nil { return nil, err } return &terminal{ cmd: cmd, pty: ptmx, }, nil } func (t *terminal) Close() error { if t.pty != nil { t.pty.Close() } if t.cmd != nil { t.cmd.Process.Kill() t.cmd.Wait() } return nil } func pumpStdin(ws *websocket.Conn, w io.Writer) { defer ws.Close() ws.SetReadLimit(maxMessageSize) ws.SetReadDeadline(time.Now().Add(pongWait)) ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) for { _, message, err := ws.ReadMessage() if err != nil { break } if _, err := w.Write(message); err != nil { break } } } func pumpStdout(ws *websocket.Conn, r io.Reader, done chan struct{}) { defer func() { }() buf := make([]byte, 1024) for { n, err := r.Read(buf) if err != nil { break } ws.SetWriteDeadline(time.Now().Add(writeWait)) if err := ws.WriteMessage(websocket.TextMessage, buf[:n]); err != nil { ws.Close() break } } close(done) ws.SetWriteDeadline(time.Now().Add(writeWait)) ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) time.Sleep(closeGracePeriod) ws.Close() } func ping(ws *websocket.Conn, done chan struct{}) { ticker := time.NewTicker(pingPeriod) defer ticker.Stop() for { select { case <-ticker.C: if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil { log.Println("ping:", err) } case <-done: return } } } func internalError(ws *websocket.Conn, msg string, err error) { log.Println(msg, err) ws.WriteMessage(websocket.TextMessage, []byte("Internal server error.")) } func serveWs(w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("upgrade:", err) return } defer ws.Close() // Create new terminal term, err := newTerminal() if err != nil { internalError(ws, "start:", err) return } defer term.Close() stdoutDone := make(chan struct{}) go pumpStdout(ws, term.pty, stdoutDone) go ping(ws, stdoutDone) pumpStdin(ws, term.pty) // Wait for stdout to finish <-stdoutDone } func serveHome(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.Error(w, "Not found", http.StatusNotFound) return } if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } http.ServeFile(w, r, "index.html") } func main() { flag.Parse() log.SetFlags(0) http.HandleFunc("/", serveHome) http.HandleFunc("/ws", serveWs) log.Fatal(http.ListenAndServe(*addr, nil)) }