post inicial
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
//go:build linux
|
||||
|
||||
// Package term lee contraseñas del terminal sin eco (§80).
|
||||
//
|
||||
// Se implementa con ioctl(TCGETS/TCSETS) de la biblioteca estándar en lugar
|
||||
// de golang.org/x/term para no añadir dependencias; el proyecto solo
|
||||
// soporta Linux.
|
||||
package term
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ErrNotTerminal indica que la entrada no es un terminal interactivo.
|
||||
var ErrNotTerminal = errors.New("input is not an interactive terminal")
|
||||
|
||||
func getTermios(fd uintptr) (*syscall.Termios, error) {
|
||||
var t syscall.Termios
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCGETS, uintptr(unsafe.Pointer(&t))); errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func setTermios(fd uintptr, t *syscall.Termios) error {
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCSETS, uintptr(unsafe.Pointer(t))); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTerminal informa si f es un terminal.
|
||||
func IsTerminal(f *os.File) bool {
|
||||
_, err := getTermios(f.Fd())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// DisableEcho desactiva el eco del terminal f y devuelve la función que lo
|
||||
// restaura. Lo usa el CLI, que ya tiene su propio lector de stdin: se
|
||||
// desactiva el eco y la línea se recibe por ese mismo lector, evitando dos
|
||||
// lectores compitiendo por la entrada.
|
||||
func DisableEcho(f *os.File) (restore func(), err error) {
|
||||
fd := f.Fd()
|
||||
old, err := getTermios(fd)
|
||||
if err != nil {
|
||||
return nil, ErrNotTerminal
|
||||
}
|
||||
noEcho := *old
|
||||
noEcho.Lflag &^= syscall.ECHO
|
||||
noEcho.Lflag |= syscall.ICANON | syscall.ISIG
|
||||
if err := setTermios(fd, &noEcho); err != nil {
|
||||
return nil, fmt.Errorf("failed to disable echo: %w", err)
|
||||
}
|
||||
return func() { setTermios(fd, old) }, nil
|
||||
}
|
||||
|
||||
// ReadPassword muestra prompt en out y lee una línea de in sin eco.
|
||||
//
|
||||
// Garantiza que el eco se restaura siempre, también si llega SIGINT/SIGTERM
|
||||
// mientras se escribe (en ese caso devuelve error).
|
||||
func ReadPassword(in *os.File, out io.Writer, prompt string) (string, error) {
|
||||
fd := in.Fd()
|
||||
old, err := getTermios(fd)
|
||||
if err != nil {
|
||||
return "", ErrNotTerminal
|
||||
}
|
||||
noEcho := *old
|
||||
noEcho.Lflag &^= syscall.ECHO
|
||||
noEcho.Lflag |= syscall.ICANON | syscall.ISIG
|
||||
noEcho.Iflag |= syscall.ICRNL
|
||||
if err := setTermios(fd, &noEcho); err != nil {
|
||||
return "", fmt.Errorf("failed to disable echo: %w", err)
|
||||
}
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||
restore := func() {
|
||||
setTermios(fd, old)
|
||||
signal.Stop(sig)
|
||||
}
|
||||
defer restore()
|
||||
|
||||
fmt.Fprint(out, prompt)
|
||||
type result struct {
|
||||
line string
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
line, err := bufio.NewReader(in).ReadString('\n')
|
||||
done <- result{line, err}
|
||||
}()
|
||||
select {
|
||||
case r := <-done:
|
||||
fmt.Fprintln(out)
|
||||
if r.err != nil && r.line == "" {
|
||||
return "", fmt.Errorf("failed to read secret: %w", r.err)
|
||||
}
|
||||
return strings.TrimRight(r.line, "\r\n"), nil
|
||||
case <-sig:
|
||||
fmt.Fprintln(out)
|
||||
return "", errors.New("secret input cancelled")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user