gotty/app/app.go

233 lines
4.8 KiB
Go
Raw Normal View History

2015-08-16 09:47:23 +00:00
package app
import (
2015-08-19 11:35:04 +00:00
"crypto/rand"
"encoding/base64"
2015-08-16 09:47:23 +00:00
"encoding/json"
"log"
2015-08-19 11:35:04 +00:00
"math/big"
2015-08-16 09:47:23 +00:00
"net/http"
"os/exec"
2015-08-19 11:35:04 +00:00
"strconv"
2015-08-16 09:47:23 +00:00
"strings"
"syscall"
"unsafe"
"github.com/elazarl/go-bindata-assetfs"
"github.com/gorilla/websocket"
"github.com/kr/pty"
"github.com/yudai/utf8reader"
2015-08-16 09:47:23 +00:00
)
type App struct {
options Options
}
type Options struct {
2015-08-16 09:47:23 +00:00
Address string
Port string
PermitWrite bool
Credential string
RandomUrl bool
2015-08-16 09:47:23 +00:00
Command []string
}
func New(options Options) *App {
2015-08-16 09:47:23 +00:00
return &App{
options: options,
2015-08-16 09:47:23 +00:00
}
}
func loggerHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
h.ServeHTTP(w, r)
})
}
func basicAuthHandler(h http.Handler, cred string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(token) != 2 || strings.ToLower(token[0]) != "basic" {
w.Header().Set("WWW-Authenticate", `Basic realm="GoTTY"`)
http.Error(w, "Bad Request", http.StatusUnauthorized)
return
}
payload, err := base64.StdEncoding.DecodeString(token[1])
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if cred != string(payload) {
w.Header().Set("WWW-Authenticate", `Basic realm="GoTTY"`)
http.Error(w, "authorization failed", http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
2015-08-16 09:47:23 +00:00
func (app *App) Run() error {
2015-08-19 11:35:04 +00:00
path := "/"
if app.options.RandomUrl {
2015-08-19 11:35:04 +00:00
randomPath := generateRandomString(8)
path = "/" + randomPath + "/"
}
fs := http.StripPrefix(path, http.FileServer(&assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "bindata"}))
http.Handle(path, fs)
http.HandleFunc(path+"ws", app.generateHandler())
endpoint := app.options.Address + ":" + app.options.Port
log.Printf("Server is running at %s, command: %s", endpoint+path, strings.Join(app.options.Command, " "))
handler := http.Handler(http.DefaultServeMux)
handler = loggerHandler(handler)
if app.options.Credential != "" {
handler = basicAuthHandler(handler, app.options.Credential)
}
2015-08-19 11:35:04 +00:00
err := http.ListenAndServe(endpoint, handler)
2015-08-16 09:47:23 +00:00
if err != nil {
return err
}
return nil
}
func (app *App) generateHandler() func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("New client connected: %s", r.RemoteAddr)
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
2015-08-16 09:47:23 +00:00
Subprotocols: []string{"gotty"},
}
if r.Method != "GET" {
http.Error(w, "Method not allowed", 405)
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("Failed to upgrade connection")
return
}
cmd := exec.Command(app.options.Command[0], app.options.Command[1:]...)
2015-08-16 09:47:23 +00:00
fio, err := pty.Start(cmd)
2015-08-21 03:33:44 +00:00
log.Printf("Command is running for client %s with PID %d", r.RemoteAddr, cmd.Process.Pid)
2015-08-16 09:47:23 +00:00
if err != nil {
log.Print("Failed to execute command")
return
}
exit := make(chan bool, 2)
go func() {
defer func() { exit <- true }()
buf := make([]byte, 1024)
utf8f := utf8reader.New(fio)
for {
size, err := utf8f.Read(buf)
2015-08-16 09:47:23 +00:00
if err != nil {
2015-08-21 03:33:44 +00:00
log.Printf("Command exited for: %s", r.RemoteAddr)
2015-08-16 09:47:23 +00:00
return
}
writer, err := conn.NextWriter(websocket.TextMessage)
2015-08-16 09:47:23 +00:00
if err != nil {
return
}
writer.Write(buf[:size])
2015-08-16 09:47:23 +00:00
writer.Close()
}
}()
go func() {
defer func() { exit <- true }()
for {
_, data, err := conn.ReadMessage()
if err != nil {
return
}
switch data[0] {
case Input:
if !app.options.PermitWrite {
2015-08-16 09:47:23 +00:00
break
}
_, err := fio.Write(data[1:])
if err != nil {
return
}
case ResizeTerminal:
var args argResizeTerminal
err = json.Unmarshal(data[1:], &args)
2015-08-16 09:47:23 +00:00
if err != nil {
log.Print("Malformed remote command")
return
}
window := struct {
row uint16
col uint16
x uint16
y uint16
}{
uint16(args.Rows),
uint16(args.Columns),
0,
0,
2015-08-16 09:47:23 +00:00
}
syscall.Syscall(
syscall.SYS_IOCTL,
fio.Fd(),
syscall.TIOCSWINSZ,
uintptr(unsafe.Pointer(&window)),
)
2015-08-16 09:47:23 +00:00
default:
log.Print("Unknown message type")
return
}
}
}()
go func() {
<-exit
cmd.Wait()
2015-08-16 09:47:23 +00:00
conn.Close()
log.Printf("Connection closed: %s", r.RemoteAddr)
}()
}
}
const (
Input = '0'
ResizeTerminal = '1'
)
type argResizeTerminal struct {
Columns float64
Rows float64
2015-08-16 09:47:23 +00:00
}
2015-08-19 11:35:04 +00:00
func generateRandomString(length int) string {
const base = 36
size := big.NewInt(base)
n := make([]byte, length)
for i, _ := range n {
c, _ := rand.Int(rand.Reader, size)
n[i] = strconv.FormatInt(c.Int64(), base)[0]
}
return string(n)
}