lib.rs 6.81 KB
Newer Older
Jeremy Soller's avatar
Jeremy Soller committed
1 2
#![deny(warnings)]

Jeremy Soller's avatar
Jeremy Soller committed
3 4
#[macro_use]
extern crate serde_derive;
SamwiseFilmore's avatar
SamwiseFilmore committed
5
extern crate argon2rs;
6
extern crate libc;
SamwiseFilmore's avatar
SamwiseFilmore committed
7 8 9 10 11 12 13
extern crate liner;
extern crate failure;
extern crate pkgutils;
extern crate rand;
extern crate termion;

mod config;
Jeremy Soller's avatar
Jeremy Soller committed
14 15

pub use config::Config;
16
use config::file::FileConfig;
Jeremy Soller's avatar
Jeremy Soller committed
17

SamwiseFilmore's avatar
SamwiseFilmore committed
18 19 20 21 22 23 24
use argon2rs::verifier::Encoded;
use argon2rs::{Argon2, Variant};
use failure::{Error, err_msg};
use rand::{OsRng, Rng};
use termion::input::TermRead;
use pkgutils::{Repo, Package};

SamwiseFilmore's avatar
SamwiseFilmore committed
25
use std::env;
SamwiseFilmore's avatar
SamwiseFilmore committed
26 27 28 29 30
use std::io::{self, stderr, Write};
use std::path::Path;
use std::process::{self, Command};
use std::str::FromStr;

31
pub(crate) type Result<T> = std::result::Result<T, Error>;
SamwiseFilmore's avatar
SamwiseFilmore committed
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121

const REMOTE: &'static str = "https://static.redox-os.org/pkg";
const TARGET: &'static str = "x86_64-unknown-redox";

/// Converts a password to a serialized argon2rs hash, understandable
/// by redox_users. If the password is blank, the hash is blank.
fn hash_password(password: &str) -> Result<String> {
    if password != "" {
        let a2 = Argon2::new(10, 1, 4096, Variant::Argon2i)?;
        let salt = format!("{:X}", OsRng::new()?.next_u64());
        let enc = Encoded::new(
            a2,
            password.as_bytes(),
            salt.as_bytes(),
            &[],
            &[]
        );

        Ok(String::from_utf8(enc.to_u8())?)
    } else {
        Ok("".to_string())
    }
}

fn unwrap_or_prompt<T: FromStr>(option: Option<T>, context: &mut liner::Context, prompt: &str) -> Result<T> {
    match option {
        Some(t) => Ok(t),
        None => {
            let line = context.read_line(prompt, &mut |_| {})?;
            T::from_str(&line).map_err(|_err| err_msg("failed to parse input"))
        }
    }
}

/// Returns a password collected from the user (plaintext)
fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result<String> {
    let stdin = io::stdin();
    let mut stdin = stdin.lock();
    let stdout = io::stdout();
    let mut stdout = stdout.lock();

    print!("{}", prompt);
    let password = stdin.read_passwd(&mut stdout)?;

    print!("\n{}", confirm_prompt);
    let confirm_password = stdin.read_passwd(&mut stdout)?;
    // TODO: Remove this debug msg
    println!("\nPass: {:?}; ConfPass: {:?};", password, confirm_password);

    // Note: Actually comparing two Option<String> values
    if confirm_password == password {
        Ok(password.unwrap_or("".to_string()))
    } else {
        Err(err_msg("passwords do not match"))
    }
}

fn install_packages<S: AsRef<str>>(config: &Config, dest: &str, cookbook: Option<S>) {
    let mut repo = Repo::new(TARGET);
    repo.add_remote(REMOTE);

    if let Some(cookbook) = cookbook {
        let status = Command::new("./repo.sh")
            .current_dir(cookbook.as_ref())
            .args(config.packages.keys())
            .spawn()
            .unwrap()
            .wait()
            .unwrap();

        if !status.success() {
            write!(stderr(), "./repo.sh failed.").unwrap();
            process::exit(1);
        }

        for (packagename, _package) in &config.packages {
            println!("Installing package {}", packagename);
            let path = format!("{}/{}/repo/{}/{}.tar.gz",
                               env::current_dir().unwrap().to_string_lossy(),
                               cookbook.as_ref(), TARGET, packagename);
            Package::from_path(&path).unwrap().install(dest).unwrap();
        }
    } else {
        for (packagename, _package) in &config.packages {
            println!("Installing package {}", packagename);
            repo.fetch(&packagename).unwrap().install(dest).unwrap();
        }
    }
}

