mirror of
https://gitlab.com/fabinfra/fabaccess/sute.git
synced 2025-03-11 22:21:52 +01:00
After way too long, the first version that does at least something
This commit is contained in:
commit
0935bbcb1d
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "schema"]
|
||||
path = schema
|
||||
url = ../Diflouroborane/schema
|
1
schema
Submodule
1
schema
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 3392b9ac25eba7225212a1220c2d2e9e2bb3ebd9
|
113
src/app.rs
Normal file
113
src/app.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use futures::prelude::*;
|
||||
use futures_signals::signal::{Mutable, Signal, MutableSignalCloned};
|
||||
|
||||
use termion::event::Key;
|
||||
|
||||
use crate::input::Inputs;
|
||||
use crate::schema::Api;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum Window {
|
||||
Main,
|
||||
Help,
|
||||
}
|
||||
|
||||
enum ConnState<F> {
|
||||
Connecting(F),
|
||||
Connected(Api)
|
||||
}
|
||||
|
||||
/// Application state struct
|
||||
pub struct Sute<S, F> {
|
||||
|
||||
// TODO: BE SMART. Inputs change the state, resize signals change the state, futures completing
|
||||
// change the state.
|
||||
|
||||
pub state: Mutable<SuteState>,
|
||||
statesig: MutableSignalCloned<SuteState>,
|
||||
signal: S,
|
||||
inputs: Inputs,
|
||||
api: Option<ConnState<F>>,
|
||||
}
|
||||
|
||||
impl<S: Unpin, F: Unpin> Sute<S, F> {
|
||||
pub fn new(s: S, api: F) -> Self {
|
||||
let inputs = Inputs::new();
|
||||
let state = Mutable::new(SuteState::new());
|
||||
|
||||
Self {
|
||||
statesig: state.signal_cloned(),
|
||||
state: state,
|
||||
signal: s,
|
||||
inputs: inputs,
|
||||
api: Some(ConnState::Connecting(api)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_resize(&mut self, new_size: (u16,u16)) {
|
||||
(self.state.lock_mut()).size = new_size;
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, key: Key) {
|
||||
// TODO modify signal implementation so we don't have to modify the state on all ticks.
|
||||
let mut state = self.state.lock_mut();
|
||||
state.tick = state.tick + 1;
|
||||
match key {
|
||||
Key::Char('q') => state.running = false,
|
||||
Key::Char('?') => state.active_win = Window::Help,
|
||||
Key::Esc => {
|
||||
if state.active_win == Window::Help {
|
||||
state.active_win = Window::Main;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Signal<Item=(u16,u16)> + Unpin, F: Future<Output=Api> + Unpin> Signal for Sute<S, F> {
|
||||
type Item = SuteState;
|
||||
fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
if let Poll::Ready(Some(key)) = Pin::new(&mut self.inputs).poll_next(cx) {
|
||||
self.handle_input(key);
|
||||
}
|
||||
if let Poll::Ready(Some(size)) = Pin::new(&mut self.signal).poll_change(cx) {
|
||||
self.handle_resize(size);
|
||||
}
|
||||
if let Some(ConnState::Connecting(mut apif)) = self.api.take() {
|
||||
if let Poll::Ready(api) = Pin::new(&mut apif).poll(cx) {
|
||||
self.api = Some(ConnState::Connected(api));
|
||||
} else {
|
||||
self.api = Some(ConnState::Connecting(apif));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO chunk this?
|
||||
|
||||
Pin::new(&mut self.statesig).poll_change(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SuteState {
|
||||
pub active_win: Window,
|
||||
pub size: (u16,u16),
|
||||
pub running: bool,
|
||||
pub tick: usize,
|
||||
pub server: Option<String>,
|
||||
}
|
||||
|
||||
impl SuteState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_win: Window::Main,
|
||||
size: (80,20),
|
||||
running: true,
|
||||
tick: 0,
|
||||
server: None
|
||||
}
|
||||
}
|
||||
}
|
7
src/banner.rs
Normal file
7
src/banner.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub const BANNER: &str = "
|
||||
. _ _
|
||||
.... ... ... .||. .... / V \\
|
||||
||. ' || || || .|...|| >-(_)-<
|
||||
. '|.. || || || || \\_/ \\_/
|
||||
|'..|' '|..'|. '|.' '|...' ̔-´
|
||||
";
|
4
src/config.rs
Normal file
4
src/config.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub server: String,
|
||||
}
|
45
src/input.rs
Normal file
45
src/input.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use std::io;
|
||||
use std::thread;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use futures::Stream;
|
||||
use futures::channel::mpsc;
|
||||
use futures::SinkExt;
|
||||
|
||||
pub struct Inputs {
|
||||
rx: mpsc::Receiver<Key>,
|
||||
hndl: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Inputs {
|
||||
pub fn new() -> Self {
|
||||
let (mut tx, rx) = mpsc::channel(64);
|
||||
|
||||
let hndl = thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
let keys = stdin.keys();
|
||||
for key in keys {
|
||||
if key.is_err() {
|
||||
break;
|
||||
}
|
||||
if let Err(_) = smol::block_on(tx.send(key.unwrap())) {
|
||||
break; // and thus stop the thread
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self { rx, hndl }
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Inputs {
|
||||
type Item = Key;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
Pin::new(&mut self.rx).poll_next(cx)
|
||||
}
|
||||
}
|
72
src/main.rs
Normal file
72
src/main.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use std::io;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tui::backend::{Backend, TermionBackend};
|
||||
use tui::Terminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::input::TermRead;
|
||||
use termion::event::Key;
|
||||
|
||||
use futures::StreamExt;
|
||||
use futures_signals::signal::SignalExt;
|
||||
|
||||
use clap::{App, Arg};
|
||||
|
||||
mod banner;
|
||||
mod config;
|
||||
mod app;
|
||||
mod input;
|
||||
mod util;
|
||||
mod ui;
|
||||
mod schema;
|
||||
|
||||
use banner::BANNER;
|
||||
|
||||
fn main() -> Result<(), io::Error> {
|
||||
|
||||
let matches = App::new("sute 🌸")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.author(env!("CARGO_PKG_AUTHORS"))
|
||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||
.usage("Press `?` in the GUI to see keybindings")
|
||||
.before_help(BANNER)
|
||||
.arg(Arg::with_name("config")
|
||||
.short("c")
|
||||
.long("config")
|
||||
.help("Specify configuration file path")
|
||||
.takes_value(true))
|
||||
.arg(Arg::with_name("server")
|
||||
.short("s")
|
||||
.long("server")
|
||||
.help("Connect to the specified address[:port] as server")
|
||||
.takes_value(true))
|
||||
.get_matches();
|
||||
|
||||
let server = matches.value_of("server").unwrap_or("localhost");
|
||||
|
||||
let stdout = io::stdout().into_raw_mode()?;
|
||||
let backend = TermionBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let resize = util::Resize::new()?;
|
||||
let api = schema::Api::connect(server);
|
||||
let app = app::Sute::new(resize, Box::pin(api));
|
||||
|
||||
let mut stream = app.to_stream();
|
||||
loop {
|
||||
if let Some(mut state) = smol::block_on(stream.next()) {
|
||||
if !state.running {
|
||||
break;
|
||||
}
|
||||
|
||||
terminal.draw(|f| ui::draw_ui(f, &mut state))?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
terminal.show_cursor()?;
|
||||
|
||||
Ok(())
|
||||
}
|
119
src/schema.rs
Normal file
119
src/schema.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use std::ffi::CStr;
|
||||
|
||||
use futures::prelude::*;
|
||||
|
||||
use smol::io;
|
||||
use smol::net::{TcpStream, AsyncToSocketAddrs};
|
||||
use smol::LocalExecutor;
|
||||
use smol::Task;
|
||||
|
||||
use rsasl::SASL;
|
||||
|
||||
use capnp_rpc::{twoparty, RpcSystem, rpc_twoparty_capnp};
|
||||
|
||||
mod auth_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/schema/auth_capnp.rs"));
|
||||
}
|
||||
mod connection_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/schema/connection_capnp.rs"));
|
||||
}
|
||||
mod api_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/schema/api_capnp.rs"));
|
||||
}
|
||||
|
||||
const PLAIN: *const libc::c_char = b"PLAIN" as *const u8 as *const libc::c_char;
|
||||
|
||||
pub struct Api {
|
||||
stream: TcpStream
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub fn new(stream: TcpStream) -> Self {
|
||||
Self { stream }
|
||||
}
|
||||
pub fn connect<A: AsyncToSocketAddrs>(addr: A) -> impl Future<Output=Api> {
|
||||
let f = async {
|
||||
let mut stream = TcpStream::connect(addr).await.unwrap();
|
||||
println!("Doing a hecking connect!");
|
||||
|
||||
let mut api = Api::new(stream);
|
||||
|
||||
api.handshake().await.unwrap();
|
||||
if api.authenticate().await.unwrap() {
|
||||
println!("Authentication successful");
|
||||
} else {
|
||||
println!("Authentication failed!");
|
||||
}
|
||||
|
||||
api
|
||||
};
|
||||
|
||||
f
|
||||
}
|
||||
async fn handshake(&mut self) -> Result<(), io::Error> {
|
||||
let host = "localhost";
|
||||
let program = format!("{}-{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||
let version = (0u32,1u32);
|
||||
|
||||
let mut outer = capnp::message::Builder::new_default();
|
||||
let mut builder = outer.init_root::<connection_capnp::message::Builder>().init_greet();
|
||||
|
||||
builder.set_host(host);
|
||||
builder.set_major(version.0);
|
||||
builder.set_minor(version.1);
|
||||
builder.set_program(program);
|
||||
|
||||
capnp_futures::serialize::write_message(&mut self.stream, outer).await.unwrap();
|
||||
|
||||
println!("{}", program);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Authenticate to the server. Returns true on success, false on error
|
||||
async fn authenticate(&mut self) -> Result<bool, io::Error> {
|
||||
let mut sasl = SASL::new().unwrap();
|
||||
let plain = std::ffi::CString::new("PLAIN").unwrap();
|
||||
let mut sess = sasl.client_start(&plain).unwrap();
|
||||
sess.set_property(rsasl::Property::GSASL_AUTHID, b"testuser");
|
||||
sess.set_property(rsasl::Property::GSASL_PASSWORD, b"testpass");
|
||||
|
||||
if let rsasl::Step::Done(data) = sess.step(&[]).unwrap() {
|
||||
self.send_authentication_request("PLAIN", Some(&data)).await;
|
||||
} else {
|
||||
println!("Sasl said moar data");
|
||||
}
|
||||
|
||||
Ok(self.receive_challenge().await?)
|
||||
}
|
||||
|
||||
fn send_authentication_request(&mut self, mech: &str, init: Option<&[u8]>) -> impl Future<Output=()> {
|
||||
let mut outer = capnp::message::Builder::new_default();
|
||||
let mut builder = outer.init_root::<connection_capnp::message::Builder>()
|
||||
.init_auth()
|
||||
.init_request();
|
||||
builder.set_mechanism(mech);
|
||||
|
||||
if let Some(data) = init {
|
||||
builder.init_initial_response().set_initial(data);
|
||||
}
|
||||
|
||||
let stream = self.stream.clone();
|
||||
capnp_futures::serialize::write_message(stream, outer).map(|r| r.unwrap())
|
||||
}
|
||||
|
||||
async fn receive_challenge(&mut self) -> Result<bool, io::Error> {
|
||||
let message = capnp_futures::serialize::read_message(&mut self.stream, capnp::message::ReaderOptions::default()).await.unwrap().unwrap();
|
||||
let m = message.get_root::<connection_capnp::message::Reader>().unwrap();
|
||||
|
||||
if let Ok(connection_capnp::message::Which::Auth(Ok(r))) = m.which() {
|
||||
if let Ok(auth_capnp::auth_message::Outcome(Ok(r))) = r.which() {
|
||||
if let Ok(auth_capnp::outcome::Result::Successful) = r.get_result() {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
47
src/ui/mod.rs
Normal file
47
src/ui/mod.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Layout, Direction, Constraint, Rect},
|
||||
widgets::{Paragraph, Block, Borders},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::app::SuteState;
|
||||
|
||||
pub fn draw_ui<B: Backend>(f: &mut Frame<B>, app: &mut SuteState) {
|
||||
let outer_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(6),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(3),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
draw_header(f, app, outer_layout[0]);
|
||||
draw_main(f, app, outer_layout[1]);
|
||||
draw_command_line(f, app, outer_layout[2]);
|
||||
}
|
||||
|
||||
fn draw_header<B: Backend>(f: &mut Frame<B>, app: &mut SuteState, layout_chunk: Rect) {
|
||||
f.render_widget(Block::default()
|
||||
.title("Header")
|
||||
.borders(Borders::ALL), layout_chunk);
|
||||
}
|
||||
|
||||
fn draw_main<B: Backend>(f: &mut Frame<B>, app: &mut SuteState, layout_chunk: Rect) {
|
||||
let chunk = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
|
||||
.split(layout_chunk);
|
||||
|
||||
f.render_widget(Paragraph::new(app.server.as_ref().map(|s| s.as_str()).unwrap_or("Not connected")), chunk[0]);
|
||||
f.render_widget(Paragraph::new("Main"), chunk[1]);
|
||||
}
|
||||
|
||||
fn draw_command_line<B: Backend>(f: &mut Frame<B>, app: &mut SuteState, layout_chunk: Rect) {
|
||||
f.render_widget(Block::default()
|
||||
.title("Command line")
|
||||
.borders(Borders::ALL), layout_chunk);
|
||||
f.render_widget(Paragraph::new(">"), layout_chunk);
|
||||
}
|
45
src/util.rs
Normal file
45
src/util.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
|
||||
use futures::prelude::*;
|
||||
use futures::ready;
|
||||
use futures::task::{Context, Poll};
|
||||
use futures_signals::signal::Signal;
|
||||
|
||||
|
||||
pub struct Resize {
|
||||
inner: smol::net::unix::UnixStream,
|
||||
size: (u16, u16),
|
||||
}
|
||||
|
||||
impl Resize {
|
||||
pub fn new() -> Result<Self, io::Error> {
|
||||
let size = termion::terminal_size()?;
|
||||
let (a, inner) = smol::net::unix::UnixStream::pair()?;
|
||||
signal_hook::pipe::register(signal_hook::SIGWINCH, a)?;
|
||||
Ok(Self { inner, size })
|
||||
}
|
||||
}
|
||||
|
||||
impl Signal for Resize {
|
||||
type Item = (u16,u16);
|
||||
|
||||
fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
if let Err(e) = ready!(AsyncRead::poll_read(Pin::new(&mut self.inner), cx, &mut [0])) {
|
||||
println!("Error in checking signals: {}", e);
|
||||
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
match termion::terminal_size() {
|
||||
Ok(s) => {
|
||||
self.size = s;
|
||||
Poll::Ready(Some(s))
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error in checking signals: {}", e);
|
||||
Poll::Ready(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user