Compare commits
No commits in common. "a027922b0e051b4eecc50ebbf96e511cae29664d" and "ed3b837c79a2e779c4726085725cbcbab32f1ffa" have entirely different histories.
a027922b0e
...
ed3b837c79
9 changed files with 183 additions and 273 deletions
|
@ -25,7 +25,7 @@ template $AudreyUiSetup: Adw.PreferencesDialog {
|
|||
sensitive: bind template.authn_can_edit;
|
||||
text: bind template.server_url bidirectional;
|
||||
|
||||
changed => $on_authn_changed () swapped;
|
||||
changed => $on_authn_changed ();
|
||||
}
|
||||
|
||||
Adw.EntryRow {
|
||||
|
@ -33,7 +33,7 @@ template $AudreyUiSetup: Adw.PreferencesDialog {
|
|||
sensitive: bind template.authn_can_edit;
|
||||
text: bind template.username bidirectional;
|
||||
|
||||
changed => $on_authn_changed () swapped;
|
||||
changed => $on_authn_changed ();
|
||||
}
|
||||
|
||||
Adw.PasswordEntryRow {
|
||||
|
@ -41,7 +41,7 @@ template $AudreyUiSetup: Adw.PreferencesDialog {
|
|||
sensitive: bind template.authn_can_edit;
|
||||
text: bind template.password bidirectional;
|
||||
|
||||
changed => $on_authn_changed () swapped;
|
||||
changed => $on_authn_changed ();
|
||||
}
|
||||
|
||||
Adw.ActionRow {
|
||||
|
@ -57,7 +57,7 @@ template $AudreyUiSetup: Adw.PreferencesDialog {
|
|||
title: _("Connect and save");
|
||||
sensitive: bind template.authn_can_validate;
|
||||
|
||||
activated => $on_authn_validate_activated () swapped;
|
||||
activated => $on_authn_validate_activated ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -44,5 +44,24 @@ fn main() -> glib::ExitCode {
|
|||
setlocale(LocaleCategory::LcNumeric, "C.UTF-8");
|
||||
let app = Application::new();
|
||||
|
||||
// smol test for the subsonic client
|
||||
glib::spawn_future_local(async {
|
||||
let keyring = oo7::Keyring::new().await.unwrap();
|
||||
let attributes = vec![("xdg:schema", APP_ID)];
|
||||
let items = keyring.search_items(&attributes).await.unwrap();
|
||||
if !items.is_empty() {
|
||||
let item = &items[0];
|
||||
let attributes = item.attributes().await.unwrap();
|
||||
let client = subsonic::Client::with_password(
|
||||
&attributes["server-url"],
|
||||
&attributes["username"],
|
||||
&item.secret().await.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
client.ping().await.unwrap();
|
||||
println!("{:#?}", client.get_random_songs(10).await.unwrap());
|
||||
}
|
||||
});
|
||||
|
||||
app.run()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ audrey_sources = [
|
|||
'playbin.vala',
|
||||
'rust.vapi',
|
||||
'subsonic.vala',
|
||||
'ui/setup.vala',
|
||||
'ui/window.vala',
|
||||
]
|
||||
|
||||
|
|
|
@ -24,8 +24,3 @@ typedef void AudreyUiPlayQueue;
|
|||
typedef void AudreyMpris;
|
||||
AudreyMpris *audrey_mpris_new(void *window);
|
||||
guint audrey_mpris_register_object(AudreyMpris *self, GDBusConnection *conn, const gchar *object_path, GError **error);
|
||||
|
||||
// ui::Setup
|
||||
typedef void AudreyUiSetup;
|
||||
AudreyUiSetup *audrey_ui_setup_new();
|
||||
void audrey_ui_setup_load(AudreyUiSetup *self);
|
||||
|
|
|
@ -28,19 +28,4 @@ namespace Audrey {
|
|||
public bool can_clear_all { get; }
|
||||
}
|
||||
|
||||
public class Ui.Setup : Adw.PreferencesDialog {
|
||||
public string status { get; }
|
||||
public bool authn_can_edit { get; }
|
||||
public bool authn_can_validate { get; }
|
||||
public string server_url { get; set; }
|
||||
public string username { get; set; }
|
||||
public string password { get; set; }
|
||||
|
||||
public signal void connected (Subsonic.Client api);
|
||||
|
||||
public Setup ();
|
||||
public void load ();
|
||||
public void save ();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,28 +18,6 @@ pub enum Error {
|
|||
OtherError(&'static str),
|
||||
}
|
||||
|
||||
use std::fmt;
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UrlParseError(err) => fmt::Display::fmt(err, f),
|
||||
Self::ReqwestError(err) => fmt::Display::fmt(err, f),
|
||||
Self::SubsonicError(err) => fmt::Display::fmt(&err.message, f),
|
||||
Self::OtherError(err) => fmt::Display::fmt(err, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::UrlParseError(err) => err.source(),
|
||||
Self::ReqwestError(err) => err.source(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
// don't print secret salt/token combo
|
||||
|
@ -93,7 +71,7 @@ impl Client {
|
|||
.map_err(Error::UrlParseError)?;
|
||||
|
||||
if base_url.scheme() != "http" && base_url.scheme() != "https" {
|
||||
return Err(Error::OtherError("url scheme is not HTTP(s)"));
|
||||
return Err(Error::OtherError("Url scheme is not HTTP(s)"));
|
||||
}
|
||||
|
||||
Ok(Client {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
mod ffi {
|
||||
use gtk::glib;
|
||||
use std::ffi::c_char;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AudreySubsonicClient {
|
||||
|
@ -14,16 +13,9 @@ mod ffi {
|
|||
|
||||
extern "C" {
|
||||
pub fn audrey_subsonic_client_get_type() -> glib::ffi::GType;
|
||||
pub fn audrey_subsonic_client_new_with_token(
|
||||
server_url: *mut c_char,
|
||||
username: *mut c_char,
|
||||
token: *mut c_char,
|
||||
salt: *mut c_char,
|
||||
) -> *mut AudreySubsonicClient;
|
||||
}
|
||||
}
|
||||
|
||||
use glib::translate::{from_glib_none, ToGlibPtr};
|
||||
use gtk::glib;
|
||||
|
||||
glib::wrapper! {
|
||||
|
@ -33,16 +25,3 @@ glib::wrapper! {
|
|||
type_ => || ffi::audrey_subsonic_client_get_type(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn with_token(server_url: &str, username: &str, token: &str, salt: &str) -> Self {
|
||||
unsafe {
|
||||
from_glib_none(ffi::audrey_subsonic_client_new_with_token(
|
||||
server_url.to_glib_none().0,
|
||||
username.to_glib_none().0,
|
||||
token.to_glib_none().0,
|
||||
salt.to_glib_none().0,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
218
src/ui/setup.rs
218
src/ui/setup.rs
|
@ -1,221 +1,29 @@
|
|||
mod imp {
|
||||
use adw::{glib, prelude::*, subclass::prelude::*};
|
||||
use glib::subclass::{InitializingObject, Signal};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::sync::OnceLock;
|
||||
mod ffi {
|
||||
use gtk::glib;
|
||||
|
||||
#[derive(gtk::CompositeTemplate, glib::Properties, Default)]
|
||||
#[template(resource = "/eu/callcc/audrey/setup.ui")]
|
||||
#[properties(wrapper_type = super::Setup)]
|
||||
pub struct Setup {
|
||||
#[property(get, set)]
|
||||
status: RefCell<String>,
|
||||
|
||||
#[property(get, set)]
|
||||
authn_can_edit: Cell<bool>,
|
||||
#[property(get, set)]
|
||||
authn_can_validate: Cell<bool>,
|
||||
|
||||
#[property(get, set)]
|
||||
server_url: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
username: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
password: RefCell<String>,
|
||||
#[repr(C)]
|
||||
pub struct AudreyUiSetup {
|
||||
parent_instance: adw::ffi::AdwPreferencesDialog,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Setup {
|
||||
const NAME: &'static str = "AudreyUiSetup";
|
||||
type Type = super::Setup;
|
||||
type ParentType = adw::PreferencesDialog;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
#[repr(C)]
|
||||
pub struct AudreyUiSetupClass {
|
||||
parent_class: adw::ffi::AdwPreferencesDialogClass,
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for Setup {
|
||||
fn signals() -> &'static [Signal] {
|
||||
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
|
||||
SIGNALS.get_or_init(|| {
|
||||
vec![Signal::builder("connected")
|
||||
.param_types([crate::subsonic_vala::Client::static_type()])
|
||||
.build()]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for Setup {}
|
||||
|
||||
impl AdwDialogImpl for Setup {}
|
||||
|
||||
impl PreferencesDialogImpl for Setup {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl Setup {
|
||||
#[template_callback]
|
||||
fn on_authn_changed(&self) {
|
||||
self.obj().set_authn_can_validate(true);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
pub(super) async fn on_authn_validate_activated(&self) {
|
||||
self.obj().set_authn_can_validate(false);
|
||||
self.obj().set_authn_can_edit(false);
|
||||
self.obj().set_status("Connecting...");
|
||||
|
||||
let api = match crate::subsonic::Client::with_password(
|
||||
&self.obj().server_url(),
|
||||
&self.obj().username(),
|
||||
self.obj().password().as_bytes(),
|
||||
) {
|
||||
Ok(api) => api,
|
||||
Err(err) => {
|
||||
self.obj().set_status(format!("Error: {err}"));
|
||||
self.obj().set_authn_can_validate(true);
|
||||
self.obj().set_authn_can_edit(true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match api.ping().await {
|
||||
Ok(()) => {
|
||||
self.obj().set_status("Connected");
|
||||
self.save().await;
|
||||
|
||||
// please REMOVEME once we've killed vala
|
||||
fn get_random_salt(length: usize) -> String {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
std::iter::repeat(())
|
||||
// 0.9: s/distributions/distr
|
||||
.map(|()| rng.sample(rand::distributions::Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(length)
|
||||
.collect::<String>()
|
||||
}
|
||||
let new_salt = get_random_salt(8);
|
||||
use md5::Digest;
|
||||
let mut hasher = md5::Md5::new();
|
||||
hasher.update(self.obj().password().as_bytes());
|
||||
hasher.update(new_salt.as_bytes());
|
||||
let new_token_bytes = hasher.finalize();
|
||||
let new_token = base16ct::lower::encode_string(&new_token_bytes);
|
||||
|
||||
let vala_api = crate::subsonic_vala::Client::with_token(
|
||||
&self.obj().server_url(),
|
||||
&self.obj().username(),
|
||||
&new_token,
|
||||
&new_salt,
|
||||
);
|
||||
self.obj().emit_by_name::<()>("connected", &[&vala_api]);
|
||||
}
|
||||
Err(err) => {
|
||||
self.obj().set_status(format!("Error: {err}"));
|
||||
self.obj().set_authn_can_validate(true);
|
||||
}
|
||||
}
|
||||
|
||||
self.obj().set_authn_can_edit(true);
|
||||
}
|
||||
|
||||
async fn save(&self) {
|
||||
self.obj().set_authn_can_edit(false);
|
||||
|
||||
// TODO remove unwraps etc etc
|
||||
let keyring = oo7::Keyring::new().await.unwrap();
|
||||
let mut attributes = vec![("xdg:schema", crate::APP_ID.to_string())];
|
||||
// clear previous passwords
|
||||
keyring.delete(&attributes).await.unwrap();
|
||||
|
||||
attributes.push(("server-url", self.obj().server_url()));
|
||||
attributes.push(("username", self.obj().username()));
|
||||
keyring
|
||||
.create_item(
|
||||
"Audrey Subsonic password",
|
||||
&attributes,
|
||||
self.obj().password(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.obj().set_authn_can_edit(true);
|
||||
}
|
||||
extern "C" {
|
||||
pub fn audrey_ui_setup_get_type() -> glib::ffi::GType;
|
||||
}
|
||||
}
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Setup(ObjectSubclass<imp::Setup>)
|
||||
pub struct Setup(Object<ffi::AudreyUiSetup, ffi::AudreyUiSetupClass>)
|
||||
@extends adw::PreferencesDialog, adw::Dialog, gtk::Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl Setup {
|
||||
pub fn load(&self) {
|
||||
glib::spawn_future_local(glib::clone!(
|
||||
#[weak(rename_to = self_)]
|
||||
self,
|
||||
async move {
|
||||
self_.set_authn_can_edit(false);
|
||||
|
||||
// TODO remove unwraps, make sure authn_can_edit is set back to true
|
||||
let keyring = oo7::Keyring::new().await.unwrap();
|
||||
let attributes = vec![("xdg:schema", crate::APP_ID)];
|
||||
let items = keyring.search_items(&attributes).await.unwrap();
|
||||
|
||||
if items.is_empty() {
|
||||
// didn't find shit, leave all empty
|
||||
self_.set_server_url("");
|
||||
self_.set_username("");
|
||||
self_.set_password("");
|
||||
// TODO: onboarding
|
||||
self_.set_authn_can_edit(true);
|
||||
self_.set_authn_can_validate(true);
|
||||
} else {
|
||||
let item = &items[0];
|
||||
let attributes = item.attributes().await.unwrap();
|
||||
|
||||
self_.set_server_url(attributes["server-url"].clone());
|
||||
self_.set_username(attributes["username"].clone());
|
||||
// strip non-utf8 elements from the pw, i guess
|
||||
self_.set_password(String::from_utf8_lossy(&item.secret().await.unwrap()));
|
||||
|
||||
// first connection
|
||||
self_.set_authn_can_validate(true);
|
||||
self_.imp().on_authn_validate_activated().await;
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
mod ffi {
|
||||
use glib::subclass::basic::InstanceStruct;
|
||||
use glib::translate::{from_glib_none, IntoGlibPtr};
|
||||
use glib::Object;
|
||||
use gtk::glib;
|
||||
|
||||
type AudreyUiSetup = InstanceStruct<super::imp::Setup>;
|
||||
|
||||
#[no_mangle]
|
||||
extern "C" fn audrey_ui_setup_new() -> *mut AudreyUiSetup {
|
||||
unsafe { Object::new::<super::Setup>().into_glib_ptr() }
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
extern "C" fn audrey_ui_setup_load(self_: *mut AudreyUiSetup) {
|
||||
let self_: &super::Setup = unsafe { &from_glib_none(self_) };
|
||||
self_.load()
|
||||
match fn {
|
||||
type_ => || ffi::audrey_ui_setup_get_type(),
|
||||
}
|
||||
}
|
||||
|
|
145
src/ui/setup.vala
Normal file
145
src/ui/setup.vala
Normal file
|
@ -0,0 +1,145 @@
|
|||
static void salt_password (string password, out string token, out string salt) {
|
||||
const int SALT_BYTES = 8;
|
||||
uchar salt_bytes[SALT_BYTES];
|
||||
GCrypt.Random.randomize (salt_bytes, GCrypt.Random.Level.STRONG);
|
||||
uchar salt_chars[2*SALT_BYTES+1];
|
||||
for (int i = 0; i < SALT_BYTES; i += 1) {
|
||||
salt_chars[2*i+0] = "0123456789abcdef"[(salt_bytes[i]>>4)&0xf];
|
||||
salt_chars[2*i+1] = "0123456789abcdef"[(salt_bytes[i]>>0)&0xf];
|
||||
}
|
||||
salt_chars[2*SALT_BYTES] = 0;
|
||||
var checksum = new GLib.Checksum (ChecksumType.MD5);
|
||||
checksum.update ((uchar[]) password, -1);
|
||||
checksum.update (salt_chars, -1);
|
||||
|
||||
token = checksum.get_string ();
|
||||
salt = (string) salt_chars;
|
||||
}
|
||||
|
||||
[GtkTemplate (ui = "/eu/callcc/audrey/setup.ui")]
|
||||
public class Audrey.Ui.Setup : Adw.PreferencesDialog {
|
||||
public string status { get; private set; default = _("Not connected"); }
|
||||
|
||||
public bool authn_can_edit { get; private set; default = true; }
|
||||
public bool authn_can_validate { get; private set; default = false; }
|
||||
|
||||
public string server_url { get; set; default = ""; }
|
||||
public string username { get; set; default = ""; }
|
||||
public string password { get; set; default = ""; }
|
||||
public string token;
|
||||
public string salt;
|
||||
|
||||
public signal void connected (Subsonic.Client api);
|
||||
|
||||
private static Secret.Schema secret_schema = new Secret.Schema (
|
||||
"eu.callcc.audrey",
|
||||
Secret.SchemaFlags.NONE,
|
||||
"server-url", Secret.SchemaAttributeType.STRING,
|
||||
"username", Secret.SchemaAttributeType.STRING
|
||||
);
|
||||
|
||||
[GtkCallback] private void on_authn_changed () {
|
||||
this.authn_can_validate = true;
|
||||
}
|
||||
|
||||
[GtkCallback] private void on_authn_validate_activated () {
|
||||
this.authn_can_validate = false;
|
||||
this.authn_can_edit = false;
|
||||
this.status = _("Connecting...");
|
||||
|
||||
string new_token, new_salt;
|
||||
salt_password (this.password, out new_token, out new_salt);
|
||||
var api = new Subsonic.Client.with_token (
|
||||
this.server_url,
|
||||
this.username,
|
||||
new_token,
|
||||
new_salt);
|
||||
|
||||
api.ping.begin ((obj, res) => {
|
||||
try {
|
||||
api.ping.end (res);
|
||||
this.status = _("Connected");
|
||||
this.token = new_token;
|
||||
this.salt = new_salt;
|
||||
this.save ();
|
||||
|
||||
this.connected (api);
|
||||
} catch (Error e) {
|
||||
this.status = @"$(_("Ping failed")): $(e.message)";
|
||||
this.authn_can_validate = true;
|
||||
}
|
||||
|
||||
this.authn_can_edit = true;
|
||||
});
|
||||
}
|
||||
|
||||
public void load () {
|
||||
this.authn_can_edit = false;
|
||||
Secret.password_searchv.begin (
|
||||
secret_schema,
|
||||
new HashTable<string, string> (null, null),
|
||||
Secret.SearchFlags.UNLOCK,
|
||||
null,
|
||||
(obj, res) => {
|
||||
try {
|
||||
var list = Secret.password_searchv.end (res);
|
||||
if (list == null) {
|
||||
// didn't find shit, leave all empty
|
||||
this.server_url = "";
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
// TODO: onboarding
|
||||
this.authn_can_edit = true;
|
||||
this.authn_can_validate = true;
|
||||
} else {
|
||||
var first = list.data;
|
||||
assert (first != null);
|
||||
|
||||
this.server_url = first.attributes["server-url"];
|
||||
this.username = first.attributes["username"];
|
||||
|
||||
first.retrieve_secret.begin (null, (obj, res) => {
|
||||
try {
|
||||
var value = first.retrieve_secret.end (res);
|
||||
this.password = value.get_text () ?? "";
|
||||
} catch (Error e) {
|
||||
error ("could not retrieve password from credentials: %s", e.message);
|
||||
}
|
||||
|
||||
// first connection
|
||||
this.authn_can_validate = true;
|
||||
this.on_authn_validate_activated ();
|
||||
});
|
||||
}
|
||||
} catch (Error e) {
|
||||
error ("could not search for password in keyring: %s", e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async void save_internal () {
|
||||
this.authn_can_edit = false;
|
||||
|
||||
try {
|
||||
yield Secret.password_clear (secret_schema, null);
|
||||
} catch (Error e) {
|
||||
error ("could not clear previous passwords from keyring: %s", e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
yield Secret.password_store (secret_schema, null, "Audrey Subsonic password", this.password, null, "server-url", this.server_url, "username", this.username);
|
||||
} catch (Error e) {
|
||||
error ("could not store password in keyring: %s", e.message);
|
||||
}
|
||||
|
||||
this.authn_can_edit = true;
|
||||
}
|
||||
|
||||
public void save () {
|
||||
this.save_internal.begin ();
|
||||
}
|
||||
|
||||
~Setup () {
|
||||
debug ("destroying setup dialog");
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue