moonfire-nvr/server/src/cmds/login.rs

153 lines
4.8 KiB
Rust

// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
//! Subcommand to login a user (without requiring a password).
use base::clock::{self, Clocks};
use db::auth::SessionFlag;
use failure::{format_err, Error};
use std::io::Write as _;
use std::os::unix::fs::OpenOptionsExt as _;
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, Default, StructOpt)]
pub struct Args {
/// Directory holding the SQLite3 index database.
#[structopt(
long,
default_value = "/var/lib/moonfire-nvr/db",
value_name = "path",
parse(from_os_str)
)]
db_dir: PathBuf,
/// Create a session with the given permissions.
///
/// If unspecified, uses user's default permissions.
#[structopt(long, value_name="perms",
parse(try_from_str = protobuf::text_format::parse_from_str))]
permissions: Option<db::Permissions>,
/// Restrict this cookie to the given domain.
#[structopt(long)]
domain: Option<String>,
/// Write the cookie to a new curl-compatible cookie-jar file.
///
/// ---domain must be specified. This file can be used later with curl's --cookie flag.
#[structopt(long, requires("domain"), value_name = "path")]
curl_cookie_jar: Option<PathBuf>,
/// Set the given db::auth::SessionFlags.
#[structopt(
long,
default_value = "http-only,secure,same-site,same-site-strict",
value_name = "flags",
use_delimiter = true
)]
session_flags: Vec<SessionFlag>,
/// Create the session for this username.
username: String,
}
pub fn run(args: &Args) -> Result<i32, Error> {
let clocks = clock::RealClocks {};
let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
let db = std::sync::Arc::new(db::Database::new(clocks.clone(), conn, true).unwrap());
let mut l = db.lock();
let u = l
.get_user(&args.username)
.ok_or_else(|| format_err!("no such user {:?}", &args.username))?;
let permissions = args.permissions.as_ref().unwrap_or(&u.permissions).clone();
let creation = db::auth::Request {
when_sec: Some(db.clocks().realtime().sec),
user_agent: None,
addr: None,
};
let mut flags = 0;
for f in &args.session_flags {
flags |= *f as i32;
}
let uid = u.id;
drop(u);
let (sid, _) = l.make_session(
creation,
uid,
args.domain.as_ref().map(|d| d.as_bytes().to_owned()),
flags,
permissions,
)?;
let mut encoded = [0u8; 64];
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8");
if let Some(ref p) = args.curl_cookie_jar {
let d = args
.domain
.as_ref()
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?;
let mut f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(p)
.map_err(|e| format_err!("Unable to open {}: {}", p.display(), e))?;
write!(
&mut f,
"# Netscape HTTP Cookie File\n\
# https://curl.haxx.se/docs/http-cookies.html\n\
# This file was generated by moonfire-nvr login! Edit at your own risk.\n\n\
{}\n",
curl_cookie(encoded, flags, d)
)?;
f.sync_all()?;
println!("Wrote cookie to {}", p.display());
} else {
println!("s={}", encoded);
}
Ok(0)
}
fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
format!(
"{httponly}{domain}\t{tailmatch}\t{path}\t{secure}\t{expires}\t{name}\t{value}",
httponly = if (flags & SessionFlag::HttpOnly as i32) != 0 {
"#HttpOnly_"
} else {
""
},
domain = domain,
tailmatch = "FALSE",
path = "/",
secure = if (flags & SessionFlag::Secure as i32) != 0 {
"TRUE"
} else {
"FALSE"
},
expires = "9223372036854775807", // 64-bit CURL_OFF_T_MAX, never expires
name = "s",
value = cookie
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_curl_cookie() {
assert_eq!(
curl_cookie(
"o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q",
SessionFlag::HttpOnly as i32,
"localhost"
),
"#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\
o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q"
);
}
}