api/shell.go
func remoteShellHandler(w http.ResponseWriter, r *http.Request) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Fprintf(w, "unable to upgrade ws connection: %v", err)
		return
	}
	var httpErr *errors.HTTP
	defer func() {
		if httpErr != nil {
			var msg string
			switch httpErr.Code {
			case http.StatusUnauthorized:
				msg = "no token provided or session expired, please login again\n"
			default:
				msg = httpErr.Message + "\n"
			}
			ws.WriteMessage(websocket.TextMessage, []byte("Error: "+msg))
		}
		ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
		ws.Close()
	}()
	token := context.GetAuthToken(r)
	if token == nil {
		httpErr = &errors.HTTP{
			Code:    http.StatusUnauthorized,
			Message: "no token provided",
		}
		return
	}
	appName := r.URL.Query().Get(":appname")
	a, err := getAppFromContext(appName, r)
	if err != nil {
		if herr, ok := err.(*errors.HTTP); ok {
			httpErr = herr
		} else {
			httpErr = &errors.HTTP{
				Code:    http.StatusInternalServerError,
				Message: err.Error(),
			}
		}
		return
	}
	allowed := permission.Check(token, permission.PermAppRunShell, contextsForApp(&a)...)
	if !allowed {
		httpErr = permission.ErrUnauthorized
		return
	}
	buf := &optionalWriterCloser{}
	var term *terminal.Terminal
	unitID := r.URL.Query().Get("unit")
	isolated, _ := strconv.ParseBool(r.URL.Query().Get("isolated"))
	width, _ := strconv.Atoi(r.URL.Query().Get("width"))
	height, _ := strconv.Atoi(r.URL.Query().Get("height"))
	clientTerm := r.URL.Query().Get("term")
	evt, err := event.New(&event.Opts{
		Target:      appTarget(appName),
		Kind:        permission.PermAppRunShell,
		Owner:       token,
		CustomData:  event.FormToCustomData(InputFields(r)),
		Allowed:     event.Allowed(permission.PermAppReadEvents, contextsForApp(&a)...),
		DisableLock: true,
	})
	if err != nil {
		httpErr = &errors.HTTP{
			Code:    http.StatusInternalServerError,
			Message: err.Error(),
		}
		return
	}
	defer func() {
		var finalErr error
		if httpErr != nil {
			finalErr = httpErr
		}
		for term != nil {
			buf.disableWrite = true
			var line string
			line, err = term.ReadLine()
			if err != nil {
				break
			}
			fmt.Fprintf(evt, "> %s\n", line)
		}
		evt.Done(finalErr)
	}()
	term = terminal.NewTerminal(buf, "")
	ws.SetReadDeadline(time.Now().Add(pongWait))
	ws.SetPongHandler(func(string) error {
		ws.SetReadDeadline(time.Now().Add(pongWait))
		return nil
	})
	quit := make(chan struct{})
	defer close(quit)
	go func() {
		for {
			select {
			case <-quit:
				return
			case <-time.After(pingInterval):
			}
			ws.WriteControl(websocket.PingMessage, nil, time.Now().Add(2*time.Second))
		}
	}()
	conn := &cmdLogger{base: &wsReadWriteCloser{ws}, term: term}
	opts := provision.ExecOptions{
		Stdout: conn,
		Stderr: conn,
		Stdin:  conn,
		Width:  width,
		Height: height,
		Units:  unitsForShell(a, unitID, isolated),
		Term:   clientTerm,
	}
	err = a.Shell(opts)
	if err != nil {
		httpErr = &errors.HTTP{
			Code:    http.StatusInternalServerError,
			Message: err.Error(),
		}
	}
}
func remoteShellHandler(w http.ResponseWriter, r *http.Request) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Fprintf(w, "unable to upgrade ws connection: %v", err)
		return
	}
	var httpErr *errors.HTTP
	defer func() {
		if httpErr != nil {
			var msg string
			switch httpErr.Code {
			case http.StatusUnauthorized:
				msg = "no token provided or session expired, please login again\n"
			default:
				msg = httpErr.Message + "\n"
			}
			ws.WriteMessage(websocket.TextMessage, []byte("Error: "+msg))
		}
		ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
		ws.Close()
	}()
	token := context.GetAuthToken(r)
	if token == nil {
		httpErr = &errors.HTTP{
			Code:    http.StatusUnauthorized,
			Message: "no token provided",
		}
		return
	}
	appName := r.URL.Query().Get(":appname")
	a, err := getAppFromContext(appName, r)
	if err != nil {
		if herr, ok := err.(*errors.HTTP); ok {
			httpErr = herr
		} else {
			httpErr = &errors.HTTP{
				Code:    http.StatusInternalServerError,
				Message: err.Error(),
			}
		}
		return
	}
	allowed := permission.Check(token, permission.PermAppRunShell, contextsForApp(&a)...)
	if !allowed {
		httpErr = permission.ErrUnauthorized
		return
	}
	buf := &optionalWriterCloser{}
	var term *terminal.Terminal
	unitID := r.URL.Query().Get("unit")
	isolated, _ := strconv.ParseBool(r.URL.Query().Get("isolated"))
	width, _ := strconv.Atoi(r.URL.Query().Get("width"))
	height, _ := strconv.Atoi(r.URL.Query().Get("height"))
	clientTerm := r.URL.Query().Get("term")
	evt, err := event.New(&event.Opts{
		Target:      appTarget(appName),
		Kind:        permission.PermAppRunShell,
		Owner:       token,
		CustomData:  event.FormToCustomData(InputFields(r)),
		Allowed:     event.Allowed(permission.PermAppReadEvents, contextsForApp(&a)...),
		DisableLock: true,
	})
	if err != nil {
		httpErr = &errors.HTTP{
			Code:    http.StatusInternalServerError,
			Message: err.Error(),
		}
		return
	}
	defer func() {
		var finalErr error
		if httpErr != nil {
			finalErr = httpErr
		}
		for term != nil {
			buf.disableWrite = true
			var line string
			line, err = term.ReadLine()
			if err != nil {
				break
			}
			fmt.Fprintf(evt, "> %s\n", line)
		}
		evt.Done(finalErr)
	}()
	term = terminal.NewTerminal(buf, "")
	ws.SetReadDeadline(time.Now().Add(pongWait))
	ws.SetPongHandler(func(string) error {
		ws.SetReadDeadline(time.Now().Add(pongWait))
		return nil
	})
	quit := make(chan struct{})
	defer close(quit)
	go func() {
		for {
			select {
			case <-quit:
				return
			case <-time.After(pingInterval):
			}
			ws.WriteControl(websocket.PingMessage, nil, time.Now().Add(2*time.Second))
		}
	}()
	conn := &cmdLogger{base: &wsReadWriteCloser{ws}, term: term}
	opts := provision.ExecOptions{
		Stdout: conn,
		Stderr: conn,
		Stdin:  conn,
		Width:  width,
		Height: height,
		Units:  unitsForShell(a, unitID, isolated),
		Term:   clientTerm,
	}
	err = a.Shell(opts)
	if err != nil {
		httpErr = &errors.HTTP{
			Code:    http.StatusInternalServerError,
			Message: err.Error(),
		}
	}
}
// title: app shell
// path: /apps/{name}/shell
// method: GET
// produce: Websocket connection upgrade
// responses:
//   101: Switch Protocol to websocket