view cmd.c @ 11:85bce13237cf

Add filter expressions to list command Refactor field parsing so it can be used for parsing fields, field assignments and filter expressions.
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Mon, 31 Jul 2017 09:20:21 +0200
parents 17fb30016e64
children 8768fbd09bc5
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 <limits.h>
#ifdef	HAVE_READPASSPHRASE_H
#include <readpassphrase.h>
#endif /* READPASSPHRASE_H */
#include <regex.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "cmd.h"
#include "pwfile.h"
#include "util.h"

#define	TIME_FORMAT	"%Y-%m-%dT%TZ"
#define	TIME_SIZE	(4 + 1 + 2 + 1 + 2 + 1 + 8 + 1 + 1)

enum field_type {
	FIELD_UNKNOWN = -1,
	FIELD_GROUP,
	FIELD_TITLE,
	FIELD_USERNAME,
	FIELD_PASSWORD,
	FIELD_NOTES,
	FIELD_URL,
	FIELD_MTIME,
	FIELD_CTIME
};

static enum cmd_return	cmd_info(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_list(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_create(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_modify(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_remove(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_show(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_pipe(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_creategroup(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_removegroup(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_changepassword(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_help(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_write(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_quit(struct pwm_ctx *, int, char *[]);

static const char *field_names[] = {
    "group",
    "title",
    "username",
    "password",
    "notes",
    "url",
    "ctime",
    "mtime"
};

static const char *field_labels[] = {
    "Group:    ",
    "Title:    ",
    "Username: ",
    "Password: ",
    "Notes:    ",
    "URL:      ",
    "Created:  ",
    "Modified: "
};

static struct cmd cmds[] = {
    { "i", "info", "info", "Show metadata information about the current file",
    cmd_info },
    { "ls", "list", "list [field~regex ...]", "List entries", cmd_list },
    { "c", "create", "create field=value ...", "Create entry", cmd_create },
    { "m", "modify", "modify id field=value ...", "Modify entry", cmd_modify },
    { "rm", "remove", "remove id", "Delete entry", cmd_remove },
    { "s", "show", "show id field", "Show entry", cmd_show },
    { "p", "pipe", "pipe id field command", "Pipe entry to external command",
    cmd_pipe },
    { "cg", "creategroup", "creategroup name", "Create empty group",
    cmd_creategroup },
    { "rg", "removegroup", "removegroup name", "Delete empty group",
    cmd_removegroup },
    { "ch", "changepassword", "changepassword", "Change password",
    cmd_changepassword },
    { "h", "help", "help", "Show help text", cmd_help },
    { "w", "write", "write", "Write the database", cmd_write },
    { "q", "quit", "quit", "Quit", cmd_quit },
    { 0 }
};

static enum field_type
parse_field(char *field_arg, int sep, char **valuep)
{
	int	i;
	size_t	field_name_len;

	for (i = 0; i < (int)COUNTOF(field_names); i++) {
		field_name_len = strlen(field_names[i]);
		if ((strncmp(field_names[i], field_arg, field_name_len) == 0) &&
		    (field_arg[field_name_len] == sep)) {
			if (valuep != NULL) {
				*valuep = field_arg + field_name_len + 1;
			}
			return (i);
		}
	}

	return (FIELD_UNKNOWN);
}

static int
parse_id(const char *arg, unsigned int *idp)
{
	long	x;
	char	*p;

	errno = 0;
	x = strtol(arg, &p, 10);
	if ((errno != 0) || (*arg == '\0') || (*p != '\0') || (x > UINT_MAX) ||
	    (x <= 0)) {
		return (-1);
	}
	*idp = (unsigned int)x;

	return (0);
}

static enum cmd_return
cmd_info(struct pwm_ctx *ctx, int argc, char *argv[])
{
	struct metadata	*metadata;
	struct tm	*tm;
	char		timebuf[TIME_SIZE];

	if (argc != 1) {
		return (CMD_USAGE);
	}

	metadata = pwfile_get_metadata(ctx);
	printf("Format:      0x%04x\n", metadata->version);
	if (metadata->user != NULL) {
		printf("User:        %s\n", metadata->user);
	}
	if (metadata->user != NULL) {
		printf("Host:        %s\n", metadata->host);
	}
	if (metadata->user != NULL) {
		printf("Application: %s\n", metadata->application);
	}
	tm = gmtime(&metadata->timestamp);
	strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
	printf("Last Saved:  %s\n", timebuf);

	pwfile_destroy_metadata(metadata);

	return (CMD_OK);
}

static enum cmd_return
cmd_list(struct pwm_ctx *ctx, int argc, char *argv[])
{
	int		retval = CMD_ERR;
	int		i;
	regex_t		*group_re = NULL;
	regex_t		*title_re = NULL;
	regex_t		*username_re = NULL;
	regex_t		*notes_re = NULL;
	regex_t		*url_re = NULL;
	enum field_type	type;
	char		*value;
	regex_t		**repp;
	int		errcode;
	char		*errbuf;
	size_t		errbuf_size;
	union list_item	**list = NULL;
	size_t		j;
	struct record	*record;

	for (i = 1; i < argc; i++) {
		type = parse_field(argv[i], '~', &value);
		if (type == FIELD_UNKNOWN) {
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			goto out;
		}
		if (value[0] == '\0') {
			/* skip empty expressions */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			repp = &group_re;
			break;
		case FIELD_TITLE:
			repp = &title_re;
			break;
		case FIELD_USERNAME:
			repp = &username_re;
			break;
		case FIELD_NOTES:
			repp = &notes_re;
			break;
		case FIELD_URL:
			repp = &url_re;
			break;
		default:
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			goto out;
		}

		if (*repp == NULL) {
			*repp = xmalloc(sizeof (regex_t));
		} else {
			regfree(*repp);
		}
		errcode = regcomp(*repp, value, REG_EXTENDED | REG_NOSUB);
		if (errcode != 0) {
			errbuf_size = regerror(errcode, *repp, "", 0);
			errbuf = xmalloc(errbuf_size);
			regerror(errcode, *repp, errbuf, errbuf_size);
			fprintf(stderr, "bad regular expression \"%s\"\n",
			    errbuf);
			free(errbuf);

			free(*repp);
			*repp = NULL;

			goto out;
		}
	}

	list = pwfile_create_list(ctx);
	for (j = 0; list[j] != NULL; j++) {
		if (list[j]->any.type == ITEM_TYPE_GROUP) {
			printf("[%s]\n", list[j]->group.group);
		} else {
			record = pwfile_get_record(ctx, list[j]->record.id);
			if (((group_re == NULL) || (regexec(group_re,
			    record->group, 0, NULL, 0) == 0)) &&
			    ((title_re == NULL) || (regexec(title_re,
			    record->title, 0, NULL, 0) == 0)) &&
			    ((username_re == NULL) || (regexec(username_re,
			    record->username, 0, NULL, 0) == 0)) &&
			    ((notes_re == NULL) || (regexec(notes_re,
			    record->notes, 0, NULL, 0) == 0)) &&
			    ((url_re == NULL) || (regexec(url_re,
			    record->url, 0, NULL, 0) == 0))) {
				printf("%4u %s\n", list[j]->record.id,
				    (list[j]->record.title != NULL) ?
				    list[j]->record.title : "");
			}
			pwfile_destroy_record(record);
		}
	}
	retval = CMD_OK;

out:
	if (group_re != NULL) {
		regfree(group_re);
		free(group_re);
	}
	if (title_re != NULL) {
		regfree(title_re);
		free(title_re);
	}
	if (username_re != NULL) {
		regfree(username_re);
		free(username_re);
	}
	if (notes_re != NULL) {
		regfree(notes_re);
		free(notes_re);
	}
	if (url_re != NULL) {
		regfree(url_re);
		free(url_re);
	}

	pwfile_destroy_list(list);

	return (retval);
}

static enum cmd_return
cmd_create(struct pwm_ctx *ctx, int argc, char *argv[])
{
	int		i;
	struct record record = { 0 };
	enum field_type	type;
	char		*value;

	if (argc < 2) {
		return (CMD_USAGE);
	}

	for (i = 1; i < argc; i++) {
		type = parse_field(argv[i], '=', &value);
		if (type == FIELD_UNKNOWN) {
			fprintf(stderr, "bad field assignment \"%s\"\n",
			    argv[i]);
		}
		if (value[0] == '\0') {
			/* skip empty assignments */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			record.group = value;
			break;
		case FIELD_TITLE:
			record.title = value;
			break;
		case FIELD_USERNAME:
			record.username = value;
			break;
		case FIELD_PASSWORD:
			record.password = value;
			break;
		case FIELD_NOTES:
			record.notes = value;
			break;
		case FIELD_URL:
			record.url = value;
			break;
		default:
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			return (CMD_ERR);
		}
	}

	pwfile_create_record(ctx, &record);

	return (CMD_OK);
}

static enum cmd_return
cmd_modify(struct pwm_ctx *ctx, int argc, char *argv[])
{
	unsigned int	id;
	int		i;
	struct record	record = { 0 };
	enum field_type	type;
	char		*value;

	if (argc < 2) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		fprintf(stderr, "invalid id %s\n", argv[1]);
		return (CMD_ERR);
	}

	for (i = 2; i < argc; i++) {
		type = parse_field(argv[i], '=', &value);
		if (type == FIELD_UNKNOWN) {
			fprintf(stderr, "bad field assignment \"%s\"\n",
			    argv[i]);
			return (CMD_ERR);
		}
		if (value[0] == '\0') {
			/* skip empty assignments */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			record.group = value;
			break;
		case FIELD_TITLE:
			record.title = value;
			break;
		case FIELD_USERNAME:
			record.username = value;
			break;
		case FIELD_PASSWORD:
			record.password = value;
			break;
		case FIELD_NOTES:
			record.notes = value;
			break;
		case FIELD_URL:
			record.url = value;
			break;
		default:
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			return (CMD_ERR);
		}
	}

	pwfile_modify_record(ctx, id, &record);

	return (CMD_OK);
}

static enum cmd_return
cmd_remove(struct pwm_ctx *ctx, int argc, char *argv[])
{
	unsigned int	id;

	if (argc != 2) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		fprintf(stderr, "invalid id %s\n", argv[1]);
		return (CMD_ERR);
	}

	if (pwfile_remove_record(ctx, id) != 0) {
		fprintf(stderr, "failed to remove record %u\n", id);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static int
print_field(const char *label, const char *value, int show_label, FILE *fp)
{
	fprintf(fp, "%s%s\n", show_label ? label : "", (value != NULL) ?
	    value : "");
	if (ferror(fp)) {
		warn("fprintf");
		return (-1);
	}
	return (0);
}

static void
print_record(struct record *record, int fields[], int show_labels, FILE *fp)
{
	struct tm	*tm;
	char		timebuf[TIME_SIZE];

	if (fields[FIELD_TITLE]) {
		if (print_field(field_labels[FIELD_TITLE], record->title,
		    show_labels, fp) != 0) {
			return;
		}
	}
	if (fields[FIELD_GROUP]) {
		if (print_field(field_labels[FIELD_GROUP], record->group,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_USERNAME]) {
		if (print_field(field_labels[FIELD_USERNAME], record->username,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_PASSWORD]) {
		if (print_field(field_labels[FIELD_PASSWORD], record->password,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_NOTES]) {
		if (print_field(field_labels[FIELD_NOTES], record->notes,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_URL]) {
		if (print_field(field_labels[FIELD_URL], record->url,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_CTIME]) {
		tm = gmtime(&record->ctime);
		strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
		if (print_field(field_labels[FIELD_CTIME], timebuf,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_MTIME]) {
		tm = gmtime(&record->mtime);
		strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
		if (print_field(field_labels[FIELD_MTIME], timebuf,
		    show_labels, fp)) {
			return;
		}
	}
}

static enum cmd_return
cmd_show(struct pwm_ctx *ctx, int argc, char *argv[])
{
	unsigned int	id;
	struct record	*record;
	int		i;
	enum field_type	type;
	int		fields[COUNTOF(field_names)] = { 0 };

	if (argc < 2) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		fprintf(stderr, "invalid id %s\n", argv[1]);
		return (CMD_ERR);
	}

	for (i = 2; i < argc; i++) {
		type = parse_field(argv[i], '\0', NULL);
		if (type < 0) {
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			return (CMD_ERR);
		}
		fields[type] = 1;
	}

	record = pwfile_get_record(ctx, id);
	if (record == NULL) {
		fprintf(stderr, "record %u does not exist\n", id);
		return (CMD_ERR);
	}
	print_record(record, fields, 1, stdout);
	pwfile_destroy_record(record);

	return (CMD_OK);
}

static enum cmd_return
cmd_pipe(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval = CMD_ERR;
	unsigned int	id;
	struct record	*record = NULL;
	enum field_type	type;
	int		fields[COUNTOF(field_names)] = { 0 };
	FILE		*fp = NULL;

	if (argc != 4) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		fprintf(stderr, "invalid id %s\n", argv[1]);
		return (CMD_ERR);
	}

	type = parse_field(argv[2], '\0', NULL);
	if (type < 0) {
		fprintf(stderr, "bad field name \"%s\"\n", argv[2]);
		return (CMD_ERR);
	}
	fields[type] = 1;

	fp = popen(argv[3], "w");
	if (fp == NULL) {
		warn("popen");
		goto out;
	}

	record = pwfile_get_record(ctx, id);
	if (record == NULL) {
		fprintf(stderr, "record %u does not exist\n", id);
		goto out;
	}

	print_record(record, fields, 0, fp);

	retval = CMD_OK;

out:
	pwfile_destroy_record(record);
	if (fp != NULL) {
		pclose(fp);
	}

	return (retval);
}

static enum cmd_return
cmd_creategroup(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 2) {
		return (CMD_USAGE);
	}

	if (pwfile_create_group(ctx, argv[1]) != 0) {
		fprintf(stderr, "group \"%s\" already exists\n", argv[1]);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_removegroup(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 2) {
		return (CMD_USAGE);
	}

	if (pwfile_remove_group(ctx, argv[1]) != 0) {
		fprintf(stderr, "there is no empty group \"%s\"\n", argv[1]);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_changepassword(struct pwm_ctx *ctx, int argc, char *argv[])
{
	size_t	password_len;
	char	password_buf[PWS3_MAX_PASSWORD_LEN + 1];
	char	confirm_buf[PWS3_MAX_PASSWORD_LEN + 1];

	if (argc > 2) {
		return (CMD_USAGE);
	} else if (argc == 2) {
		password_len = strlen(argv[1]);
		if (password_len == 0) {
			fprintf(stderr, "password must not be empty\n");
			return (CMD_ERR);
		} else if (password_len + 1 > sizeof (ctx->password)) {
			fprintf(stderr, "password too long\n");
			return (CMD_ERR);
		}
		memcpy(ctx->password, argv[1], password_len + 1);
	} else {
		if (readpassphrase("Enter password: ", password_buf,
		    sizeof (password_buf), RPP_ECHO_OFF | RPP_REQUIRE_TTY) ==
		    NULL) {
			err(1, "readpassphrase");
		}
		password_len = strlen(password_buf);
		if (password_len == 0) {
			fprintf(stderr, "password must not be empty\n");
			return (CMD_ERR);
		}
		if (readpassphrase("Confirm password: ", confirm_buf,
		    sizeof (confirm_buf),
		    RPP_ECHO_OFF | RPP_REQUIRE_TTY) == NULL) {
			err(1, "readpassphrase");
		}
		if (strcmp(password_buf, confirm_buf) != 0) {
			fprintf(stderr, "passwords do not match\n");
			return (CMD_ERR);
		}
		memcpy(ctx->password, password_buf, password_len + 1);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_help(struct pwm_ctx *ctx, int argc, char *argv[])
{
	struct cmd	*cmd;

	if (argc != 1) {
		return (CMD_USAGE);
	}

	printf("Commands:\n");
	for (cmd = cmds; cmd->cmd_func != NULL; cmd++) {
		printf("%-2s %-16s %s\n", cmd->abbrev_cmd, cmd->full_cmd,
		    cmd->description);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_write(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 1) {
		return (CMD_USAGE);
	}

	return ((pwfile_write_file(ctx) == 0) ? CMD_OK : CMD_ERR);
}

static enum cmd_return
cmd_quit(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 1) {
		return (CMD_USAGE);
	}

	return (CMD_QUIT);
}

struct cmd *
cmd_match(const char *cmd_name)
{
	size_t	i;

	for (i = 0; cmds[i].cmd_func != NULL; i++) {
		if ((strcmp(cmds[i].abbrev_cmd, cmd_name) == 0) ||
		    (strcmp(cmds[i].full_cmd, cmd_name) == 0)) {
			return (&cmds[i]);
		}
	}

	return (NULL);
}