122
pub fn install<P: AsRef<Path>, S: AsRef<str>>(config: Config, output_dir: P, cookbook: Option<S>) -> Result<()> {
SamwiseFilmore's avatar
SamwiseFilmore committed
123
    let mut context = liner::Context::new();
124
    
SamwiseFilmore's avatar
SamwiseFilmore committed
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    macro_rules! prompt {
        ($dst:expr, $def:expr, $($arg:tt)*) => (if config.general.prompt {
            match unwrap_or_prompt($dst, &mut context, &format!($($arg)*)) {
                Ok(res) => if res.is_empty() {
                    Ok($def)
                } else {
                    Ok(res)
                },
                Err(err) => Err(err)
            }
        } else {
            Ok($dst.unwrap_or($def))
        })
    }

140 141
    let output_dir = output_dir.as_ref();

142
    println!("Install {:#?} to {}", config, output_dir.display());
SamwiseFilmore's avatar
SamwiseFilmore committed
143

144 145 146 147
    // TODO: Mount disk if output is a file
    let output_dir = output_dir.to_owned();

    install_packages(&config, output_dir.to_str().unwrap(), cookbook);
SamwiseFilmore's avatar
SamwiseFilmore committed
148 149

    for file in config.files {
150
        file.create(&output_dir)?;
SamwiseFilmore's avatar
SamwiseFilmore committed
151 152 153
    }

    let mut passwd = String::new();
SamwiseFilmore's avatar
SamwiseFilmore committed
154
    let mut shadow = String::new();
SamwiseFilmore's avatar
SamwiseFilmore committed
155
    let mut next_uid = 1000;
156

SamwiseFilmore's avatar
SamwiseFilmore committed
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
    for (username, user) in config.users {
        // plaintext
        let password = if let Some(password) = user.password {
            password
        } else if config.general.prompt {
            prompt_password(
                &format!("{}: enter password: ", username),
                &format!("{}: confirm password: ", username))?
        } else {
            String::new()
        };

        let uid = user.uid.unwrap_or(next_uid);

        if uid >= next_uid {
            next_uid = uid + 1;
        }

        let gid = user.gid.unwrap_or(uid);

177
        let name = prompt!(user.name, username.clone(), "{}: name (GECOS) [{}]: ", username, username)?;
SamwiseFilmore's avatar
SamwiseFilmore committed
178 179 180 181 182 183 184 185 186 187 188
        let home = prompt!(user.home, format!("/home/{}", username), "{}: home [/home/{}]: ", username, username)?;
        let shell = prompt!(user.shell, "/bin/ion".to_string(), "{}: shell [/bin/ion]: ", username)?;

        println!("Adding user {}:", username);
        println!("\tPassword: {}", password);
        println!("\tUID: {}", uid);
        println!("\tGID: {}", gid);
        println!("\tName: {}", name);
        println!("\tHome: {}", home);
        println!("\tShell: {}", shell);

SamwiseFilmore's avatar
SamwiseFilmore committed
189 190 191 192 193 194 195 196 197
        FileConfig {
            path: home.clone(),
            data: String::new(),
            symlink: false,
            directory: true,
            mode: Some(0o0700),
            uid: Some(uid),
            gid: Some(gid)
        }.create(&output_dir)?;
198

SamwiseFilmore's avatar
SamwiseFilmore committed
199
        let password = hash_password(&password)?;
200

SamwiseFilmore's avatar
SamwiseFilmore committed
201 202
        passwd.push_str(&format!("{};{};{};{};file:{};file:{}\n", username, uid, gid, name, home, shell));
        shadow.push_str(&format!("{};{}\n", username, password));
SamwiseFilmore's avatar
SamwiseFilmore committed
203
    }
204 205

    if !passwd.is_empty() {
206 207 208 209
        FileConfig {
            path: "/etc/passwd".to_string(),
            data: passwd,
            symlink: false,
SamwiseFilmore's avatar
SamwiseFilmore committed
210 211 212 213 214 215
            directory: false,
            // Take defaults
            mode: None,
            uid: None,
            gid: None
        }.create(&output_dir)?;
SamwiseFilmore's avatar
SamwiseFilmore committed
216
    }
SamwiseFilmore's avatar
SamwiseFilmore committed
217 218 219 220 221 222 223 224 225 226 227 228
    
    if !shadow.is_empty() {
        FileConfig {
            path: "/etc/shadow".to_string(),
            data: shadow,
            symlink: false,
            directory: false,
            mode: Some(0o0600),
            uid: Some(0),
            gid: Some(0)
        }.create(&output_dir)?;
    }
SamwiseFilmore's avatar
SamwiseFilmore committed
229 230 231

    Ok(())
}