mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-02-15 06:35:04 +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-run",
|
||||||
"plugin-desktop",
|
"plugin-desktop",
|
||||||
"plugin-audio",
|
"plugin-audio",
|
||||||
|
"plugin-network",
|
||||||
]
|
]
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -21,6 +21,7 @@ deploy:
|
||||||
cp -vf ./target/release/desktop ${DEST}/drun
|
cp -vf ./target/release/desktop ${DEST}/drun
|
||||||
cp -vf ./target/release/run ${DEST}/run
|
cp -vf ./target/release/run ${DEST}/run
|
||||||
cp -vf ./target/release/audio ${DEST}/audio
|
cp -vf ./target/release/audio ${DEST}/audio
|
||||||
|
cp -vf ./target/release/rmenu-network ${DEST}/network
|
||||||
cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml
|
cp -vf ./rmenu/public/config.yaml ${DEST}/config.yaml
|
||||||
|
|
||||||
build: build-rmenu build-plugins
|
build: build-rmenu build-plugins
|
||||||
|
@ -32,3 +33,4 @@ build-plugins:
|
||||||
${CARGO} build -p run ${FLAGS}
|
${CARGO} build -p run ${FLAGS}
|
||||||
${CARGO} build -p desktop ${FLAGS}
|
${CARGO} build -p desktop ${FLAGS}
|
||||||
${CARGO} build -p audio ${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
|
# Configure RMenu to Spawn Floating in the Center of the Screen
|
||||||
|
|
||||||
for_window [app_id="rmenu"] floating enable
|
for_window [app_id="rmenu"] floating enable
|
||||||
|
for_window [app_id="rmenu-network"] floating enable
|
||||||
|
|
|
@ -29,6 +29,10 @@ plugins:
|
||||||
exec: ["~/.config/rmenu/audio"]
|
exec: ["~/.config/rmenu/audio"]
|
||||||
cache: false
|
cache: false
|
||||||
placeholder: "Select an Audio Sink"
|
placeholder: "Select an Audio Sink"
|
||||||
|
network:
|
||||||
|
exec: ["~/.config/rmenu/network"]
|
||||||
|
cache: false
|
||||||
|
placeholder: "Connect to the Specified Wi-Fi"
|
||||||
|
|
||||||
# custom keybindings
|
# custom keybindings
|
||||||
keybinds:
|
keybinds:
|
||||||
|
|
Loading…
Reference in a new issue