view pwm.c @ 32:b5ebed168e59

Only invoke pager in the help command for the summary of all commands Display usage information for a single command directly and prefix it with "usage:" like usage error messages.
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Tue, 28 Nov 2017 17:16:24 +0100
parents 2552eec9b913
children 2a8298bafec2
line wrap: on
line source

/*
 * Copyright (C) 2017 Guido Berhoerster <guido+pwm@berhoerster.name>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "compat.h"

#ifdef	HAVE_ERR_H
#include <err.h>
#endif /* HAVE_ERR_H */
#include <errno.h>
#include <fcntl.h>
#include <langinfo.h>
#include <locale.h>
#include <pwd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pwd.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>

#include "pwm.h"
#include "cmd.h"
#include "io.h"
#include "macro.h"
#include "pwfile.h"
#include "pwmrc.h"
#include "tok.h"
#include "util.h"

static void
usage(void)
{
	fprintf(stderr, "usage: %s [-P file] [-R] [filename]\n", getprogname());
}

void
pwm_err(struct pwm_ctx *ctx, char *fmt, ...)
{
	va_list	args;

	free(ctx->errmsg);

	if (fmt != NULL) {
		va_start(args, fmt);
		xvasprintf(&ctx->errmsg, fmt, args);
		va_end(args);

		fprintf(stderr, "%s\n", ctx->errmsg);
	} else {
		ctx->errmsg = NULL;
	}
}

void
pwm_block_signals(void)
{
	sigset_t	set;

	sigemptyset(&set);
	sigaddset(&set, SIGINT);
	sigaddset(&set, SIGTERM);
	sigaddset(&set, SIGHUP);
	sigaddset(&set, SIGQUIT);
	if (sigprocmask(SIG_BLOCK, &set, NULL) != 0) {
		err(1, "sigprocmask");
	}
}

void
pwm_unblock_signals(void)
{
	sigset_t	set;

	sigemptyset(&set);
	sigaddset(&set, SIGINT);
	sigaddset(&set, SIGTERM);
	sigaddset(&set, SIGHUP);
	sigaddset(&set, SIGQUIT);
	if (sigprocmask(SIG_UNBLOCK, &set, NULL) != 0) {
		err(1, "sigprocmask");
	}
}

