183 lines
4.3 KiB
Go
183 lines
4.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"email-mcp/internal/secretstore"
|
|
)
|
|
|
|
type SetupPrompter interface {
|
|
PromptSetup(ctx context.Context) (secretstore.Credential, error)
|
|
}
|
|
|
|
type PasswordReader interface {
|
|
ReadPassword(prompt string) (string, error)
|
|
}
|
|
|
|
type InteractiveSetupPrompter struct {
|
|
input *bufio.Reader
|
|
rawInput io.Reader
|
|
output io.Writer
|
|
passwordReader PasswordReader
|
|
}
|
|
|
|
type terminalPasswordReader struct {
|
|
input io.Reader
|
|
output io.Writer
|
|
isTerminal func(io.Reader) bool
|
|
readHiddenPassword func(*os.File) ([]byte, error)
|
|
readFallbackPassword func(string) (string, error)
|
|
}
|
|
|
|
func NewInteractiveSetupPrompter(input io.Reader, output io.Writer) *InteractiveSetupPrompter {
|
|
return NewInteractiveSetupPrompterWithPasswordReader(input, output, nil)
|
|
}
|
|
|
|
func NewInteractiveSetupPrompterWithPasswordReader(input io.Reader, output io.Writer, passwordReader PasswordReader) *InteractiveSetupPrompter {
|
|
if input == nil {
|
|
input = strings.NewReader("")
|
|
}
|
|
if output == nil {
|
|
output = io.Discard
|
|
}
|
|
|
|
prompter := &InteractiveSetupPrompter{
|
|
input: bufio.NewReader(input),
|
|
rawInput: input,
|
|
output: output,
|
|
}
|
|
if passwordReader == nil {
|
|
passwordReader = newTerminalPasswordReader(input, output, prompter.prompt)
|
|
}
|
|
prompter.passwordReader = passwordReader
|
|
|
|
return prompter
|
|
}
|
|
|
|
func (p *InteractiveSetupPrompter) PromptSetup(context.Context) (secretstore.Credential, error) {
|
|
host, err := p.prompt("IMAP host: ")
|
|
if err != nil {
|
|
return secretstore.Credential{}, err
|
|
}
|
|
username, err := p.prompt("Username: ")
|
|
if err != nil {
|
|
return secretstore.Credential{}, err
|
|
}
|
|
password, err := p.passwordReader.ReadPassword("Password: ")
|
|
if err != nil {
|
|
return secretstore.Credential{}, err
|
|
}
|
|
|
|
cred := secretstore.Credential{
|
|
Host: host,
|
|
Username: username,
|
|
Password: password,
|
|
}
|
|
if err := cred.Validate(); err != nil {
|
|
return secretstore.Credential{}, err
|
|
}
|
|
return cred, nil
|
|
}
|
|
|
|
func (p *InteractiveSetupPrompter) prompt(label string) (string, error) {
|
|
if _, err := fmt.Fprint(p.output, label); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
value, err := p.input.ReadString('\n')
|
|
if err != nil && err != io.EOF {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(value), nil
|
|
}
|
|
|
|
func newTerminalPasswordReader(input io.Reader, output io.Writer, fallback func(string) (string, error)) PasswordReader {
|
|
return terminalPasswordReader{
|
|
input: input,
|
|
output: output,
|
|
isTerminal: isTerminalReader,
|
|
readHiddenPassword: readHiddenPassword,
|
|
readFallbackPassword: fallback,
|
|
}
|
|
}
|
|
|
|
func (r terminalPasswordReader) ReadPassword(prompt string) (string, error) {
|
|
if r.isTerminal != nil && r.isTerminal(r.input) {
|
|
file, ok := r.input.(*os.File)
|
|
if ok {
|
|
if _, err := fmt.Fprint(r.output, prompt); err != nil {
|
|
return "", err
|
|
}
|
|
password, err := r.readHiddenPassword(file)
|
|
if _, printErr := fmt.Fprintln(r.output); err == nil && printErr != nil {
|
|
return "", printErr
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(password)), nil
|
|
}
|
|
}
|
|
|
|
return r.readFallbackPassword(prompt)
|
|
}
|
|
|
|
func isTerminalReader(reader io.Reader) bool {
|
|
file, ok := reader.(*os.File)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return isTerminalFile(file)
|
|
}
|
|
|
|
func isTerminalFile(file *os.File) bool {
|
|
_, err := getTermios(file.Fd())
|
|
return err == nil
|
|
}
|
|
|
|
func readHiddenPassword(file *os.File) ([]byte, error) {
|
|
state, err := getTermios(file.Fd())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
next := *state
|
|
next.Lflag &^= syscall.ECHO
|
|
if err := setTermios(file.Fd(), &next); err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = setTermios(file.Fd(), state)
|
|
}()
|
|
|
|
reader := bufio.NewReader(file)
|
|
value, err := reader.ReadBytes('\n')
|
|
if err != nil && err != io.EOF {
|
|
return nil, err
|
|
}
|
|
return []byte(strings.TrimRight(string(value), "\r\n")), nil
|
|
}
|
|
|
|
func getTermios(fd uintptr) (*syscall.Termios, error) {
|
|
state := &syscall.Termios{}
|
|
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(state)))
|
|
if errno != 0 {
|
|
return nil, errno
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func setTermios(fd uintptr, state *syscall.Termios) error {
|
|
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(state)))
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
return nil
|
|
}
|