/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/

/***
  This file is part of systemd.

  Copyright 2010 Lennart Poettering

  systemd is free software; you can redistribute it and/or modify it
  under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation; either version 2.1 of the License, or
  (at your option) any later version.

  systemd 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
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with systemd; If not, see <http://www.gnu.org/licenses/>.
***/

#include <string.h>
#include <errno.h>
#include <unistd.h>

#include "log.h"
#include "util.h"
#include "unit-name.h"
#include "mkdir.h"
#include "virt.h"
#include "strv.h"
#include "fileio.h"

static const char *arg_dest = "/tmp";
static bool arg_enabled = true;
static bool arg_read_crypttab = true;
static char **arg_proc_cmdline_disks = NULL;

static bool has_option(const char *haystack, const char *needle) {
        const char *f = haystack;
        size_t l;

        assert(needle);

        if (!haystack)
                return false;

        l = strlen(needle);

        while ((f = strstr(f, needle))) {

                if (f > haystack && f[-1] != ',') {
                        f++;
                        continue;
                }

                if (f[l] != 0 && f[l] != ',') {
                        f++;
                        continue;
                }

                return true;
        }

        return false;
}

static int create_disk(
                const char *name,
                const char *device,
                const char *password,
                const char *options) {

        char *p = NULL, *n = NULL, *d = NULL, *u = NULL, *from = NULL, *to = NULL, *e = NULL;
        int r;
        FILE *f = NULL;
        bool noauto, nofail;

        assert(name);
        assert(device);

        noauto = has_option(options, "noauto");
        nofail = has_option(options, "nofail");

        n = unit_name_from_path_instance("systemd-cryptsetup", name, ".service");
        if (!n) {
                r = log_oom();
                goto fail;
        }

        p = strjoin(arg_dest, "/", n, NULL);
        if (!p) {
                r = log_oom();
                goto fail;
        }

        u = fstab_node_to_udev_node(device);
        if (!u) {
                r = log_oom();
                goto fail;
        }

        d = unit_name_from_path(u, ".device");
        if (!d) {
                r = log_oom();
                goto fail;
        }

        f = fopen(p, "wxe");
        if (!f) {
                r = -errno;
                log_error("Failed to create unit file %s: %m", p);
                goto fail;
        }

        fprintf(f,
                "# Automatically generated by systemd-cryptsetup-generator\n\n"
                "[Unit]\n"
                "Description=Cryptography Setup for %%I\n"
                "Documentation=man:systemd-cryptsetup@.service(8) man:crypttab(5)\n"
                "SourcePath=/etc/crypttab\n"
                "Conflicts=umount.target\n"
                "DefaultDependencies=no\n"
                "BindsTo=%s dev-mapper-%%i.device\n"
                "After=systemd-readahead-collect.service systemd-readahead-replay.service %s\n"
                "Before=umount.target\n",
                d, d);

        if (!nofail)
                fprintf(f,
                        "Before=cryptsetup.target\n");

        if (password && (streq(password, "/dev/urandom") ||
                         streq(password, "/dev/random") ||
                         streq(password, "/dev/hw_random")))
                fputs("After=systemd-random-seed-load.service\n", f);
        else
                fputs("Before=local-fs.target\n", f);

        fprintf(f,
                "\n[Service]\n"
                "Type=oneshot\n"
                "RemainAfterExit=yes\n"
                "TimeoutSec=0\n" /* the binary handles timeouts anyway */
                "ExecStart=" SYSTEMD_CRYPTSETUP_PATH " attach '%s' '%s' '%s' '%s'\n"
                "ExecStop=" SYSTEMD_CRYPTSETUP_PATH " detach '%s'\n",
                name, u, strempty(password), strempty(options),
                name);

        if (has_option(options, "tmp"))
                fprintf(f,
                        "ExecStartPost=/sbin/mke2fs '/dev/mapper/%s'\n",
                        name);

        if (has_option(options, "swap"))
                fprintf(f,
                        "ExecStartPost=/sbin/mkswap '/dev/mapper/%s'\n",
                        name);

        fflush(f);

        if (ferror(f)) {
                r = -errno;
                log_error("Failed to write file %s: %m", p);
                goto fail;
        }

        if (asprintf(&from, "../%s", n) < 0) {
                r = log_oom();
                goto fail;
        }

        if (!noauto) {

                to = strjoin(arg_dest, "/", d, ".wants/", n, NULL);
                if (!to) {
                        r = log_oom();
                        goto fail;
                }

                mkdir_parents_label(to, 0755);
                if (symlink(from, to) < 0) {
                        log_error("Failed to create symlink '%s' to '%s': %m", from, to);
                        r = -errno;
                        goto fail;
                }

                free(to);

                if (!nofail)
                        to = strjoin(arg_dest, "/cryptsetup.target.requires/", n, NULL);
                else
                        to = strjoin(arg_dest, "/cryptsetup.target.wants/", n, NULL);
                if (!to) {
                        r = log_oom();
                        goto fail;
                }

                mkdir_parents_label(to, 0755);
                if (symlink(from, to) < 0) {
                        log_error("Failed to create symlink '%s' to '%s': %m", from, to);
                        r = -errno;
                        goto fail;
                }

                free(to);
                to = NULL;
        }

        e = unit_name_escape(name);
        to = strjoin(arg_dest, "/dev-mapper-", e, ".device.requires/", n, NULL);
        if (!to) {
                r = log_oom();
                goto fail;
        }

        mkdir_parents_label(to, 0755);
        if (symlink(from, to) < 0) {
                log_error("Failed to create symlink '%s' to '%s': %m", from, to);
                r = -errno;
                goto fail;
        }

        r = 0;

fail:
        free(p);
        free(n);
        free(d);
        free(e);

        free(from);
        free(to);

        if (f)
                fclose(f);

        return r;
}

