diff --git a/Makefile b/Makefile index 5de9618..ac8aad2 100644 --- a/Makefile +++ b/Makefile @@ -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..21c5069 --- /dev/null +++ b/docs/peek.1 @@ -0,0 +1,116 @@ +.\" Copyright (c) 2023-2024 DTB +.\" +.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license, +.\" visit . + +.TH PEEK 1 + +.SH NAME + +peek \(en read from standard input, furtively + +.SH SYNOPSIS + +peek +.RB ( -1enot ) +.RB ( -p +.RB [ program +.RB [ arguments... ]]) + +.SH DESCRIPTION + +Peek reads input from standard input with terminal echo disabled, which may be +useful to prevent secrets being spied upon by adversaries watching a user's +screen. + +.SH OPTIONS + +.B -1 +.RS +Limits input to a single line (stopping when the newline character is read). +.RE + +.B -e +.RS +Configures peek to output to standard error. +.RE + +.B -n +.RS +Prints ("iNcludes") the terminating character in outputs. This is nonsensical +if not combined with the +.B -1 +option. +.RE + +.B -o +.RS +Configures peek to output to standard output. +.RE + +.B -p +.RS +Configures peek to pipe output to an executed child program, e.g. a password +hashing utility. This is provided as a convenience and to avoid potential +insecurities resulting from programmer error in scripts. +.RE + +.B -t +.RS +Makes peek exit if not run within a terminal. +.RE + +.SH DIAGNOSTICS + +Peek prints an error message and exits with the appropriate status from +sysexits.h(3) if it encounters an unrecoverable error. + +Peek tries to handle the signal SIGINT (^C) to ensure the user's terminal is +still usable after premature termination; if the signal can't be handled, it +prints an error message and continues. If peek is interrupted, it exits +unsuccessfully, without an error message. + +.SH BUGS + +Accepting secrets in shell scripts is not adviseable in any context. + +If used in a safety-critical application it must be ensured that the +environment with which peek is used is not compromised. + +On systems that support it, the ioctl(2) command TIOCSTI can be used to insert +characters into the standard input going to peek. This doesn't allow snooping +but can be used for general mischief. + +Peek will happily run without outputs, slurping input and doing nothing with +it. + +.SH EXAMPLES + +This is a command line for POSIX sh(1), using POSIX env(1) to discard the +environment except for PATH, and htpassword(1) from Apache's utilities to hash +the input with the bcrypt algorithm, printing the resulting hash: + +.RS +.R $ env -i PATH="$PATH" peek -1tp htpasswd -nBi _ | cut -d : -f 2 +.RE + +This is a POSIX sh(1) command line that lets a user blindly write into a text +file, only able to see written lines. Some writers have the habit of +prematurely revising their work and use tools like this to prevent it: + +.RS +.R $ echo Input ^D to quit. && peek -eot >writing.txt +.RE + +.SH AUTHOR + +Written by DTB . + +.SH COPYRIGHT + +Copyright (c) 2023-2024 DTB. License AGPLv3+: GNU AGPL version 3 or later +. + +.SH SEE ALSO + +env(1), ioctl(2), ioctl_tty(2), read(1), sh(1) diff --git a/src/peek.c b/src/peek.c new file mode 100644 index 0000000..27c9793 --- /dev/null +++ b/src/peek.c @@ -0,0 +1,159 @@ +/* + * 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 /* fclose(3), fdopen(3), fprintf(3), getc(3), perror(3), + * putc(3), stderr, stdin, stdout, EOF, NULL */ +#include /* exit(3), size_t, EXIT_FAILURE */ +#include /* strerror(3) */ +#if !defined EX_OK || !defined EX_OSERR || !defined EX_USAGE +# include +#endif +#include /* tcgetattr(3), tcsetattr(3), struct termios, ECHO */ +#include /* dup(2), execvp(3), fork(2), getopt(3), isatty(3), + * pipe(2), STDIN_FILENO */ + +static int oserr(char *s){ perror(s); return EX_OSERR; } + +static char *program_name = "peek"; + +static int usage(char *s){ + fprintf(stderr, "Usage: %s (-1enot) (-p [program [arguments...]])\n", + s == NULL ? program_name : s); + return EX_USAGE; +} + +/* 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. */ +void restore_echo(int sig){ + static struct termios t; + + tcgetattr(STDIN_FILENO, &t); + t.c_lflag |= ECHO; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &t); + + if(sig != 0) + exit(EXIT_FAILURE); + else + return; +} + +int main(int argc, char *argv[]){ + int eof; + char include_eof; + FILE *outputs[] = { + NULL /* stdout */, + NULL /* stderr */, + NULL /* -p */ + }; + int p[2] = {0, 0}; + + if(argc < 1) + usage(argv[0]); + + eof = EOF; + include_eof = 0; + { /* options parsing */ + int c; + + while((c = getopt(argc, argv, "1enopt")) != -1) + switch(c){ + case '1': eof = '\n'; break; + case 'n': include_eof = 1; break; + case 'o': outputs[0] = stdout; break; + case 'e': outputs[1] = stderr; break; + case 'p': + if(pipe(p) != 0 || (outputs[2] = fdopen(p[1], "ab")) == NULL) + return oserr(argv[0]); + break; + case 't': + if(isatty(STDIN_FILENO) != 1){ + fprintf(stderr, "%s: Must be run in a terminal" + " (option -t specified)\n", argv[0]); + return EX_USAGE; + } + default: return usage(argv[0]); + } + } + + /* If -p is used there must be additional arguments. getopt(3) wouldn't + * work for this because optarg would have to be one string to give to + * system(3) or an equivalent and it would be a mess of parsing and + * security issues. Any intended usage works with this slightly funkier + * argument parsing, unintended usages work as happy coincidence. */ + if((argc > optind) == (outputs[2] == 0)) + return usage(argv[0]); + + if(outputs[2] != 0) + switch(fork()){ + case 0: + if(close(p[1]) == 0 && dup2(p[0], STDIN_FILENO) == STDIN_FILENO) + execvp(argv[optind], &argv[optind]); + case -1: + return oserr(argv[0]); + default: + if(close(p[0]) != 0) + return oserr(argv[0]); + } + + { /* 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 configurations can only use 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(argv[0]); +#else + if(signal(SIGINT, restore_echo) == SIG_ERR) + perror(argv[0]); +#endif + } + + { /* banish terminal echo */ + struct termios t; + + tcgetattr(STDIN_FILENO, &t); + t.c_lflag ^= ECHO; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &t); + } + + { /* actual input loop */ + int c; + size_t i; + + do{ if((c = getc(stdin)) != eof || include_eof) + for(i = 0; i < (sizeof outputs)/(sizeof *outputs); ++i) + if(outputs[i] != NULL && putc(c, outputs[i]) == EOF){ + perror(argv[0]); + if(outputs[i] != stdout && outputs[i] != stderr) + fclose(outputs[i]); + outputs[i] = NULL; + } + }while(c != eof); + } + + restore_echo(0); + + return EX_OK; +}