lib.rs 6.81 KB
Newer Older
1 2
#![deny(warnings)]

Jeremy Soller's avatar
Jeremy Soller committed
3 4
#[macro_use]
extern crate serde_derive;
5
extern crate argon2rs;
6
extern crate libc;
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

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;
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>;
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<()> {
123
    let mut context = liner::Context::new();
124
    
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());
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);
148 149

    for file in config.files {
150
        file.create(&output_dir)?;
151 152 153
    }

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

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)?;
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

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));
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)?;
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)?;
    }
229 230 231

    Ok(())
}