/*
 *  Phusion Passenger - https://www.phusionpassenger.com/
 *  Copyright (c) 2018 Phusion Holding B.V.
 *
 *  "Passenger", "Phusion Passenger" and "Union Station" are registered
 *  trademarks of Phusion Holding B.V.
 *
 *  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.
 */

// A helper app that reads from an arbitrary file.
// Its main reason for existance is to allow root processes (such as the Core)
// to read from arbitrary files in a way that's safe from symlink and other
// kinds of attacks. See the documentation for safeReadFile()
// to learn more about the different types of attacks.
//
// file-read-helper is used when the caller cannot use safeReadFile(),
// e.g. when the following two conditions hold at the same time:
//
//  1. The caller does not have control over the safety of the parent
//     directories leading to the file.
//  2. The caller cannot choose not to disclose the contents of the file.
//
// file-read-helper MUST be used in combination with exec-helper in order
// to lower its privilege, otherwise no protection is provided.

#include <cstdio>
#include <cstdlib>
#include <cstddef>
#include <cerrno>
#include <cstring>
#include <string>
#include <algorithm>
#include <limits>

#include <fcntl.h>
#include <unistd.h>

#include <Constants.h>
#include <MainFunctions.h>
#include <ProcessManagement/Utils.h>
#include <IOTools/IOUtils.h>
#include <Utils/OptionParsing.h>

namespace Passenger {
namespace FileReadHelper {

using namespace std;


struct Options {
	size_t limit;
	int programArgStart;

	Options()
		: limit(std::numeric_limits<size_t>::max()),
		  programArgStart(2)
		{ }
};

static void
usage() {
	// ....|---------------Keep output within standard terminal width (80 chars)------------|
	printf("Usage: " AGENT_EXE " file-read-helper [OPTIONS...] <PATH>\n");
	printf("Reads the given file with O_NONBLOCK.\n");
	printf("\n");
	printf("Options:\n");
	printf("  --limit <SIZE>  Limit the number of bytes read (default: unlimited).\n");
	printf("  --help          Show this help message.\n");
}

static bool
parseOption(int argc, const char *argv[], int &i, Options &options) {
	OptionParser p(usage);

	if (p.isValueFlag(argc, i, argv[i], '\0', "--limit")) {
		options.limit = atoi(argv[i + 1]);
		i += 2;
	} else {
		return false;
	}
	return true;
}

static bool
parseOptions(int argc, const char *argv[], Options &options) {
	OptionParser p(usage);
	int i = 2;

	while (i < argc) {
		if (parseOption(argc, argv, i, options)) {
			continue;
		} else if (p.isFlag(argv[i], 'h', "--help")) {
			usage();
			exit(0);
		} else if (*argv[i] == '-') {
			fprintf(stderr, "ERROR: unrecognized argument %s. Please type "
				"'%s file-read-helper --help' for usage.\n", argv[i], argv[0]);
			exit(1);
		} else {
			options.programArgStart = i;
			return true;
		}
	}

	return true;
}

} // namespace FileReadHelper
} // namespace Passenger

int
fileReadHelperMain(int argc, char *argv[]) {
	using namespace Passenger;
	using namespace Passenger::FileReadHelper;

	if (argc < 3) {
		usage();
		exit(1);
	}

	Options options;
	if (!parseOptions(argc, (const char **) argv, options)) {
		fprintf(stderr, "Error parsing arguments.\n");
		usage();
		exit(1);
	}

	if (argc != options.programArgStart + 1) {
		fprintf(stderr, "ERROR: no file path given. Please type "
			"'%s file-read-helper --help' for usage.\n", argv[0]);
		exit(1);
	}

	if (geteuid() == 0) {
		fprintf(stderr, "ERROR: file-read-helper cannot be run with root"
			" privileges. Please use in combination with exec-helper.\n");
		exit(1);
	}

	resetSignalHandlersAndMask();
	disableMallocDebugging();

	const char *path = argv[options.programArgStart];
	int fd;
	do {
		fd = open(path, O_RDONLY | O_NONBLOCK);
	} while (fd == -1 && errno == EINTR);
	if (fd == -1) {
		int e = errno;
		fprintf(stderr, "Error opening %s for reading: %s (errno=%d)\n",
			path, strerror(e), e);
		exit(1);
	}

	size_t totalRead = 0;
	char buf[1024 * 16];

	while (totalRead < options.limit) {
		ssize_t ret;
		do {
			ret = read(fd, buf,
				std::min<size_t>(sizeof(buf), options.limit - totalRead));
		} while (ret == -1 && errno == EINTR);
		if (ret == -1) {
			int e = errno;
			fprintf(stderr, "Error reading from %s: %s (errno=%d)\n",
				path, strerror(e), e);
			exit(1);
		} else if (ret == 0) {
			break;
		} else {
			totalRead += ret;
			writeExact(1, StaticString(buf, ret));
		}
	}

	return 0;
}
