package shell import ( "context" "fmt" "io" "git.t-juice.club/torjus/oubliette/internal/storage" ) // Shell is the interface that all honeypot shell implementations must satisfy. type Shell interface { Name() string Description() string Handle(ctx context.Context, sess *SessionContext, rw io.ReadWriteCloser) error } // SessionContext carries metadata about the current SSH session. type SessionContext struct { SessionID string Username string RemoteAddr string ClientVersion string Store storage.Store ShellConfig map[string]any CommonConfig ShellCommonConfig } // ShellCommonConfig holds settings shared across all shell types. type ShellCommonConfig struct { Hostname string Banner string FakeUser string // override username in prompt; empty = use authenticated user } // ReadLine reads a line of input byte-by-byte, handling backspace, Ctrl+C, and Ctrl+D. func ReadLine(ctx context.Context, rw io.ReadWriter) (string, error) { var buf []byte b := make([]byte, 1) for { select { case <-ctx.Done(): return "", ctx.Err() default: } n, err := rw.Read(b) if err != nil { return "", err } if n == 0 { continue } ch := b[0] switch { case ch == '\r' || ch == '\n': fmt.Fprint(rw, "\r\n") return string(buf), nil case ch == 4: // Ctrl+D if len(buf) == 0 { return "", io.EOF } case ch == 3: // Ctrl+C fmt.Fprint(rw, "^C\r\n") return "", nil case ch == 127 || ch == 8: // DEL or Backspace if len(buf) > 0 { buf = buf[:len(buf)-1] fmt.Fprint(rw, "\b \b") } case ch == 27: // ESC - start of escape sequence // Read and discard the rest of the escape sequence. // Most are 3 bytes: ESC [ X (arrow keys, etc.) next := make([]byte, 1) if n, _ := rw.Read(next); n > 0 && next[0] == '[' { rw.Read(next) // read the final byte } case ch >= 32 && ch < 127: // printable ASCII buf = append(buf, ch) rw.Write([]byte{ch}) } } }