static int
run_input_loop(struct pwm_ctx *ctx)
{
	int		retval = -1;
	char		prompt[8 + 2 + 1];
	GetLine		*gl = NULL;
	char		buf[PWM_LINE_MAX + 1];
	int		io_retval;
	size_t		tokenc = 0;
	union tok	**tokenv = NULL;
	int		argc = 0;
	char		**argv = NULL;
	struct cmd	*cmd;
	int		i;

	snprintf(prompt, sizeof (prompt), "%.*s> ", 8, getprogname());

	pwm_block_signals();

	/* initialize libtecla */
	gl = new_GetLine(PWM_LINE_MAX, PWM_HISTORY_MAX);
	if (gl == NULL) {
		err(1, "new_GetLine");
	}
	gl_catch_blocked(gl);
	gl_limit_history(gl, PWM_HISTORY_LINES_MAX);
	/* disable default filename completion */
	gl_customize_completion(gl, NULL, io_gl_complete_nothing);

	for (;;) {
		/* read next line */
		cmd = NULL;
		buf[0] = '\0';
		io_retval = io_get_line(gl, prompt, 1, NULL, 0,
		    sizeof (buf), buf);
		switch (io_retval) {
		case IO_OK:
			break;
		case IO_TRUNCATED:
			/* line was truncated in non-interactive mode */
			fprintf(stderr, "line too long\n");
			goto out;
		case IO_EOF:
			if (ctx->is_interactive) {
				/* treat as "q" command */
				strcpy(buf, "q\n");
				io_retval = IO_OK;
				break;
			}
			/* FALLTHORUGH */
		case IO_SIGNAL:
			if (ctx->unsaved_changes) {
				pwfile_write_autosave_file(ctx);
			}
			goto quit;
		default:
			fprintf(stderr, "unknown error\n");
			goto quit;
		}

		/* tokenize line */
		switch (tok_tokenize(buf, &tokenc, &tokenv)) {
		case TOK_ERR_UNTERMINATED_QUOTE:
			fprintf(stderr, "unterminated quote\n");
			if (!ctx->is_interactive) {
				goto out;
			}
			goto next;
		case TOK_ERR_TRAILING_BACKSLASH:
			fprintf(stderr, "trailing backslash\n");
			if (!ctx->is_interactive) {
				goto out;
			}
			goto next;
		case TOK_ERR_INVALID_MACRO_NAME:
			fprintf(stderr, "invalid macro name\n");
			if (!ctx->is_interactive) {
				goto out;
			}
			goto next;
		case TOK_ERR_OK:
			break;
		}

		/* expand macros */
		if (macro_expand_macros(ctx->macro_head, tokenc, tokenv, &argc,
		    &argv) != 0) {
			fprintf(stderr, "undefined macro\n");
			if (!ctx->is_interactive) {
				goto out;
			}
			goto next;
		}

		if (argc == 0) {
			/* empty line */
			goto next;
		}

		/* find and execute the command */
		cmd = cmd_match(argv[0]);
		if (cmd == NULL) {
			pwm_err(ctx, "unknown command: %s", argv[0]);
			if (ctx->is_interactive) {
				goto next;
			} else {
				goto out;
			}
		}
		switch (cmd->cmd_func(ctx, argc, argv)) {
		case CMD_OK:
			pwm_err(ctx, NULL); /* clear error */
			break;
		case CMD_USAGE:
			fprintf(stderr, "usage: %s\n", cmd->usage);
		case CMD_ERR:	/* FALLTHROUGH */
			if (!ctx->is_interactive) {
				goto out;
			}
			break;
		case CMD_SIGNAL:
			fprintf(stderr, "received signal, quitting\n");
		case CMD_QUIT:	/* FALLTHROUGH */
			goto quit;
		default:
			break;
		}
		ctx->prev_cmd = cmd->full_cmd;

next:
		for (i = 0; i < argc; i++) {
			free(argv[i]);
		}
		free(argv);
		argc = 0;
		argv = NULL;
		tok_free(tokenv);
		tokenc = 0;
		tokenv = NULL;
	}

quit:
	retval = 0;

out:
	for (i = 0; i < argc; i++) {
		free(argv[i]);
	}
	free(argv);
	tok_free(tokenv);
	del_GetLine(gl);

	return (retval);
}

int
pwm_read_password(struct pwm_ctx *ctx, int is_new_password)
{
	switch (io_get_password(is_new_password ? "New Password:" :
	    "Password:", is_new_password ? "Confirm Password:" : NULL,
	    sizeof (ctx->password), ctx->password)) {
	case IO_OK:
		return (0);
	case IO_SIGNAL:
		return (-2);
	case IO_PASSWORD_EMPTY:
		pwm_err(ctx, "password must not be empty");
		return (-1);
	case IO_PASSWORD_MISMATCH:
		pwm_err(ctx, "passwords do not match");
		return (-1);
	default:
		return (-1);
	}
}

static int
read_password_from_file(const char *filename, char *password,
    size_t password_size)
{
	int	retval = -1;
	char	*buf = NULL;
	FILE	*fp = NULL;
	size_t	password_len = 0;

	buf = xmalloc(password_size);

	fp = fopen(filename, "r");
	if (fp == NULL) {
		warn("failed to open master password file \"%s\"", filename);
		goto out;
	}
	if (fgets(buf, password_size, fp) == NULL) {
		/* read error or empty file */
		if (ferror(fp)) {
			/* read error */
			warn("failed to read master password from \"%s\"",
			    filename);
		}
		goto out;
	}
	password_len = strlen(buf);
	if (buf[password_len - 1] == '\n') {
		/* strip trailing newline */
		password_len--;
		if (password_len == 0) {
			/* first line is empty */
			fprintf(stderr, "malformed password file\n");
			goto out;
		}
	} else if (!feof(fp)) {
		/* the first line was truncated, password is too long */
		fprintf(stderr, "malformed password file\n");
		goto out;
	}
	memcpy(password, buf, password_size);
	retval = 0;

out:
	password[password_len] = '\0';

	if (fp != NULL) {
		fclose(fp);
	}
	free(buf);

	return (retval);
}

