1use std::collections::HashMap;
7use std::sync::mpsc;
8use std::sync::Arc;
9
10use cliban_tenancy::{Caps, Tenant, TenantManager, User};
11use cliban_tui::remote::RemoteInput;
12use russh::keys::ssh_key::HashAlg;
13use russh::keys::PrivateKey;
14use russh::server::{Auth, ChannelOpenHandle, Handler, Msg, Session};
15use russh::{Channel, ChannelId, MethodKind, MethodSet, Pty};
16
17use crate::config::{ServerConfig, SignupPolicy};
18use crate::ServerError;
19use crate::{commands, shell};
20
21pub fn russh_config(key: PrivateKey) -> Arc<russh::server::Config> {
23 Arc::new(russh::server::Config {
24 keys: vec![key],
25 inactivity_timeout: Some(std::time::Duration::from_secs(3600)),
26 auth_rejection_time: std::time::Duration::from_secs(3),
27 auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)),
28 ..Default::default()
29 })
30}
31
32pub struct AppState {
34 pub manager: TenantManager,
35 pub signup_policy: SignupPolicy,
36 pub signup_token: Option<String>,
37}
38
39impl AppState {
40 pub fn from_config(cfg: &ServerConfig) -> Result<AppState, ServerError> {
43 fn cap(v: u32) -> i64 {
44 if v == 0 {
45 i64::MAX
46 } else {
47 i64::from(v)
48 }
49 }
50 let manager = TenantManager::open(
51 &cfg.data_dir,
52 Caps {
53 max_tenants_per_user: cap(cfg.max_tenants_per_key),
54 max_tenants_global: cap(cfg.max_tenants),
55 },
56 )?;
57 Ok(AppState {
58 manager,
59 signup_policy: cfg.signup_policy,
60 signup_token: cfg.signup_token.clone(),
61 })
62 }
63}
64
65pub struct KeyInfo {
67 pub fingerprint: String,
69 pub openssh: String,
71 pub username: String,
73 pub user: Option<User>,
75}
76
77pub struct ClibandServer {
79 pub state: Arc<AppState>,
80}
81
82impl russh::server::Server for ClibandServer {
83 type Handler = SessionHandler;
84
85 fn new_client(&mut self, _peer: Option<std::net::SocketAddr>) -> SessionHandler {
86 SessionHandler {
87 state: self.state.clone(),
88 key: None,
89 ptys: HashMap::new(),
90 boards: HashMap::new(),
91 }
92 }
93}
94
95pub struct SessionHandler {
97 state: Arc<AppState>,
98 key: Option<KeyInfo>,
100 ptys: HashMap<ChannelId, (u16, u16)>,
102 boards: HashMap<ChannelId, mpsc::Sender<RemoteInput>>,
105}
106
107fn dim(v: u32) -> u16 {
109 u16::try_from(v).unwrap_or(u16::MAX).max(1)
110}
111
112fn pubkey_only() -> Auth {
114 let mut methods = MethodSet::empty();
115 methods.push(MethodKind::PublicKey);
116 Auth::Reject {
117 proceed_with_methods: Some(methods),
118 partial_success: false,
119 }
120}
121
122impl Handler for SessionHandler {
123 type Error = ServerError;
124
125 async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> {
126 Ok(pubkey_only())
127 }
128
129 async fn auth_password(&mut self, _user: &str, _password: &str) -> Result<Auth, Self::Error> {
130 Ok(pubkey_only())
131 }
132
133 async fn auth_publickey(
137 &mut self,
138 user: &str,
139 key: &russh::keys::ssh_key::PublicKey,
140 ) -> Result<Auth, Self::Error> {
141 let fingerprint = key.fingerprint(HashAlg::Sha256).to_string();
142 let openssh = key.to_openssh()?;
143 let known = self
144 .state
145 .manager
146 .registry()
147 .user_for_pubkey(&fingerprint)?;
148 self.key = Some(KeyInfo {
149 fingerprint,
150 openssh,
151 username: user.to_string(),
152 user: known,
153 });
154 Ok(Auth::Accept)
155 }
156
157 async fn channel_open_session(
158 &mut self,
159 _channel: Channel<Msg>,
160 reply: ChannelOpenHandle,
161 _session: &mut Session,
162 ) -> Result<(), Self::Error> {
163 reply.accept().await;
164 Ok(())
165 }
166
167 async fn pty_request(
168 &mut self,
169 channel: ChannelId,
170 _term: &str,
171 col_width: u32,
172 row_height: u32,
173 _pix_width: u32,
174 _pix_height: u32,
175 _modes: &[(Pty, u32)],
176 session: &mut Session,
177 ) -> Result<(), Self::Error> {
178 self.ptys.insert(channel, (dim(col_width), dim(row_height)));
179 session.channel_success(channel)?;
180 Ok(())
181 }
182
183 async fn window_change_request(
184 &mut self,
185 channel: ChannelId,
186 col_width: u32,
187 row_height: u32,
188 _pix_width: u32,
189 _pix_height: u32,
190 _session: &mut Session,
191 ) -> Result<(), Self::Error> {
192 let size = (dim(col_width), dim(row_height));
193 self.ptys.insert(channel, size);
196 if let Some(tx) = self.boards.get(&channel) {
197 let _ = tx.send(RemoteInput::Resize(size.0, size.1));
198 }
199 Ok(())
200 }
201
202 async fn shell_request(
206 &mut self,
207 channel: ChannelId,
208 session: &mut Session,
209 ) -> Result<(), Self::Error> {
210 session.channel_success(channel)?;
211
212 let Some(user) = self.key.as_ref().and_then(|k| k.user.clone()) else {
215 let text = commands::USAGE.replace('\n', "\r\n");
217 session.data(channel, text.into_bytes())?;
218 session.exit_status_request(channel, 1)?;
219 session.eof(channel)?;
220 session.close(channel)?;
221 return Ok(());
222 };
223
224 let Some(&size) = self.ptys.get(&channel) else {
225 session.data(channel, shell::NO_TTY.as_bytes().to_vec())?;
226 session.exit_status_request(channel, 1)?;
227 session.eof(channel)?;
228 session.close(channel)?;
229 return Ok(());
230 };
231
232 if self.boards.contains_key(&channel) {
233 return Ok(()); }
235
236 let tenants: Vec<Tenant> = match self.state.manager.registry().tenants_for_user(user.id) {
237 Ok(ts) => ts.into_iter().map(|(t, _)| t).collect(),
238 Err(e) => {
239 eprintln!("cliband: shell: {e}");
242 session.data(channel, b"cliband: internal error\r\n".to_vec())?;
243 session.exit_status_request(channel, 1)?;
244 session.eof(channel)?;
245 session.close(channel)?;
246 return Ok(());
247 }
248 };
249 if tenants.is_empty() {
250 let text = format!(
251 "no boards yet — create or join one first:\r\n\r\n{}",
252 commands::USAGE.replace('\n', "\r\n")
253 );
254 session.data(channel, text.into_bytes())?;
255 session.exit_status_request(channel, 1)?;
256 session.eof(channel)?;
257 session.close(channel)?;
258 return Ok(());
259 }
260
261 let (tx, rx) = mpsc::channel();
262 self.boards.insert(channel, tx);
263 let task = shell::BoardTask {
264 rt: tokio::runtime::Handle::current(),
265 state: self.state.clone(),
266 handle: session.handle(),
267 channel,
268 size,
269 tenants,
270 input: rx,
271 };
272 tokio::task::spawn_blocking(move || shell::run_board(task));
273 Ok(())
274 }
275
276 async fn exec_request(
277 &mut self,
278 channel: ChannelId,
279 data: &[u8],
280 session: &mut Session,
281 ) -> Result<(), Self::Error> {
282 session.channel_success(channel)?;
283 let line = String::from_utf8_lossy(data);
284 let out = match self.key.as_mut() {
285 Some(key) => commands::run(&self.state, key, &line),
286 None => commands::Output {
288 text: commands::USAGE.to_string(),
289 exit: 1,
290 },
291 };
292 session.data(channel, out.text.into_bytes())?;
293 session.exit_status_request(channel, out.exit)?;
294 session.eof(channel)?;
295 session.close(channel)?;
296 Ok(())
297 }
298
299 async fn data(
301 &mut self,
302 channel: ChannelId,
303 data: &[u8],
304 _session: &mut Session,
305 ) -> Result<(), Self::Error> {
306 if let Some(tx) = self.boards.get(&channel) {
307 let _ = tx.send(RemoteInput::Bytes(data.to_vec()));
308 }
309 Ok(())
310 }
311
312 async fn channel_eof(
313 &mut self,
314 channel: ChannelId,
315 _session: &mut Session,
316 ) -> Result<(), Self::Error> {
317 self.boards.remove(&channel);
318 Ok(())
319 }
320
321 async fn channel_close(
322 &mut self,
323 channel: ChannelId,
324 _session: &mut Session,
325 ) -> Result<(), Self::Error> {
326 self.boards.remove(&channel);
327 self.ptys.remove(&channel);
328 Ok(())
329 }
330}