Add code to authenticate to NextCloud server

This commit is contained in:
Matteo Settenvini 2022-07-02 18:32:41 +02:00
parent 24751c4912
commit ce5503da89
Signed by: matteo
GPG key ID: 8576CC1AD97D42DF
7 changed files with 614 additions and 3 deletions

118
src/config/credentials.rs Normal file
View file

@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
use {
anyhow::{anyhow, Result},
reqwest::{StatusCode, Url},
serde::{Deserialize, Serialize},
std::collections::HashMap,
std::io::ErrorKind,
std::path::{Path, PathBuf},
std::time::Duration,
};
#[derive(Deserialize, Serialize, Default)]
pub struct Credentials {
#[serde(skip)]
config_file: PathBuf,
servers: HashMap<String, Server>,
}
#[derive(Deserialize, Serialize)]
pub struct Server {
url: String,
login_name: String,
password: String,
}
impl Credentials {
pub fn from_directory(config_dir: &Path) -> Result<Self> {
let credentials_file = config_dir.join("credentials.toml");
match std::fs::read_to_string(&credentials_file) {
Ok(content) => {
let mut credentials: Credentials = toml::from_str(&content)?;
credentials.config_file = credentials_file;
Ok(credentials)
}
Err(err) if err.kind() == ErrorKind::NotFound => {
let mut credentials = Credentials::default();
credentials.config_file = credentials_file;
credentials.write_back()?;
Ok(credentials)
}
Err(err) => Err(anyhow!(err)),
}
}
pub fn add(&mut self, server: Url) -> Result<()> {
let http_client = reqwest::blocking::Client::new();
#[derive(Deserialize)]
struct LoginFlow {
poll: LoginPollEndpoint,
login: String,
}
#[derive(Deserialize)]
struct LoginPollEndpoint {
token: String,
endpoint: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct LoginResult {
server: String,
login_name: String,
app_password: String,
}
let login_url = server.join("index.php/login/v2")?;
let login_flow = http_client
.post(login_url.clone())
.send()?
.json::<LoginFlow>()?;
// User auth flow happens in browser
webbrowser::open(&login_flow.login)?;
// We start polling for the end of the flow
loop {
let response = http_client
.post(&login_flow.poll.endpoint)
.form(&[("token", &login_flow.poll.token)])
.send()?;
match response.status() {
StatusCode::OK => {
let login_result = response.json::<LoginResult>()?;
let new_server = Server {
url: login_result.server,
login_name: login_result.login_name,
password: login_result.app_password,
};
let host_name = server
.host_str()
.ok_or(anyhow!("No hostname for provided URL, is it valid?"))?;
self.servers.insert(host_name.to_owned(), new_server);
break;
}
StatusCode::NOT_FOUND => {
std::thread::sleep(Duration::from_secs(1));
// ...then keep polling
}
_ => {
response.error_for_status()?;
}
}
}
self.write_back()
}
fn write_back(&self) -> Result<()> {
std::fs::write(&self.config_file, toml::to_string(self)?)?;
Ok(())
}
}

40
src/config/mod.rs Normal file
View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod credentials;
use {
self::credentials::Credentials,
anyhow::Result,
directories::ProjectDirs,
serde::Deserialize,
std::io::{Error, ErrorKind},
std::path::PathBuf,
};
#[derive(Deserialize)]
pub struct Config {
#[serde(skip_deserializing)]
pub credentials: Credentials,
}
impl Config {
pub fn new() -> Self {
let config_dir = Self::ensure_config_dir().unwrap();
Config {
credentials: Credentials::from_directory(&config_dir).unwrap(),
}
}
fn ensure_config_dir() -> Result<PathBuf> {
let config_dir = ProjectDirs::from("eu", "montecristosoftware", "cooking-schedule")
.ok_or(Error::new(
ErrorKind::Other,
"Unable to determine application configuration folder",
))?
.config_dir()
.to_path_buf();
std::fs::create_dir_all(&config_dir)?;
Ok(config_dir)
}
}

View file

@ -1,6 +1,47 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
fn main() {
println!("Hello, world!");
mod config;
use {
self::config::Config,
clap::{arg, command, ArgMatches, Command},
reqwest::Url,
};
fn parse_args() -> ArgMatches {
command!()
.propagate_version(true)
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("init")
.about("Authenticate against the provided NextCloud server")
.arg(arg!(<server> "NextCloud server to connect to")),
)
.get_matches()
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> anyhow::Result<()> {
let args = parse_args();
let mut configuration = Config::new();
match args.subcommand() {
Some(("init", sub_matches)) => {
let server = sub_matches
.get_one::<String>("server")
.expect("Mandatory parameter <server>");
tokio::task::block_in_place(move || -> anyhow::Result<()> {
configuration
.credentials
.add(Url::parse(server)?)
.expect("Unable to authenticate to NextCloud instance");
Ok(())
})?;
}
_ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
};
Ok(())
}