post inicial

This commit is contained in:
lastdragon
2026-09-27 16:04:02 -06:00
parent d84587e1f3
commit 52864794f5
67 changed files with 15023 additions and 2 deletions
+112
View File
@@ -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")
}
}