Add code to authenticate to NextCloud server
This commit is contained in:
parent
24751c4912
commit
ce5503da89
7 changed files with 614 additions and 3 deletions
118
src/config/credentials.rs
Normal file
118
src/config/credentials.rs
Normal 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
40
src/config/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
45
src/main.rs
45
src/main.rs
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue