...
 
Commits (6)
This diff is collapsed.
......@@ -11,13 +11,15 @@ name = "redox_installer"
path = "src/lib.rs"
[dependencies]
arg_parser = { git = "https://gitlab.redox-os.org/redox-os/arg-parser.git" }
arg_parser = { git = "https://github.com/redox-os/arg-parser.git" }
argon2rs = { version = "0.2", default-features = false }
liner = "0.1"
pkgutils = { git = "https://gitlab.redox-os.org/redox-os/pkgutils.git" }
libc = "0.2"
failure = "0.1"
pkgutils = { git = "https://github.com/redox-os/pkgutils.git" }
rand = "0.3"
redoxfs = "0.3"
serde = "0.8"
serde_derive = "0.8"
termion = "1.5.1"
toml = { version = "0.2", default-features = false, features = ["serde"] }
redox_users = { git = "https://gitlab.redox-os.org/redox-os/users.git" }
......@@ -59,8 +59,7 @@ uutils = {}
# User settings
[users.root]
# Password is set to "password"
password = "$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk"
password = "password"
uid = 0
gid = 0
name = "root"
......@@ -172,3 +171,10 @@ Welcome to Redox OS!
path = "/usr"
data = "/"
symlink = true
[[files]]
path = "/tmp"
data = ""
directory = true
# 0o1777
mode = 1023
......@@ -17,8 +17,7 @@ userutils = {}
# User settings
[users.root]
# Password is set to "password"
password = "$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk"
password = "password"
uid = 0
gid = 0
name = "root"
......
use Result;
use libc::chown;
use std::io::{Error, Write};
use std::ffi::{CString, OsStr};
use std::fs::{self, File};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{PermissionsExt, symlink};
use std::path::Path;
//type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Default, Deserialize)]
pub struct FileConfig {
pub path: String,
pub data: String,
#[serde(default)]
pub symlink: bool
pub symlink: bool,
#[serde(default)]
pub directory: bool,
pub mode: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>
}
// TODO: Rewrite impls
impl FileConfig {
pub(crate) fn create<P: AsRef<Path>>(self, prefix: P) -> Result<()> {
let path = self.path.trim_left_matches('/');
let target_file = prefix.as_ref()
.join(path);
if self.directory {
println!("Create directory {}", target_file.display());
fs::create_dir_all(&target_file)?;
self.apply_perms(&target_file)?;
return Ok(());
} else if let Some(parent) = target_file.parent() {
println!("Create file parent {}", parent.display());
fs::create_dir_all(parent)?;
}
if self.symlink {
println!("Create symlink {}", target_file.display());
symlink(&OsStr::new(&self.data), &target_file)?;
Ok(())
} else {
println!("Create file {}", target_file.display());
let mut file = File::create(&target_file)?;
file.write_all(self.data.as_bytes())?;
self.apply_perms(target_file)
}
}
fn apply_perms<P: AsRef<Path>>(&self, target: P) -> Result<()> {
let path = target.as_ref();
let mode = self.mode.unwrap_or_else(|| if self.directory {
0o0755
} else {
0o0644
});
let uid = self.uid.unwrap_or(0);
let gid = self.gid.unwrap_or(0);
// chmod
fs::set_permissions(path, fs::Permissions::from_mode(mode))?;
// chown
let c_path = CString::new(path.as_os_str().as_bytes()).unwrap();
let ret = unsafe {
chown(c_path.as_ptr(), uid, gid)
};
// credit to uutils
if ret == 0 {
Ok(())
} else {
Err(Error::last_os_error().into())
}
}
}
use std::collections::BTreeMap;
mod general;
mod file;
pub(crate) mod file;
mod package;
mod user;
......
extern crate liner;
extern crate pkgutils;
extern crate rand;
extern crate termion;
extern crate redox_users;
use self::rand::Rng;
use self::termion::input::TermRead;
use self::pkgutils::{Repo, Package};
use std::{env, fs};
use std::ffi::OsStr;
use std::io::{self, stderr, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::{self, Command};
use std::str::FromStr;
use config::Config;
const REMOTE: &'static str = "https://static.redox-os.org/pkg";
const TARGET: &'static str = "x86_64-unknown-redox";
fn unwrap_or_prompt<T: FromStr>(option: Option<T>, context: &mut liner::Context, prompt: &str) -> Result<T, String> {
match option {
None => {
let line = context.read_line(prompt, &mut |_| {}).map_err(|err| format!("failed to read line: {}", err))?;
T::from_str(&line).map_err(|_| format!("failed to parse '{}'", line))
},
Some(t) => Ok(t)
}
}
fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result<String, String> {
let stdin = io::stdin();
let mut stdin = stdin.lock();
let stdout = io::stdout();
let mut stdout = stdout.lock();
stdout.write(prompt.as_bytes()).map_err(|err| format!("failed to write to stdout: {}", err))?;
stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?;
if let Some(password) = stdin.read_passwd(&mut stdout).map_err(|err| format!("failed to read password: {}", err))? {
stdout.write(b"\n").map_err(|err| format!("failed to write to stdout: {}", err))?;
stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?;
if password.is_empty() {
Ok(password)
} else {
stdout.write(confirm_prompt.as_bytes()).map_err(|err| format!("failed to write to stdout: {}", err))?;
stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?;
if let Some(confirm_password) = stdin.read_passwd(&mut stdout).map_err(|err| format!("failed to read password: {}", err))? {
stdout.write(b"\n").map_err(|err| format!("failed to write to stdout: {}", err))?;
stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?;
if confirm_password == password {
let salt = format!("{:X}", rand::OsRng::new().unwrap().next_u64());
Ok(redox_users::User::encode_passwd(&password, &salt))
} else {
Err("passwords do not match".to_string())
}
} else {
Err("passwords do not match".to_string())
}
}
} else {
Ok(String::new())
}
}
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();
}
}
}
pub fn install<P: AsRef<Path>, S: AsRef<str>>(config: Config, output: P, cookbook: Option<S>) -> Result<(), String> {
let output = output.as_ref();
println!("Install {:#?} to {}", config, output.display());
let mut context = liner::Context::new();
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))
})
}
// TODO: Mount disk if output is a file
let sysroot = output.to_owned();
macro_rules! dir {
($path:expr) => {{
let mut path = sysroot.clone();
path.push($path);
println!("Create directory {}", path.display());
fs::create_dir_all(&path).map_err(|err| format!("failed to create {}: {}", path.display(), err))?;
}};
}
macro_rules! file {
($path:expr, $data:expr, $symlink:expr) => {{
let mut path = sysroot.clone();
path.push($path);
if let Some(parent) = path.parent() {
println!("Create file parent {}", parent.display());
fs::create_dir_all(parent).map_err(|err| format!("failed to create file parent {}: {}", parent.display(), err))?;
}
if $symlink {
println!("Create symlink {}", path.display());
symlink(&OsStr::from_bytes($data), &path).map_err(|err| format!("failed to symlink {}: {}", path.display(), err))?;
} else {
println!("Create file {}", path.display());
let mut file = fs::File::create(&path).map_err(|err| format!("failed to create {}: {}", path.display(), err))?;
file.write_all($data).map_err(|err| format!("failed to write {}: {}", path.display(), err))?;
}
}};
}
dir!("");
install_packages(&config, sysroot.to_str().unwrap(), cookbook);
for file in config.files {
file!(file.path.trim_matches('/'), file.data.as_bytes(), file.symlink);
}
let mut passwd = String::new();
let mut shadow = String::new();
let mut next_uid = 1000;
for (username, user) in config.users {
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);
let name = prompt!(user.name, username.clone(), "{}: name [{}]: ", username, username)?;
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);
dir!(home.trim_matches('/'));
passwd.push_str(&format!("{};{};{};{};file:{};file:{}\n", username, uid, gid, name, home, shell));
shadow.push_str(&format!("{};{}\n", username, password));
}
if !passwd.is_empty() {
file!("etc/passwd", passwd.as_bytes(), false);
}
if !shadow.is_empty() {
file!("etc/shadow", shadow.as_bytes(), false);
}
Ok(())
}
......@@ -2,9 +2,230 @@
#[macro_use]
extern crate serde_derive;
extern crate argon2rs;
extern crate libc;
extern crate liner;
extern crate failure;
extern crate pkgutils;
extern crate rand;
extern crate termion;
mod config;
pub use config::Config;
pub use install::install;
use config::file::FileConfig;
mod config;
mod install;
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};
use std::env;
use std::io::{self, stderr, Write};
use std::path::Path;
use std::process::{self, Command};
use std::str::FromStr;
pub(crate) type Result<T> = std::result::Result<T, Error>;
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();
}
}
}
pub fn install<P: AsRef<Path>, S: AsRef<str>>(config: Config, output_dir: P, cookbook: Option<S>) -> Result<()> {
let mut context = liner::Context::new();
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))
})
}
let output_dir = output_dir.as_ref();
println!("Install {:#?} to {}", config, output_dir.display());
// TODO: Mount disk if output is a file
let output_dir = output_dir.to_owned();
install_packages(&config, output_dir.to_str().unwrap(), cookbook);
for file in config.files {
file.create(&output_dir)?;
}
let mut passwd = String::new();
let mut shadow = String::new();
let mut next_uid = 1000;
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);
let name = prompt!(user.name, username.clone(), "{}: name (GECOS) [{}]: ", username, username)?;
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);
FileConfig {
path: home.clone(),
data: String::new(),
symlink: false,
directory: true,
mode: Some(0o0700),
uid: Some(uid),
gid: Some(gid)
}.create(&output_dir)?;
let password = hash_password(&password)?;
passwd.push_str(&format!("{};{};{};{};file:{};file:{}\n", username, uid, gid, name, home, shell));
shadow.push_str(&format!("{};{}\n", username, password));
}
if !passwd.is_empty() {
FileConfig {
path: "/etc/passwd".to_string(),
data: passwd,
symlink: false,
directory: false,
// Take defaults
mode: None,
uid: None,
gid: None
}.create(&output_dir)?;
}
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)?;
}
Ok(())
}