int
main(int argc, char *argv[])
{
	int		status = EXIT_FAILURE;
	char		*locale;
	int		errflag = 0;
	int		c;
	const char	*master_password_filename = NULL;
	struct pwm_ctx	ctx = { 0 };
	struct passwd	*passwd;
	FILE		*fp = NULL;

	setprogname(argv[0]);

	/* set up locale and check for UTF-8 codeset */
	locale = setlocale(LC_ALL, "");
	if (locale == NULL) {
		errx(1, "invalid locale");
	}
	if ((strcasecmp(nl_langinfo(CODESET), "UTF-8") != 0) &&
	    (strcasecmp(nl_langinfo(CODESET), "UTF8") != 0)) {
		fprintf(stderr, "pwm requires a locale with UTF-8 character "
		    "encoding.\n");
		goto out;
	}

	/* timestamps are processed as UTC */
	if (setenv("TZ", "", 1) != 0) {
		goto out;
	}
	tzset();

	/* initialize libpws */
	if (pws_init() != 0) {
		goto out;
	}

	ctx.is_interactive = isatty(STDIN_FILENO);

	macro_init(&ctx.macro_head);

	while (!errflag && (c = getopt(argc, argv, "P:Rh")) != -1) {
		switch (c) {
		case 'P':
			master_password_filename = optarg;
			break;
		case 'R':
			ctx.is_readonly = 1;
			break;
		case 'h':
			usage();
			status = EXIT_SUCCESS;
			goto out;
		default:
			errflag = 1;
		}
	}
	if (errflag || (optind + 1 < argc)) {
		usage();
		status = EXIT_USAGE;
		goto out;
	}

	passwd = getpwuid(getuid());
	if (passwd == NULL) {
		err(1, "getpwuid");
	}
	xasprintf(&ctx.dirname, "%s/.pwm", passwd->pw_dir);

	/* create ~/.pwm directory if necessary */
	if ((mkdir(ctx.dirname, S_IRWXU) != 0) && (errno != EEXIST)) {
		warn("failed to create directory \"%s\"", ctx.dirname);
		goto out;
	}

	if (optind == argc) {
		xasprintf(&ctx.filename, "%s/pwm.psafe3", ctx.dirname);
	} else if (optind + 1 == argc) {
		ctx.filename = xstrdup(argv[optind++]);
	} else {
		usage();
		status = EXIT_USAGE;
		goto out;
	}

	if (ctx.is_interactive) {
		printf("pwm version %s\n", VERSION);
	} else if (master_password_filename == NULL) {
		fprintf(stderr, "master password file must be specified when "
		    "running non-interactively\n");
		goto out;
	}

	/* read ~/.pwm/pwmrc */
	if (pwmrc_read(&ctx) != 0) {
		goto out;
	}

	pwfile_init(&ctx);
	fp = fopen(ctx.filename, "r");
	if ((fp == NULL) && (errno != ENOENT)) {
		warn("failed to open password database");
		goto out;
	}
	/* obtain master password */
	if (master_password_filename != NULL) {
		if (read_password_from_file(master_password_filename,
		    ctx.password, sizeof (ctx.password)) != 0) {
			goto out;
		}
	} else if (pwm_read_password(&ctx, (fp == NULL)) != 0) {
		goto out;
	}
	if (fp != NULL) {
		if (pwfile_read_file(&ctx, fp) != 0) {
			goto out;
		}
		fclose(fp);
		fp = NULL;
	}

	/* run main input loop */
	status = (run_input_loop(&ctx) != 0);

out:
	macro_destroy(ctx.macro_head);
	pwfile_destroy(&ctx);
	if (fp != NULL) {
		fclose(fp);
	}
	free(ctx.filename);
	free(ctx.dirname);
	free(ctx.errmsg);
	free(ctx.pipecmd);

	exit(status);
}