init: initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
.direnv
|
||||
1996
Cargo.lock
generated
Normal file
1996
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "publik"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.11.7"
|
||||
log = "0.4.26"
|
||||
russh = "0.51.1"
|
||||
thiserror = "2.0.12"
|
||||
tokio = "1.44.1"
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Himadri Bhattacharjee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
57
flake.lock
generated
Normal file
57
flake.lock
generated
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 0,
|
||||
"narHash": "sha256-C7jVfohcGzdZRF6DO+ybyG/sqpo1h6bZi9T56sxLy+k=",
|
||||
"path": "/nix/store/alzxn3hjisc84hrlv44x6hni48crww26-source",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
27
flake.nix
Normal file
27
flake.nix
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
description = "devshell for github:lavafroth/sweet";
|
||||
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell rec {
|
||||
packages = with pkgs; [
|
||||
stdenv.cc.cc.lib
|
||||
];
|
||||
|
||||
LD_LIBRARY_PATH = "${nixpkgs.lib.makeLibraryPath packages}";
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
204
src/main.rs
Normal file
204
src/main.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use russh::keys::{PublicKey, ssh_key, ssh_key::rand_core::OsRng};
|
||||
use russh::server::{self, Msg, Server as _, Session};
|
||||
use russh::{Channel, ChannelId, CryptoVec};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::builder()
|
||||
.filter_level(log::LevelFilter::Debug)
|
||||
.init();
|
||||
|
||||
let mut methods = russh::MethodSet::empty();
|
||||
methods.push(russh::MethodKind::PublicKey);
|
||||
|
||||
let keychain = read_authfile(Path::new("./authfile")).await.unwrap();
|
||||
|
||||
let config = russh::server::Config {
|
||||
inactivity_timeout: Some(std::time::Duration::from_secs(3600)),
|
||||
auth_rejection_time: std::time::Duration::from_secs(3),
|
||||
auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)),
|
||||
methods,
|
||||
keys: vec![
|
||||
russh::keys::PrivateKey::random(&mut OsRng, russh::keys::Algorithm::Ed25519).unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let config = Arc::new(config);
|
||||
let mut sh = Server {
|
||||
keychain,
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
id: 0,
|
||||
user_map: Arc::new(Mutex::new(HashMap::default())),
|
||||
};
|
||||
sh.run_on_address(config, ("0.0.0.0", 2222)).await.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AuthFileError {
|
||||
#[error("unable to read authorization file")]
|
||||
FileNotReadable(#[from] std::io::Error),
|
||||
#[error("failed to parse public key")]
|
||||
PublicKeyParsingError(#[from] russh::keys::ssh_key::Error),
|
||||
#[error("invalid role specified in authorization file at line: {0}")]
|
||||
InvalidRole(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Role {
|
||||
Normal,
|
||||
Super,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthorizedEntity {
|
||||
name: String,
|
||||
role: Role,
|
||||
key: PublicKey,
|
||||
}
|
||||
|
||||
async fn read_authfile(path: &Path) -> Result<Vec<Arc<AuthorizedEntity>>, AuthFileError> {
|
||||
let handle = std::fs::File::open(path)?;
|
||||
let reader = BufReader::new(handle);
|
||||
let mut keys = vec![];
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let key = PublicKey::from_openssh(&line)?;
|
||||
|
||||
let comment = key.comment();
|
||||
let (name, role) = match comment.rsplit_once(":") {
|
||||
Some((name, "admin")) => (name, Role::Super),
|
||||
None => (comment, Role::Normal),
|
||||
_ => {
|
||||
return Err(AuthFileError::InvalidRole(comment.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let authorized_entity = AuthorizedEntity {
|
||||
name: name.to_string(),
|
||||
role,
|
||||
key,
|
||||
};
|
||||
keys.push(authorized_entity.into());
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Server {
|
||||
keychain: Vec<Arc<AuthorizedEntity>>,
|
||||
clients: Arc<Mutex<HashMap<usize, (ChannelId, russh::server::Handle)>>>,
|
||||
user_map: Arc<Mutex<HashMap<usize, Arc<AuthorizedEntity>>>>,
|
||||
id: usize,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
async fn post(&mut self, data: CryptoVec) {
|
||||
let mut clients = self.clients.lock().await;
|
||||
for (id, (channel, s)) in clients.iter_mut() {
|
||||
if *id != self.id {
|
||||
let _ = s.data(*channel, data.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn entity(&mut self) -> Arc<AuthorizedEntity> {
|
||||
self.user_map.lock().await.get(&self.id).cloned().unwrap()
|
||||
}
|
||||
|
||||
async fn announce(&mut self) {
|
||||
let entity = self.entity().await;
|
||||
let data = CryptoVec::from(format!(
|
||||
"{} with {:?} privileges has joined the lounge\r\n",
|
||||
entity.name, entity.role
|
||||
));
|
||||
self.post(data.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl server::Server for Server {
|
||||
type Handler = Self;
|
||||
fn new_client(&mut self, _: Option<std::net::SocketAddr>) -> Self {
|
||||
let s = self.clone();
|
||||
self.id += 1;
|
||||
s
|
||||
}
|
||||
fn handle_session_error(&mut self, _error: <Self::Handler as russh::server::Handler>::Error) {
|
||||
eprintln!("Session error: {:#?}", _error);
|
||||
}
|
||||
}
|
||||
|
||||
impl server::Handler for Server {
|
||||
type Error = russh::Error;
|
||||
|
||||
async fn channel_open_session(
|
||||
&mut self,
|
||||
channel: Channel<Msg>,
|
||||
session: &mut Session,
|
||||
) -> Result<bool, Self::Error> {
|
||||
{
|
||||
let mut clients = self.clients.lock().await;
|
||||
clients.insert(self.id, (channel.id(), session.handle()));
|
||||
}
|
||||
self.announce().await;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn auth_publickey(
|
||||
&mut self,
|
||||
_: &str,
|
||||
key: &ssh_key::PublicKey,
|
||||
) -> Result<server::Auth, Self::Error> {
|
||||
// Search for the key in our keychain
|
||||
if let Some(entity) = self
|
||||
.keychain
|
||||
.iter()
|
||||
.find(|entity| entity.key.key_data() == key.key_data())
|
||||
{
|
||||
{
|
||||
let mut user_map = self.user_map.lock().await;
|
||||
user_map.insert(self.id, entity.clone());
|
||||
}
|
||||
return Ok(server::Auth::Accept);
|
||||
}
|
||||
Ok(server::Auth::reject())
|
||||
}
|
||||
|
||||
async fn data(
|
||||
&mut self,
|
||||
channel: ChannelId,
|
||||
data: &[u8],
|
||||
session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
// Sending Ctrl+C ends the session and disconnects the client
|
||||
if data == [3] {
|
||||
return Err(russh::Error::Disconnect);
|
||||
}
|
||||
|
||||
let data = CryptoVec::from(format!(
|
||||
"[{}]: {}\r\n",
|
||||
self.entity().await.name,
|
||||
String::from_utf8_lossy(data)
|
||||
));
|
||||
self.post(data.clone()).await;
|
||||
session.data(channel, data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
let id = self.id;
|
||||
let clients = self.clients.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut clients = clients.lock().await;
|
||||
clients.remove(&id);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user