static int parse_proc_cmdline(void) {
        char *line, *w, *state;
        int r;
        size_t l;

        if (detect_container(NULL) > 0)
                return 0;

        r = read_one_line_file("/proc/cmdline", &line);
        if (r < 0) {
                log_warning("Failed to read /proc/cmdline, ignoring: %s", strerror(-r));
                return 0;
        }

        FOREACH_WORD_QUOTED(w, l, line, state) {
                char *word;

                word = strndup(w, l);
                if (!word) {
                        r = log_oom();
                        goto finish;
                }

                if (startswith(word, "luks=")) {
                        r = parse_boolean(word + 5);
                        if (r < 0)
                                log_warning("Failed to parse luks switch %s. Ignoring.", word + 5);
                        else
                                arg_enabled = r;

                } else if (startswith(word, "rd.luks=")) {

                        if (in_initrd()) {
                                r = parse_boolean(word + 8);
                                if (r < 0)
                                        log_warning("Failed to parse luks switch %s. Ignoring.", word + 8);
                                else
                                        arg_enabled = r;
                        }

                } else if (startswith(word, "luks.crypttab=")) {
                        r = parse_boolean(word + 14);
                        if (r < 0)
                                log_warning("Failed to parse luks crypttab switch %s. Ignoring.", word + 14);
                        else
                                arg_read_crypttab = r;

                } else if (startswith(word, "rd.luks.crypttab=")) {

                        if (in_initrd()) {
                                r = parse_boolean(word + 17);
                                if (r < 0)
                                        log_warning("Failed to parse luks crypttab switch %s. Ignoring.", word + 17);
                                else
                                        arg_read_crypttab = r;
                        }

                } else if (startswith(word, "luks.uuid=")) {
                        char **t;

                        t = strv_append(arg_proc_cmdline_disks, word + 10);
                        if (!t) {
                                r = log_oom();
                                goto finish;
                        }
                        strv_free(arg_proc_cmdline_disks);
                        arg_proc_cmdline_disks = t;

                } else if (startswith(word, "rd.luks.uuid=")) {

                        if (in_initrd()) {
                                char **t;

                                t = strv_append(arg_proc_cmdline_disks, word + 13);
                                if (!t) {
                                        r = log_oom();
                                        goto finish;
                                }
                                strv_free(arg_proc_cmdline_disks);
                                arg_proc_cmdline_disks = t;
                        }

                } else if (startswith(word, "luks.") ||
                           (in_initrd() && startswith(word, "rd.luks."))) {

                        log_warning("Unknown kernel switch %s. Ignoring.", word);
                }

                free(word);
        }

        strv_uniq(arg_proc_cmdline_disks);

        r = 0;

finish:
        free(line);
        return r;
}

