mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-02-14 14:15:03 +01:00
feat: implement wifi-connection plugin
This commit is contained in:
parent
f00ad7ca49
commit
16ccc5cdef
11 changed files with 844 additions and 0 deletions
|
@ -6,4 +6,5 @@ members = [
|
|||
"plugin-run",
|
||||
"plugin-desktop",
|
||||
"plugin-audio",
|
||||
"plugin-network",
|
||||
]
|
||||
|
|
2
Makefile
2
Makefile
|
@ -21,6 +21,7 @@ deploy:
|
|||
cp -vf ./target/release/desktop ${DEST}/drun
|
||||
cp -vf ./target/release/run ${DEST}/run
|
||||
cp -vf ./target/release/audio ${DEST}/audio
|
||||
cp -vf ./target/release/rmenu-network ${DEST}/network
|
||||
cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml
|
||||
|
||||
build: build-rmenu build-plugins
|
||||
|
@ -32,3 +33,4 @@ build-plugins:
|
|||
${CARGO} build -p run ${FLAGS}
|
||||
${CARGO} build -p desktop ${FLAGS}
|
||||
${CARGO} build -p audio ${FLAGS}
|
||||
${CARGO} build -p rmenu-network ${FLAGS}
|
||||
|
|
23
plugin-network/Cargo.toml
Normal file
23
plugin-network/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "rmenu-network"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.72"
|
||||
async-std = "1.12.0"
|
||||
clap = { version = "4.3.21", features = ["derive"] }
|
||||
dioxus = "0.4.0"
|
||||
dioxus-desktop = "0.4.0"
|
||||
dioxus-free-icons = { version = "0.7.0", features = ["font-awesome-regular"] }
|
||||
env_logger = "0.10.0"
|
||||
futures-channel = "0.3.28"
|
||||
glib = { git = "https://github.com/gtk-rs/gtk-rs-core", version = "0.19.0" }
|
||||
keyboard-types = "0.6.2"
|
||||
log = "0.4.20"
|
||||
nm = { git = "https://github.com/balena-io-modules/libnm-rs.git", version = "0.4.0" }
|
||||
once_cell = "1.18.0"
|
||||
rmenu-plugin = { version = "0.0.0", path = "../rmenu-plugin" }
|
||||
serde_json = "1.0.104"
|
96
plugin-network/public/default.css
Normal file
96
plugin-network/public/default.css
Normal file
|
@ -0,0 +1,96 @@
|
|||
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
background-color: #e8edee;
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 0.15rem;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
#secret {
|
||||
width: -webkit-fill-available;
|
||||
height: 2rem;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0 0.5rem;
|
||||
background-color: #ccd2df;
|
||||
}
|
||||
|
||||
#icon {
|
||||
float: right;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-left: -2.5rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
@keyframes movein {
|
||||
from { margin-top: 10rem; }
|
||||
to { margin-top: 1rem; }
|
||||
}
|
||||
|
||||
@keyframes moveout {
|
||||
from { margin-top: 1rem; }
|
||||
to { bottom: 10rem; }
|
||||
}
|
||||
|
||||
#message {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
animation: movein 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
#message.error {
|
||||
color: white;
|
||||
background-color: #de5a5a;
|
||||
}
|
||||
|
||||
#message.success {
|
||||
color: white;
|
||||
background-color: #53c351;
|
||||
}
|
||||
|
||||
#blackout {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
|
||||
background-color: black;
|
||||
filter: alpha(opacity=30);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#blackout > #spinner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#blackout.active {
|
||||
display: block;
|
||||
}
|
78
plugin-network/public/spinner.css
Normal file
78
plugin-network/public/spinner.css
Normal file
|
@ -0,0 +1,78 @@
|
|||
.lds-spinner {
|
||||
color: official;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-spinner div {
|
||||
transform-origin: 40px 40px;
|
||||
animation: lds-spinner 1.2s linear infinite;
|
||||
}
|
||||
.lds-spinner div:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 37px;
|
||||
width: 6px;
|
||||
height: 18px;
|
||||
border-radius: 20%;
|
||||
background: #fff;
|
||||
}
|
||||
.lds-spinner div:nth-child(1) {
|
||||
transform: rotate(0deg);
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
.lds-spinner div:nth-child(2) {
|
||||
transform: rotate(30deg);
|
||||
animation-delay: -1s;
|
||||
}
|
||||
.lds-spinner div:nth-child(3) {
|
||||
transform: rotate(60deg);
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
.lds-spinner div:nth-child(4) {
|
||||
transform: rotate(90deg);
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
.lds-spinner div:nth-child(5) {
|
||||
transform: rotate(120deg);
|
||||
animation-delay: -0.7s;
|
||||
}
|
||||
.lds-spinner div:nth-child(6) {
|
||||
transform: rotate(150deg);
|
||||
animation-delay: -0.6s;
|
||||
}
|
||||
.lds-spinner div:nth-child(7) {
|
||||
transform: rotate(180deg);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.lds-spinner div:nth-child(8) {
|
||||
transform: rotate(210deg);
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
.lds-spinner div:nth-child(9) {
|
||||
transform: rotate(240deg);
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-spinner div:nth-child(10) {
|
||||
transform: rotate(270deg);
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
.lds-spinner div:nth-child(11) {
|
||||
transform: rotate(300deg);
|
||||
animation-delay: -0.1s;
|
||||
}
|
||||
.lds-spinner div:nth-child(12) {
|
||||
transform: rotate(330deg);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
@keyframes lds-spinner {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
14
plugin-network/public/spinner.html
Normal file
14
plugin-network/public/spinner.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<div class="lds-spinner">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
211
plugin-network/src/gui.rs
Normal file
211
plugin-network/src/gui.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
///! NetworkManager Authenticator GUI
|
||||
use anyhow::{anyhow, Result};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::LogicalSize;
|
||||
use dioxus_free_icons::icons::fa_regular_icons::{FaEye, FaEyeSlash};
|
||||
use dioxus_free_icons::Icon;
|
||||
use keyboard_types::Code;
|
||||
|
||||
use crate::network::Manager;
|
||||
|
||||
static SPINNER_CSS: &'static str = include_str!("../public/spinner.css");
|
||||
static SPINNER_HTML: &'static str = include_str!("../public/spinner.html");
|
||||
static DEFAULT_CSS_CONTENT: &'static str = include_str!("../public/default.css");
|
||||
|
||||
/// Run GUI Application
|
||||
pub fn run_app(ssid: &str, timeout: u32) {
|
||||
let builder = dioxus_desktop::WindowBuilder::new()
|
||||
.with_title("RMenu - Network Login")
|
||||
.with_inner_size(LogicalSize {
|
||||
width: 400,
|
||||
height: 150,
|
||||
})
|
||||
.with_focused(true)
|
||||
.with_decorations(false)
|
||||
.with_always_on_top(true);
|
||||
dioxus_desktop::launch_with_props(
|
||||
App,
|
||||
AppProp {
|
||||
ssid: ssid.to_owned(),
|
||||
timeout,
|
||||
},
|
||||
dioxus_desktop::Config::new().with_window(builder),
|
||||
);
|
||||
}
|
||||
|
||||
/// Message to send to GUI
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum UserMessage {
|
||||
Nothing,
|
||||
Error(String),
|
||||
Success(String),
|
||||
}
|
||||
|
||||
impl UserMessage {
|
||||
/// Retrieve CSS-Class to use on UserMessage
|
||||
fn css_class(&self) -> &str {
|
||||
match self {
|
||||
Self::Error(_) => "error",
|
||||
Self::Success(_) => "success",
|
||||
Self::Nothing => "none",
|
||||
}
|
||||
}
|
||||
/// Retrieve Message Value
|
||||
fn message(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Nothing => None,
|
||||
Self::Error(msg) => Some(msg.to_owned()),
|
||||
Self::Success(msg) => Some(msg.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Props)]
|
||||
struct AppProp {
|
||||
ssid: String,
|
||||
timeout: u32,
|
||||
}
|
||||
|
||||
//TODO: add auth timeout
|
||||
|
||||
/// Set Cursor/Keyboard Focus onto Input
|
||||
#[inline]
|
||||
fn focus<T>(cx: Scope<T>) {
|
||||
let eval = use_eval(cx);
|
||||
let js = "document.getElementById(`secret`).focus()";
|
||||
let _ = eval(&js);
|
||||
}
|
||||
|
||||
/// Complete Network Connection/Authentication Attempt
|
||||
async fn connect_async(ssid: String, timeout: u32, secret: &str) -> Result<()> {
|
||||
// retrieve access-point from manager
|
||||
let manager = Manager::new().await?.with_timeout(timeout);
|
||||
let access_point = manager
|
||||
.access_points()
|
||||
.into_iter()
|
||||
.find(|a| a.ssid == ssid)
|
||||
.ok_or_else(|| anyhow!("Unable to find Access Point: {ssid:?}"))?;
|
||||
// attempt to establish connection w/ secret
|
||||
log::debug!("Found AccessPoint: {ssid:?} | {:?}", access_point.security);
|
||||
manager.connect(&access_point, Some(secret)).await
|
||||
}
|
||||
|
||||
/// Simple Login GUI Application
|
||||
fn App(cx: Scope<AppProp>) -> Element {
|
||||
let secret = use_state(cx, || String::new());
|
||||
let message = use_state(cx, || UserMessage::Nothing);
|
||||
let show_secret = use_state(cx, || false);
|
||||
let loading = use_state(cx, || false);
|
||||
|
||||
// always ensure focus
|
||||
focus(cx);
|
||||
|
||||
// build keyboard actions event handler
|
||||
let keyboard_controls = move |e: KeyboardEvent| match e.code() {
|
||||
Code::Escape => std::process::exit(0),
|
||||
Code::Enter => {
|
||||
let ssid = cx.props.ssid.to_owned();
|
||||
let timeout = cx.props.timeout.to_owned();
|
||||
let secret = secret.get().to_owned();
|
||||
let message = message.to_owned();
|
||||
let loading = loading.to_owned();
|
||||
loading.set(true);
|
||||
message.set(UserMessage::Nothing);
|
||||
|
||||
cx.spawn(async move {
|
||||
log::info!("connecting to ssid: {ssid:?}");
|
||||
let result = connect_async(ssid, timeout, &secret).await;
|
||||
log::info!("connection result: {result:?}");
|
||||
match result {
|
||||
Ok(_) => {
|
||||
// update message and unlock gui
|
||||
message.set(UserMessage::Success("Connection Complete!".to_owned()));
|
||||
loading.set(false);
|
||||
// exit program after timeout
|
||||
let wait = std::time::Duration::from_secs(1);
|
||||
async_std::task::sleep(wait).await;
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
message.set(UserMessage::Error(format!("Connection Failed: {err:?}")));
|
||||
loading.set(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
// retrieve message details / set input-type / get loading css-class
|
||||
let msg_css = message.css_class();
|
||||
let msg_str = message.message();
|
||||
let input_type = match show_secret.get() {
|
||||
true => "text",
|
||||
false => "password",
|
||||
};
|
||||
let blackout_css = match loading.get() {
|
||||
true => "active",
|
||||
false => "",
|
||||
};
|
||||
|
||||
// complete final rendering
|
||||
cx.render(rsx! {
|
||||
style { DEFAULT_CSS_CONTENT }
|
||||
style { SPINNER_CSS }
|
||||
div {
|
||||
onkeydown: keyboard_controls,
|
||||
label {
|
||||
id: "header",
|
||||
"for": "secret",
|
||||
"Wi-Fi Network {cx.props.ssid:?} Requires a Password"
|
||||
}
|
||||
div {
|
||||
id: "controls",
|
||||
input {
|
||||
id: "secret",
|
||||
value: "{secret}",
|
||||
placeholder: "Password",
|
||||
oninput: move |e| secret.set(e.value.clone()),
|
||||
"type": "{input_type}"
|
||||
}
|
||||
button {
|
||||
id: "icon",
|
||||
onclick: |_| show_secret.modify(|v| !v),
|
||||
match show_secret.get() {
|
||||
true => cx.render(rsx! {
|
||||
Icon {
|
||||
fill: "black",
|
||||
icon: FaEye,
|
||||
}
|
||||
}),
|
||||
false => cx.render(rsx! {
|
||||
Icon {
|
||||
fill: "black",
|
||||
icon: FaEyeSlash,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(msg) = msg_str {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
id: "message",
|
||||
class: "{msg_css}",
|
||||
"{msg}"
|
||||
}
|
||||
})
|
||||
}
|
||||
div {
|
||||
id: "blackout",
|
||||
class: "{blackout_css}",
|
||||
div {
|
||||
id: "spinner",
|
||||
dangerous_inner_html: "{SPINNER_HTML}"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
105
plugin-network/src/main.rs
Normal file
105
plugin-network/src/main.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use rmenu_plugin::Entry;
|
||||
|
||||
mod gui;
|
||||
mod network;
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Commands {
|
||||
ListAccessPoints,
|
||||
Connect { ssid: String, timeout: Option<u32> },
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(propagate_version = true)]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
/// Get Signal Bars based on a signal-value
|
||||
fn get_bars(signal: u8) -> String {
|
||||
if signal >= 80 {
|
||||
return "▂▄▆█".to_owned();
|
||||
}
|
||||
if signal > 50 {
|
||||
return "▂▄▆_".to_owned();
|
||||
}
|
||||
if signal > 30 {
|
||||
return "▂▄__".to_owned();
|
||||
}
|
||||
"▂___".to_owned()
|
||||
}
|
||||
|
||||
/// List AccessPoints using NetworkManager
|
||||
async fn list_aps() -> Result<()> {
|
||||
let exe = std::env::current_exe()?.to_str().unwrap().to_string();
|
||||
// spawn manager and complete scan (if nessesary)
|
||||
let manager = network::Manager::new().await?;
|
||||
if !manager.scanned_recently() {
|
||||
log::info!("Scanning for Access-Points...");
|
||||
manager.scan_wifi().await?;
|
||||
}
|
||||
// retrive access-points and print as entries
|
||||
for ap in manager.access_points() {
|
||||
let star = ap.is_active.then(|| " *").unwrap_or("");
|
||||
let bars = get_bars(ap.signal);
|
||||
let desc = format!("{bars} {}{star}", ap.ssid);
|
||||
let exec = format!("{exe} connect {:?}", ap.ssid);
|
||||
let entry = Entry::new(&desc, &exec, None);
|
||||
println!("{}", serde_json::to_string(&entry).unwrap());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to Connect to the Specified SSID (If Connection Already Exists)
|
||||
async fn try_connect_ap(ssid: String, timeout: Option<u32>) -> Result<bool> {
|
||||
// spawn manager and complete scan (if nessesary)
|
||||
let mut manager = network::Manager::new().await?;
|
||||
if let Some(timeout) = timeout {
|
||||
manager = manager.with_timeout(timeout)
|
||||
}
|
||||
if !manager.scanned_recently() {
|
||||
log::info!("Scanning for Access-Points...");
|
||||
manager.scan_wifi().await?;
|
||||
}
|
||||
// attempt to find access-point
|
||||
let access_point = manager
|
||||
.access_points()
|
||||
.into_iter()
|
||||
.find(|ap| ap.ssid == ssid)
|
||||
.ok_or_else(|| anyhow!("Unable to find Access-Point: {ssid:?}"))?;
|
||||
// if connection already exists, try to connect
|
||||
if access_point.connection.is_some() {
|
||||
log::info!("Attempting Connection to {ssid:?} w/o Password");
|
||||
match manager.connect(&access_point, None).await {
|
||||
Err(err) => log::info!("Connection Failed: {err:?}"),
|
||||
Ok(_) => {
|
||||
log::info!("Connection Successful!");
|
||||
return Ok(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
let cli = Cli::parse();
|
||||
let context = glib::MainContext::default();
|
||||
let command = cli.command.unwrap_or(Commands::ListAccessPoints);
|
||||
match command {
|
||||
Commands::ListAccessPoints => context.block_on(list_aps())?,
|
||||
Commands::Connect { ssid, timeout } => {
|
||||
let connected = context.block_on(try_connect_ap(ssid.clone(), timeout))?;
|
||||
if !connected {
|
||||
log::info!("Spawning GUI to complete AP Login");
|
||||
gui::run_app(&ssid, timeout.unwrap_or(30));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
309
plugin-network/src/network.rs
Normal file
309
plugin-network/src/network.rs
Normal file
|
@ -0,0 +1,309 @@
|
|||
use glib::translate::FromGlib;
|
||||
|
||||
use async_std::task;
|
||||
use futures_channel::oneshot;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use nm::*;
|
||||
|
||||
static SCAN_INTERVAL_MS: u64 = 500;
|
||||
static SCAN_TOTAL_WAIT: u64 = 3;
|
||||
static SCAN_BETWEEN: i64 = 30;
|
||||
|
||||
/// NetworkManager Simplified API
|
||||
#[derive(Debug)]
|
||||
pub struct Manager {
|
||||
client: Client,
|
||||
wifi: DeviceWifi,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
/// AccessPoint Information
|
||||
#[derive(Debug)]
|
||||
pub struct AccessPoint {
|
||||
pub in_use: bool,
|
||||
pub ssid: String,
|
||||
pub rate: u32,
|
||||
pub signal: u8,
|
||||
pub security: String,
|
||||
pub is_active: bool,
|
||||
pub connection: Option<Connection>,
|
||||
}
|
||||
|
||||
// SETTING_WIRELESS_MODE
|
||||
// SETTING_IP4_CONFIG_METHOD_AUTO
|
||||
|
||||
/// Generate a NEW Connection to Use AccessPoint
|
||||
fn new_conn(ap: &AccessPoint, password: Option<&str>) -> Result<SimpleConnection> {
|
||||
let connection = SimpleConnection::new();
|
||||
|
||||
// configure generate connection settings
|
||||
let s_connection = SettingConnection::new();
|
||||
s_connection.set_type(Some(&SETTING_WIRELESS_SETTING_NAME));
|
||||
s_connection.set_id(Some(&ap.ssid));
|
||||
s_connection.set_autoconnect(false);
|
||||
// s_connection.set_interface_name(interface);
|
||||
connection.add_setting(s_connection);
|
||||
|
||||
// configure wireless settings
|
||||
let s_wireless = SettingWireless::new();
|
||||
s_wireless.set_ssid(Some(&(ap.ssid.as_bytes().into())));
|
||||
// s_wireless.set_band(Some("bg"));
|
||||
// s_wireless.set_hidden(false);
|
||||
// s_wireless.set_mode(Some(&SETTING_WIRELESS_MODE));
|
||||
connection.add_setting(s_wireless);
|
||||
|
||||
// configure login settings
|
||||
if let Some(password) = password {
|
||||
//TODO: potentially determine key-mgmt based on ap-security
|
||||
let s_wireless_security = SettingWirelessSecurity::new();
|
||||
s_wireless_security.set_key_mgmt(Some("wpa-psk"));
|
||||
s_wireless_security.set_psk(Some(password));
|
||||
connection.add_setting(s_wireless_security);
|
||||
}
|
||||
|
||||
// assume DHCP Assignment
|
||||
let s_ip4 = SettingIP4Config::new();
|
||||
s_ip4.set_method(Some(&SETTING_IP4_CONFIG_METHOD_AUTO));
|
||||
connection.add_setting(s_ip4);
|
||||
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
/// Exit Security Section to Specify New Password
|
||||
fn edit_conn(conn: &Connection, password: Option<&str>) -> Result<()> {
|
||||
let s_wireless_security = conn
|
||||
.setting_wireless_security()
|
||||
.unwrap_or_else(|| SettingWirelessSecurity::new());
|
||||
s_wireless_security.set_key_mgmt(Some("wpa-psk"));
|
||||
s_wireless_security.set_psk(password);
|
||||
conn.add_setting(s_wireless_security);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait for Connection to Fail or Complete
|
||||
async fn wait_conn(active: &ActiveConnection, timeout: Duration) -> Result<()> {
|
||||
// spawn communication channels
|
||||
let (sender, receiver) = oneshot::channel::<Result<()>>();
|
||||
let sender = Rc::new(RefCell::new(Some(sender)));
|
||||
// spawn state-callback
|
||||
active.connect_state_changed(move |active_connection, state, _| {
|
||||
let sender = sender.clone();
|
||||
let active_connection = active_connection.clone();
|
||||
glib::MainContext::ref_thread_default().spawn_local(async move {
|
||||
let state = unsafe { ActiveConnectionState::from_glib(state as _) };
|
||||
log::debug!("[Connect] Active connection state: {:?}", state);
|
||||
// generate send function
|
||||
let send = move |result| {
|
||||
let sender = sender.borrow_mut().take();
|
||||
if let Some(sender) = sender {
|
||||
sender.send(result).expect("Sender Dropped");
|
||||
}
|
||||
};
|
||||
// handle connection state-changes
|
||||
match state {
|
||||
ActiveConnectionState::Activated => {
|
||||
log::debug!("[Connect] Successfully activated");
|
||||
return send(Ok(()));
|
||||
}
|
||||
ActiveConnectionState::Deactivated => {
|
||||
log::debug!("[Connect] Connection deactivated");
|
||||
match active_connection.connection() {
|
||||
Some(remote_connection) => {
|
||||
let result = remote_connection
|
||||
.delete_future()
|
||||
.await
|
||||
.context("Failed to delete connection");
|
||||
if result.is_err() {
|
||||
return send(result);
|
||||
}
|
||||
return send(Err(anyhow!("Connection Failed (Deactivated)")));
|
||||
}
|
||||
None => {
|
||||
return send(Err(anyhow!(
|
||||
"Failed to get remote connection from active connection"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
});
|
||||
});
|
||||
// wait until state notification is done
|
||||
let result = async_std::future::timeout(timeout, receiver).await;
|
||||
match result {
|
||||
Ok(result) => match result {
|
||||
Ok(res) => res,
|
||||
Err(err) => Err(anyhow!("Connection Cancelled: {err:?}")),
|
||||
},
|
||||
Err(err) => Err(anyhow!("Timeout Reached: {err:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
/// Spawn new Wifi Manager Instance
|
||||
pub async fn new() -> Result<Self> {
|
||||
// get network-manager client
|
||||
let client = Client::new_future()
|
||||
.await
|
||||
.context("Failed to create NM Client")?;
|
||||
// get wifi device if any are available
|
||||
let device = client
|
||||
.devices()
|
||||
.into_iter()
|
||||
.filter(|d| d.device_type() == DeviceType::Wifi)
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Cannot find a Wi-Fi device"))?;
|
||||
// access inner wifi-device object
|
||||
let wifi: DeviceWifi = device
|
||||
.downcast()
|
||||
.map_err(|_| anyhow!("Failed to Access Wi-Fi Device"))?;
|
||||
log::debug!("NetworkManager Connection Established");
|
||||
Ok(Self {
|
||||
client,
|
||||
wifi,
|
||||
timeout: Duration::from_secs(30),
|
||||
})
|
||||
}
|
||||
|
||||
/// Update Manager Timeout and Return Self
|
||||
pub fn with_timeout(mut self, secs: u32) -> Self {
|
||||
self.timeout = Duration::from_secs(secs.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if NetworkManager already scanned recently
|
||||
pub fn scanned_recently(&self) -> bool {
|
||||
let last_ms = self.wifi.last_scan();
|
||||
let now_ms = utils_get_timestamp_msec();
|
||||
let elapsed = (now_ms - last_ms) / 1000;
|
||||
last_ms > 0 && elapsed < SCAN_BETWEEN
|
||||
}
|
||||
|
||||
/// Complete General Wifi-Scan
|
||||
pub async fn scan_wifi(&self) -> Result<()> {
|
||||
// request wifi-scan
|
||||
self.wifi
|
||||
.request_scan_future()
|
||||
.await
|
||||
.context("Failed to Request Wi-Fi Scan")?;
|
||||
// wait until access-points are collected
|
||||
let mut then = SystemTime::now();
|
||||
let mut current = self.wifi.access_points().len();
|
||||
loop {
|
||||
// wait interval for more access-points
|
||||
task::sleep(Duration::from_millis(SCAN_INTERVAL_MS)).await;
|
||||
// check if time has elapsed
|
||||
let now = SystemTime::now();
|
||||
let elapsed = now.duration_since(then)?;
|
||||
if elapsed.as_secs() > SCAN_TOTAL_WAIT {
|
||||
break;
|
||||
}
|
||||
// check if more access-points were discovered
|
||||
let found = self.wifi.access_points().len();
|
||||
if found > current {
|
||||
then = now;
|
||||
current = found;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve Access-Point Information
|
||||
pub fn access_points(&self) -> Vec<AccessPoint> {
|
||||
let conns: Vec<Connection> = self
|
||||
.client
|
||||
.connections()
|
||||
.into_iter()
|
||||
.map(|c| c.upcast())
|
||||
.collect();
|
||||
let mut access: BTreeMap<String, AccessPoint> = BTreeMap::new();
|
||||
let active = self.wifi.active_access_point();
|
||||
for a in self.wifi.access_points() {
|
||||
// retrieve access-point information
|
||||
let rate = a.max_bitrate() / 1000;
|
||||
let signal = a.strength();
|
||||
let ssid = a
|
||||
.ssid()
|
||||
.map(|b| b.escape_ascii().to_string())
|
||||
.unwrap_or_else(|| "--".to_owned());
|
||||
// determine if connection-map should be updated
|
||||
let is_active = active
|
||||
.as_ref()
|
||||
.map(|b| a.bssid() == b.bssid())
|
||||
.unwrap_or(false);
|
||||
if !is_active {
|
||||
if let Some(point) = access.get(&ssid) {
|
||||
if point.rate > rate {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// build security-string
|
||||
let mut security = vec![];
|
||||
let wpa_flags = a.wpa_flags();
|
||||
let wpa2_flags = a.rsn_flags();
|
||||
if !wpa_flags.is_empty() {
|
||||
security.push("WPA1");
|
||||
}
|
||||
if !wpa2_flags.is_empty() {
|
||||
security.push("WPA2");
|
||||
}
|
||||
if wpa2_flags.intersects(_80211ApSecurityFlags::KEY_MGMT_802_1X) {
|
||||
security.push("802.1X");
|
||||
}
|
||||
if security.is_empty() {
|
||||
security.push("--");
|
||||
}
|
||||
// insert access-point
|
||||
access.insert(
|
||||
ssid.to_owned(),
|
||||
AccessPoint {
|
||||
in_use: is_active,
|
||||
ssid,
|
||||
rate,
|
||||
signal,
|
||||
is_active,
|
||||
security: security.join(" ").to_owned(),
|
||||
connection: a.filter_connections(&conns).get(0).cloned(),
|
||||
},
|
||||
);
|
||||
}
|
||||
// move map values into vector and sort by signal-strength
|
||||
let mut points: Vec<AccessPoint> = access.into_values().collect();
|
||||
points.sort_by_key(|a| a.signal);
|
||||
points.reverse();
|
||||
points
|
||||
}
|
||||
|
||||
/// Attempt to Authenticate and Activate Access-Point Connection
|
||||
pub async fn connect(&self, ap: &AccessPoint, password: Option<&str>) -> Result<()> {
|
||||
let device = self.wifi.clone().upcast::<Device>();
|
||||
match &ap.connection {
|
||||
Some(conn) => {
|
||||
edit_conn(conn, password)?;
|
||||
let active_conn = self
|
||||
.client
|
||||
.activate_connection_future(Some(conn), Some(&device), None)
|
||||
.await
|
||||
.context("Failed to activate existing connection")?;
|
||||
wait_conn(&active_conn, self.timeout).await?;
|
||||
}
|
||||
None => {
|
||||
let conn = new_conn(ap, password)?;
|
||||
let active_conn = self
|
||||
.client
|
||||
.add_and_activate_connection_future(Some(&conn), Some(&device), None)
|
||||
.await
|
||||
.context("Failed to add and activate connection")?;
|
||||
wait_conn(&active_conn, self.timeout).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
# Configure RMenu to Spawn Floating in the Center of the Screen
|
||||
|
||||
for_window [app_id="rmenu"] floating enable
|
||||
for_window [app_id="rmenu-network"] floating enable
|
||||
|
|
|
@ -29,6 +29,10 @@ plugins:
|
|||
exec: ["~/.config/rmenu/audio"]
|
||||
cache: false
|
||||
placeholder: "Select an Audio Sink"
|
||||
network:
|
||||
exec: ["~/.config/rmenu/network"]
|
||||
cache: false
|
||||
placeholder: "Connect to the Specified Wi-Fi"
|
||||
|
||||
# custom keybindings
|
||||
keybinds:
|
||||
|
|
Loading…
Reference in a new issue