diff --git a/Makefile b/Makefile index 5de9618..b86db1e 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ BIN = build/bin default: all test .PHONY: all -all: dj false fop hru intcmp mm npc rpn scrut str strcmp swab true +all: docs dj false fop hru intcmp mm npc peek rpn scrut str strcmp swab true # keep build/include until bindgen(1) has stdin support # https://github.com/rust-lang/rust-bindgen/issues/2703 @@ -137,6 +137,11 @@ npc: build/bin/npc build/bin/npc: src/npc.c build $(CC) $(CFLAGS) -o $@ src/npc.c +.PHONY: peek +peek: build/bin/peek +build/bin/peek: src/peek.c build + $(CC) $(CFLAGS) -o $@ src/peek.c + .PHONY: rpn rpn: build/bin/rpn build/bin/rpn: src/rpn.rs build rustlibs diff --git a/docs/peek.1 b/docs/peek.1 new file mode 100644 index 0000000..9ec336c --- /dev/null +++ b/docs/peek.1 @@ -0,0 +1,111 @@ +.\" Copyright (c) 2023-2024 DTB +.\" Copyright (c) 2024 Emma Tebibyte +.\" +.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, +.\" visit . +.\" +.TH PEEK 1 2024-08-14 "Harakit X.X.X" +.SH NAME +peek \(en read from the standard input, furtively +.\" +.SH SYNOPSIS + +peek +.RB [ -i ] +.\" +.SH DESCRIPTION + +Read input from the standard input with terminal echo disabled. +.\" +.SH OPTIONS + +.IP \fB-i\fP +Allows input to come from sources other than terminals (pipes). +.\" +.SH DIAGNOSTICS + +In the event of an error, a debug message will be printed and the program will +exit with the appropriate +.BR sysexits.h (3) +error code. + +In order to ensure the user\(cqs terminal is still usable after premature +termination, the program attempts to handle the SIGINT signal; if it cannot, +an error message is printed and execution continues. If the program is +interrupted, it exits unsuccessfully without an error message. +.\" +.SH RATIONALE + +This tool was originally written to accept passwords in shell scripts as an +extremely simple alternative to the GNU Privacy Guard project\(cqs +.BR pinentry (1) +utility. + +Accepting input without showing what is being typed is useful when keying in +secrets in public settings or in places where surveillance cameras are +installed. +.\" +.SH CAVEATS + +This program does nothing to prevent others from seeing the key presses input to +a keyboard. It also does not protect against the sound of typing being analyzed +to determine what was input without needing to see screen or keyboard. + +Accepting secrets in shell scripts is probably not advisable. + +On systems that support it, the +.BR ioctl (2) +command TIOCSTI can be used to insert characters into the standard input. This +doesn't allow snooping but can be used for general mischief. +.\" +.SH EXAMPLES + +This is an +.BR sh (1p) +command line that hashes a given password. It uses +.BR head (1p) +to only accept one line of input, +.BR xargs (1p) +and +.BR printf (1p) +to strip the trailing newline, +.BR htpasswd (1) +from Apache\(cqs utilities to hash the input with the bcrypt algorithm, and +.BR cut (1p) +to print only the resulting hash: + +.RS +$ peek | head -n 1 | xargs printf '%s' | htpasswd -nBi _ | cut -d : -f 2 +.RE + +This is an +.BR sh (1p) +command line that allows a user to write blindly into a text file but displaying +only written lines. Some writers have the habit of prematurely revising their +work and use tools with functionality similar to this to prevent it. +It uses +.BR mm (1) +to pipe the output of the program to both the standard error and the regular +file writing.txt: + +.RS +$ echo Input ^D to quit. && peek | mm -eo - >writing.txt +.RE +.\" +.SH AUTHOR + +Written by DTB +.MT trinity@trinity.moe +.ME . +.\" +.SH COPYRIGHT + +Copyright \(co 2023-2024 DTB. License AGPLv3+: GNU AGPL version 3 or later +. +.\" +.SH SEE ALSO +.BR ioctl (2), +.BR ioctl_tty (2), +.BR read (1p), +.BR sh (1p), +.BR stty (1p) diff --git a/src/peek.c b/src/peek.c new file mode 100644 index 0000000..fb77189 --- /dev/null +++ b/src/peek.c @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023–2024 DTB + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +#include /* sigaction(2), signal(2), struct sigaction, SIGINT */ +#include /* bool */ +#include /* fprintf(3), fgetc(3), perror(3), fputc(3), stderr, stdin, + * stdout, EOF, NULL */ +#include /* exit(3), EXIT_FAILURE */ +#include /* EX_IOERR, EX_OK, EX_USAGE */ +#include /* tcgetattr(3), tcsetattr(3), struct termios, ECHO */ +#include /* getopt(3), isatty(3), pledge(2), unveil(2), + * STDIN_FILENO */ + +char *program_name = "peek"; + +/* Restores terminal echo; otherwise when a user ^Cs the terminal would + * continue to not display typed text. If sig isn't zero, this will terminate + * the program. */ +static void +restore_echo(int sig) { + static struct termios t; + + /* Failure isn't reported because this is the termination routine anyway; + * errors will be obvious. */ + if (tcgetattr(STDIN_FILENO, &t) == 0) { + t.c_lflag |= ECHO; + (void)tcsetattr(STDIN_FILENO, TCSAFLUSH, &t); + } + + if (sig != 0) { exit(EXIT_FAILURE); } /* Terminated by signal. */ + + return; +} + +static int +ioerr(char *argv0) { + perror(argv0); + restore_echo(0); + return EX_IOERR; +} + +static int +usage(char *argv0) { + (void)fprintf(stderr, "Usage: %s [-i]\n", argv0); + + return EX_USAGE; +} + +int main(int argc, char *argv[]){ + bool is_term; /* Is stdin a terminal? */ + bool must_be_term = 1; /* Must it be? */ + +#ifdef __OpenBSD__ + if (pledge("stdio tty unveil", "") != 0 || unveil(NULL, NULL) != 0) { + /* This isn't fatal; these return values could be cast to void just as + * easily. */ + (void)perror(argv[0] == NULL ? argv[0] : program_name); + } +#endif + + is_term = isatty(STDIN_FILENO); + + if (argc > 0) { /* option parsing */ + int c; + + program_name = argv[0]; + + while ((c = getopt(argc, argv, "i")) != -1) { + switch (c) { + case 'i': must_be_term = 0; break; + default: return usage(argv[0]); + } + } + + if (argc > optind) { return usage(argv[0]); } + } + + if (!is_term && must_be_term) { + (void)fprintf( + stderr, + "%s: Must be run in a terminal (specify -i to skip this check)\n", + argv[0] + ); + return EX_USAGE; + } + + if (is_term) { + { /* Install signal handler */ + /* There isn't a difference in functionality between the signal(2) + * and sigaction(2) methods. sigaction(2) is vastly preferred for + * portability but some older systems only have signal(2). */ + /* Errors aren't terminating because the worst that happens is some + * terminal phooeyness if things go awry. */ +#if defined _POSIX_C_SOURCE + struct sigaction act = { 0 }; + + act.sa_handler = restore_echo; + if (sigaction(SIGINT, &act, NULL) != 0) { perror(program_name); } +#else + if (signal(SIGINT, restore_echo) == SIG_ERR) { + perror(program_name); + } +#endif + } + + { /* Banish terminal echo */ + /* This terminates when it fails because it's the whole point of + * the program. */ + struct termios t; + + if (tcgetattr(STDIN_FILENO, &t) != 0) { + return ioerr(program_name); + } + t.c_lflag ^= ECHO; + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t) != 0) { + return ioerr(program_name); + } + } + } + + { /* Input loop */ + int c; + + while ((c = fgetc(stdin)) != EOF) { + if (fputc(c, stdout) == EOF) { return ioerr(program_name); } + } + } + + if (is_term) { restore_echo(0); } + + return EX_OK; +} diff --git a/tests/bonsai/peek.mk b/tests/bonsai/peek.mk new file mode 100755 index 0000000..20e2e24 --- /dev/null +++ b/tests/bonsai/peek.mk @@ -0,0 +1,24 @@ +# Copyright (c) 2024 DTB +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +# Testing peek is hard as it requires visual confirmation that text isn't being +# echoed. These tests don't go that far but are a start, and have already +# caught a bug in -i behavior. + +.PHONY: peek_tests +peek_tests: peek_help peek_stdio + +.PHONY: peek_help +peek_help: $(BIN)/peek + ! $(BIN)/peek -h + +.PHONY: peek_stdio +# Test peek -i +peek_stdio: $(BIN)/peek + printf 'Test.\n' \ + | $(BIN)/peek -i \ + | xargs test 'Test.' =