/* 
 * cado: execute a command in a capability ambient
 * Copyright (C) 2016  Renzo Davoli and Davide Berardi, University of Bologna
 * 
 * This file is part of cado.
 *
 * Cado is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; If not, see <http://www.gnu.org/licenses/>. 
 *
 */

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <capset_from_namelist.h>
#include <compute_digest.h>

/* this structure stores the output of the "parsing" of a scado line.
	 a line has (at most) three colon ":" separated fields.
	 all field and end pointers are addresses within the string "line".
	 the i-th field begins at field[i] and terminates just before end[i] */
	 
struct scado_line {
	char *line;
	char *field[3];
	char *end[3];
};

/* parse a scado line */
static struct scado_line scado_parse(char *line) {
	struct scado_line out={line, {NULL, NULL, NULL}, {NULL, NULL, NULL}};
	char *scan=line;
	int i;
	for (i=0; i<3; i++) {
		while (isspace(*scan)) scan++;
		out.field[i] = scan;
		if (*scan == '#' || *scan == 0) goto end;
		while (*scan != ':') {
			if (scan[0] == '\\' && scan[1] != 0) scan++;
			if (*scan == '#' || *scan == 0) goto end;
			scan++;
		}
		out.end[i] = scan;
		scan++;
	}
	return out;
end:
	out.end[i] = scan;
	return out;
}

/* true if the current line matches path, false otherwise.
	 scado_path_match manages the escape chars '\' in the scado line
	 without any copy */
static int scado_path_match(const char *path, struct scado_line *line) {
	char *lpath;
	for (lpath = line->field[0]; *path && *lpath; path++, lpath++) {
		if (lpath[0] == '\\' && lpath[1] != 0) lpath++;
		if (*lpath != *path) 
			break;
	}
	return (*path == 0 && (*lpath == 0 || isspace(*lpath) || *lpath == ':'));
}

/* get the capability set from a scado file line */
static int scado_get_caps(struct scado_line *line, uint64_t *capset) {
	if (line->field[1]) {
		size_t caplen = line->end[1] - line->field[1];
		char caps[caplen+1];
		strncpy(caps,line->field[1],caplen);
		caps[caplen]=0;
		return capset_from_namelist(caps, capset);
	} else
		return -1;
}

/* scado_check_digest_syntax returns:
	 -1 if the digest field has syntax errors,
	 0 if it is an ampty field (but the field exists!)
	 1 if the line has just two fields (one ':') OR there is a well-formed hex digest */

static int scado_check_digest_syntax(struct scado_line *line) {
	if (line->field[2]) {
		int i;
		size_t digestlen = line->end[2] - line->field[2];
		char digest[digestlen+1];
		strncpy(digest,line->field[2],digestlen);
		digest[digestlen]=0;
		if (*digest == 0)
			return 0;
		if (strlen(digest) < DIGESTSTRLEN)
			return -1;
		for (i = 0; i < DIGESTSTRLEN && isxdigit(digest[i]); i++)
			;
		if (i < DIGESTSTRLEN)
			return -1;
		for (; digest[i] != 0; i++)
			if (!isspace(digest[i]))
				return -1;
		return 1;
	} else
		return 1;
}

/* true if the field exists and it is not empty */
static inline int fieldok(struct scado_line *line, int i) {
	return line->field[i] && line->end[i] != line->field[i];
}

/* some syntax sanity checks */
static int scado_syntax_check(char *path, int lineno, struct scado_line *line) {
	uint64_t capset;
	int rv=0;
	if (line->end[2] && line->end[2][0] == ':') {
		fprintf(stderr,"%s line %d: extra trailing chars\n", path, lineno);
		rv = -1;
	}
	if (fieldok(line, 0) && line->field[0][0] != '/') {
		fprintf(stderr,"%s line %d: pathname is not absolute\n", path, lineno);
		rv = -1;
	}
	if (fieldok(line, 0) && !fieldok(line, 1)) {
		fprintf(stderr,"%s line %d: missing capability set\n", path, lineno);
		rv = -1;
	}
	if (fieldok(line, 1) && scado_get_caps(line, &capset) < 0) {
		fprintf(stderr,"%s line %d: wrong capability syntax\n", path, lineno);
		rv = -1;
	}
	if (fieldok(line, 2) && scado_check_digest_syntax(line) < 0) {
		fprintf(stderr,"%s line %d: wrong digest syntax\n", path, lineno);
		rv = -1;
	}
	return rv;
}
	
/* clean the path (parsing escape chars) in place */
static void cleanpath(char *path) {
	char *out=path;
	while(*path != 0 && !isspace(*path)) {
		if (path[0] == '\\' && path[1] != 0) path++;
		*out++ = *path++;
	}
	*out=0;
}
	
/* update a line */
/* inpath == NULL -> add the digest it if is missing
	 inpath == "" -> update the digest in any case
	 inpath == "/....." (a valid path) -> update the digest if the path of this line matches */
