Browse Source

Add PromptHub for async prompt in gui and terminal

Needed so that we can prompt for secure password upon challenge from
CMS.
Martin Hebnes Pedersen 2 years ago
parent
commit
b640f26c92
4 changed files with 155 additions and 12 deletions
  1. 2 10
      exchange.go
  2. 6 1
      main.go
  3. 127 0
      prompt_hub.go
  4. 20 1
      websocket_hub.go

+ 2 - 10
exchange.go

@@ -13,8 +13,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/howeyc/gopass"
-
 	"github.com/la5nta/wl2k-go/fbb"
 )
 
@@ -92,14 +90,8 @@ func sessionExchange(conn net.Conn, targetCall string, master bool) error {
 		if config.SecureLoginPassword != "" {
 			return config.SecureLoginPassword, nil
 		}
-
-		fmt.Print("Enter secure login password: ")
-
-		passwd, err := gopass.GetPasswdMasked()
-		if err != nil {
-			return "", err
-		}
-		return string(passwd), nil
+		resp := <-promptHub.Prompt("password", "Enter secure login password")
+		return resp.Value, resp.Err
 	})
 
 	for _, addr := range config.AuxAddrs {

+ 6 - 1
main.go

@@ -144,7 +144,9 @@ var (
 	exchangeConn net.Conn            // Pointer to the active session connection (exchange)
 	mbox         *mailbox.DirHandler // The mailbox
 	listenHub    *ListenerHub
-	appDir       string
+	promptHub    *PromptHub
+
+	appDir string
 )
 
 var fOptions struct {
@@ -182,6 +184,7 @@ func optionsSet() *pflag.FlagSet {
 
 func init() {
 	listenHub = NewListenerHub()
+	promptHub = NewPromptHub()
 
 	var err error
 	appDir, err = mailbox.DefaultAppDir()
@@ -356,6 +359,8 @@ func httpHandle(args []string) {
 		os.Exit(1)
 	}
 
+	promptHub.OmitTerminal(true)
+
 	if err := ListenAndServe(addr); err != nil {
 		log.Fatal(err)
 	}

+ 127 - 0
prompt_hub.go

@@ -0,0 +1,127 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"time"
+
+	"github.com/howeyc/gopass"
+)
+
+type Prompt struct {
+	resp     chan PromptResponse
+	cancel   chan struct{}
+	ID       string    `json:"id"`
+	Kind     string    `json:"kind"`
+	Deadline time.Time `json:"deadline"`
+	Message  string    `json:"message"`
+}
+
+type PromptResponse struct {
+	ID    string `json:"id"`
+	Value string `json:"value"`
+	Err   error  `json:"error"`
+}
+
+type PromptHub struct {
+	c  chan *Prompt
+	rc chan PromptResponse
+
+	omitTerminal bool
+}
+
+func NewPromptHub() *PromptHub { p := new(PromptHub); go p.loop(); return p }
+
+func (p *PromptHub) OmitTerminal(t bool) { p.omitTerminal = t }
+
+func (p *PromptHub) loop() {
+	p.c = make(chan *Prompt, 0)
+	p.rc = make(chan PromptResponse, 0)
+	for prompt := range p.c {
+		timeout := time.After(prompt.Deadline.Sub(time.Now()))
+		select {
+		case <-timeout:
+			prompt.resp <- PromptResponse{ID: prompt.ID, Err: fmt.Errorf("Deadline reached")}
+			close(prompt.cancel)
+		case resp := <-p.rc:
+			if resp.ID != prompt.ID {
+				continue
+			}
+			select {
+			case prompt.resp <- resp:
+			default:
+			}
+			close(prompt.cancel)
+		}
+	}
+}
+
+func (p *PromptHub) Respond(id, value string, err error) {
+	select {
+	case p.rc <- PromptResponse{ID: id, Value: value, Err: err}:
+	default:
+	}
+}
+
+func (p *PromptHub) Prompt(kind, message string) <-chan PromptResponse {
+	prompt := &Prompt{
+		resp:     make(chan PromptResponse),
+		cancel:   make(chan struct{}), // Closed on cancel (e.g. prompt response received)
+		ID:       fmt.Sprint(time.Now().UnixNano()),
+		Kind:     kind,
+		Message:  message,
+		Deadline: time.Now().Add(time.Minute),
+	}
+	p.c <- prompt
+
+	websocketHub.Prompt(*prompt)
+	if !p.omitTerminal {
+		go p.promptTerminal(*prompt)
+	}
+
+	return prompt.resp
+}
+
+type ReadAborter struct {
+	*os.File
+	abort chan struct{}
+}
+
+func (r ReadAborter) Read(p []byte) (int, error) {
+	tick := time.Tick(100 * time.Millisecond)
+	for {
+		select {
+		case <-r.abort:
+			return 0, io.EOF
+		case <-tick:
+			stat, err := r.Stat()
+			if err != nil {
+				panic(err)
+			}
+			if stat.Size() > 0 {
+				return r.File.Read(p)
+			}
+		}
+	}
+}
+
+func (p *PromptHub) promptTerminal(prompt Prompt) {
+	switch prompt.Kind {
+	case "password":
+		q := make(chan struct{}, 1)
+		go func() {
+			select {
+			case <-prompt.cancel:
+				fmt.Printf(" Prompt Aborted - Press ENTER to continue...")
+			case <-q:
+				return
+			}
+		}()
+		passwd, err := gopass.GetPasswdPrompt(prompt.Message+": ", true, os.Stdin, os.Stdout)
+		q <- struct{}{}
+		p.Respond(prompt.ID, string(passwd), err)
+	default:
+		panic(prompt.Kind + " prompt not implemented")
+	}
+}

+ 20 - 1
websocket_hub.go

@@ -6,6 +6,7 @@ package main
 
 import (
 	"bufio"
+	"encoding/json"
 	"io"
 	"log"
 	"os"
@@ -41,6 +42,11 @@ func (w *WSHub) UpdateStatus()                    { w.WriteJSON(struct{ Status S
 func (w *WSHub) WriteProgress(p Progress)         { w.WriteJSON(struct{ Progress Progress }{p}) }
 func (w *WSHub) WriteNotification(n Notification) { w.WriteJSON(struct{ Notification Notification }{n}) }
 
+func (w *WSHub) Prompt(p Prompt) {
+	w.WriteJSON(struct{ Prompt Prompt }{p})
+	go func() { <-p.cancel; w.WriteJSON(struct{ PromptAbort Prompt }{p}) }()
+}
+
 func (w *WSHub) WriteJSON(v interface{}) {
 	if w == nil {
 		return
@@ -192,14 +198,27 @@ func tailFile(path string) (<-chan []byte, chan<- struct{}, error) {
 	return (<-chan []byte)(lines), (chan<- struct{})(done), nil
 }
 
+func handleWSMessage(v map[string]json.RawMessage) {
+	raw, ok := v["prompt_response"]
+	if !ok {
+		return
+	}
+	var resp PromptResponse
+	json.Unmarshal(raw, &resp)
+	promptHub.Respond(resp.ID, resp.Value, resp.Err)
+}
+
 func wsReadLoop(c *websocket.Conn) <-chan struct{} {
 	quit := make(chan struct{})
 	go func() {
 		for {
-			if _, _, err := c.NextReader(); err != nil {
+			v := map[string]json.RawMessage{}
+			err := c.ReadJSON(&v)
+			if err != nil {
 				close(quit)
 				return
 			}
+			go handleWSMessage(v)
 		}
 	}()
 	return quit