int main(int argc, char *argv[]) {
        FILE *f = NULL;
        int r = EXIT_SUCCESS;
        unsigned n = 0;
        char **i;
        char **arg_proc_cmdline_disks_done = NULL;

        if (argc > 1 && argc != 4) {
                log_error("This program takes three or no arguments.");
                return EXIT_FAILURE;
        }

        if (argc > 1)
                arg_dest = argv[1];

        log_set_target(LOG_TARGET_SAFE);
        log_parse_environment();
        log_open();

        umask(0022);

        if (parse_proc_cmdline() < 0)
                return EXIT_FAILURE;

        if (!arg_enabled) {
                r = EXIT_SUCCESS;
                goto finish;
        }

        if (arg_read_crypttab) {
                f = fopen("/etc/crypttab", "re");

                if (!f) {
                        if (errno == ENOENT)
                                r = EXIT_SUCCESS;
                        else {
                                r = EXIT_FAILURE;
                                log_error("Failed to open /etc/crypttab: %m");
                        }

                        goto finish;
                }

                for (;;) {
                        char line[LINE_MAX], *l;
                        char *name = NULL, *device = NULL, *password = NULL, *options = NULL;
                        int k;

                        if (!fgets(line, sizeof(line), f))
                                break;

                        n++;

                        l = strstrip(line);
                        if (*l == '#' || *l == 0)
                                continue;

                        k = sscanf(l, "%ms %ms %ms %ms", &name, &device, &password, &options);
                        if (k < 2 || k > 4) {
                                log_error("Failed to parse /etc/crypttab:%u, ignoring.", n);
                                r = EXIT_FAILURE;
                                goto next;
                        }

                        if (arg_proc_cmdline_disks) {
                                /*
                                  If luks UUIDs are specified on the kernel command line, use them as a filter
                                  for /etc/crypttab and only generate units for those.
                                */
                                STRV_FOREACH(i, arg_proc_cmdline_disks) {
                                        char *proc_device, *proc_name;
                                        const char *p = *i;

                                        if (startswith(p, "luks-"))
                                                p += 5;

                                        proc_name = strappend("luks-", p);
                                        proc_device = strappend("UUID=", p);

                                        if (!proc_name || !proc_device) {
                                                log_oom();
                                                r = EXIT_FAILURE;
                                                free(proc_name);
                                                free(proc_device);
                                                goto finish;
                                        }
                                        if (streq(proc_device, device) || streq(proc_name, name)) {
                                                char **t;

                                                if (create_disk(name, device, password, options) < 0)
                                                        r = EXIT_FAILURE;

                                                t = strv_append(arg_proc_cmdline_disks_done, p);
                                                if (!t) {
                                                        r = log_oom();
                                                        goto finish;
                                                }
                                                strv_free(arg_proc_cmdline_disks_done);
                                                arg_proc_cmdline_disks_done = t;
                                        }

                                        free(proc_device);
                                        free(proc_name);
                                }
                        } else {
                                if (create_disk(name, device, password, options) < 0)
                                        r = EXIT_FAILURE;
                        }

                next:
                        free(name);
                        free(device);
                        free(password);
                        free(options);
                }
        }

        STRV_FOREACH(i, arg_proc_cmdline_disks) {
                /*
                  Generate units for those UUIDs, which were specified
                  on the kernel command line and not yet written.
                */

                char *name, *device;
                const char *p = *i;

                if (startswith(p, "luks-"))
                        p += 5;

                if (strv_contains(arg_proc_cmdline_disks_done, p))
                        continue;

                name = strappend("luks-", p);
                device = strappend("UUID=", p);

                if (!name || !device) {
                        log_oom();
                        r = EXIT_FAILURE;
                        free(name);
                        free(device);
                        goto finish;
                }

                if (create_disk(name, device, NULL, "timeout=0") < 0)
                        r = EXIT_FAILURE;

                free(name);
                free(device);
        }

finish:
        if (f)
                fclose(f);

        strv_free(arg_proc_cmdline_disks);
        strv_free(arg_proc_cmdline_disks_done);

        return r;
}