static void scado_update_line(FILE *fout, struct scado_line *line, char *inpath) {
	if (line->field[2] != NULL) {
		/* there is the digest field */
		size_t pathlen = line->end[0] - line->field[0];
		char path[pathlen+1];
		/* create a temporary copy of the path on the stack and clean it*/
		strncpy(path,line->field[0],pathlen);
		path[pathlen]=0;
		cleanpath(path);
		int digestok=scado_check_digest_syntax(line);
		if (digestok < 0) // do not update in case of syntax error
			fprintf(fout, "%s\n", line->line);
		else if (digestok == 0) { //missing digest -> fill in!
			char digest[DIGESTSTRLEN + 1];
			int len = line->field[2] - line->line;
			if (compute_digest(path, digest) < 0)
				fprintf(fout, "%s\n", line->line);
			else if (line->end[2] != 0)
				fprintf(fout,"%*.*s%s %s\n",len,len,line->line,digest,line->end[2]);
			else 
				fprintf(fout,"%*.*s%s\n",len,len,line->line,digest);
		} else if (inpath && (*inpath == 0 || strcmp(path, inpath) == 0)) { /* if ALL or this path */
			char digest[DIGESTSTRLEN + 1];
			int len = line->field[2] - line->line;
			if (compute_digest(path, digest) < 0)
				fprintf(fout, "%s\n", line->line);
			else 
			  fprintf(fout,"%*.*s%s%s\n",len,len,line->line,digest,line->field[2]+DIGESTSTRLEN);
		} else
			fprintf(fout, "%s\n", line->line);
	} else
		fprintf(fout, "%s\n", line->line);
}

/* copy the file from inpath to outpath, updating the digest where required:
	 path == NULL -> add missing digests
	 path == "" -> all
*/
void scado_copy_update(char *inpath, char *outpath, char *path) {
	FILE *fin = NULL;
	mode_t oldmask;
	FILE *fout;
	char *line;
	size_t len=0;
	int lineno=0;
	ssize_t n;

	int fdin;
	struct stat l_stats;

	/* Symlinks check */
	if ((fdin = open(inpath, O_RDONLY | O_NOFOLLOW)) < 0) {
		return;
	}

	if (fstat(fdin, &l_stats) || l_stats.st_mode == S_IFLNK) {
		return;
	}

	fin = fdopen(fdin, "r");
	if (fin == NULL)
		return;
	oldmask = umask(077);
	fout = fopen(outpath, "w");
	umask(oldmask);
	if (fout == NULL) {
		fclose(fin);
		return;
	}
	while ((n = getline(&line, &len, fin)) > 0) {
		struct scado_line scado_line;
		line[n-1] = 0;
		scado_line = scado_parse(line);
		lineno++;
		scado_syntax_check(outpath, lineno, &scado_line);
		scado_update_line(fout, &scado_line, path);
	}
	fclose(fin);
	fclose(fout);
	free(line);
}

/* scan the scado file whose pathname is inpath seeking for the line matching with 'path'
	 scado_path_getinfo returns:
	 -1 in case of error, 0 if the line is missing, 1 if the line has been found */
/* if scado_path_getinfo succeeds, pcapset and digest are set to
	 the set of permitted capabilities and the hash digest, respectively. */
int scado_path_getinfo(char *inpath, const char *path, uint64_t *pcapset, char *digest) {
	FILE *fin = fopen(inpath, "r");
	char *line;
	size_t len=0;
	ssize_t n;
	int lineno=0;
	int rv = 0;
	if (fin == NULL)
		return -1;
	while ((n = getline(&line, &len, fin)) > 0) {
		struct scado_line scado_line;
		line[n-1] = 0;
		scado_line = scado_parse(line);
		lineno++;
		if (scado_path_match(path, &scado_line)) {
			if (scado_syntax_check(inpath, lineno, &scado_line) == 0) {
				if (scado_get_caps(&scado_line, pcapset) >= 0) {
					if (scado_line.field[2]) {
						int i;
						for (i = 0; i < DIGESTSTRLEN ; i++) 
							digest[i] = tolower(scado_line.field[2][i]);
						digest[i] = 0;
						rv = 1;
					} else {
						digest[0] = 0;
						rv = 1;
					}	
				}
			}
			break;
		}
	}
	fclose(fin);
	free(line);
	return rv;
}

#if 0
int main() {
	char *line;
	size_t len=0;
	struct scado_line s;
	int lineno=0;
	while (1) {
		ssize_t n=getline(&line, &len, stdin);
		line[n-1]=0;
		lineno++;
		s=scado_parse(line);
		printf("L %s\n",s.line);
		printf("P %s|%s\n",s.field[0],s.end[0]);
		printf("C %s|%s\n",s.field[1],s.end[1]);
		printf("H %s|%s\n",s.field[2],s.end[2]);
		uint64_t cap=0;
		printf("%d ",scado_get_caps(&s,&cap));
		printf("%lx\n",cap);
		scado_syntax_check(lineno, &s);
		printf("NULL->");fflush(stdout);
		scado_update_line(1, &s, NULL);
		printf("ALL ->");fflush(stdout);
		scado_update_line(1, &s, "");
		printf("bash ->");fflush(stdout);
		scado_update_line(1, &s, "/bin/bash");
	}
}
#endif
#if 0
int main(int argc, char *argv[]) {
	scado_update(argv[1], argv[2], argv[3]);
}
#endif
#if 0
int main(int argc, char *argv[]) {
	char digest[DIGESTSTRLEN + 1];
	uint64_t capset;
	if (scado_path_getinfo(argv[1], argv[2], &capset, digest)) {
		printf("%lx %s\n",capset,digest);
	}
}
#endif
