Skip to content
773 changes: 731 additions & 42 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ futures = "*"
headers = "*"
image = "*"
rand = "*"
reqwest = { version = "0.12.9", optional = true }
rust-embed = "*"
serde = { version = "*", features = ["derive"] }
tokio = { version = "*", features = ["full"] }
Expand All @@ -30,6 +31,9 @@ tracing-subscriber = { version = "*", features = ["env-filter"] }
default = ["text", "binary"]
# contains all the parsers
all = ["text", "binary"]
# utilities
auth = ["dep:reqwest"]
# protocols
text = ["dep:atoi_radix10"]
binary = []

Expand Down
67 changes: 67 additions & 0 deletions src/blame.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#[cfg(feature = "auth")]
use std::cell::SyncUnsafeCell;

#[cfg(feature = "auth")]
use image::{GenericImageView, Rgba};

#[cfg(feature = "auth")]
use crate::Coordinate;

#[cfg(feature = "auth")]
pub(crate) type User = u32;

#[cfg(feature = "auth")]
pub(crate) struct BlameMap {
size_x: usize,
size_y: usize,
cells: SyncUnsafeCell<Vec<User>>,
}

#[cfg(feature = "auth")]
impl BlameMap {
fn index(&self, x: Coordinate, y: Coordinate) -> Option<usize> {
let x = x as usize;
let y = y as usize;
if x >= self.size_x || y >= self.size_y {
return None;
}
Some((y * self.size_x) + x)
}

pub(crate) fn new(size_x: usize, size_y: usize) -> Self {
let mut cells = Vec::with_capacity(size_x * size_y);
for _y in 0..size_y {
for _x in 0..size_x {
cells.push(0);
}
}
BlameMap {
size_x,
size_y,
cells: cells.into(),
}
}

pub(crate) fn set_blame(&self, x: Coordinate, y: Coordinate, user: User) {
match self.index(x, y) {
None => (),
Some(idx) => unsafe { (*self.cells.get())[idx] = user },
}
}
}

#[cfg(feature = "auth")]
impl GenericImageView for BlameMap {
type Pixel = Rgba<u8>;

fn dimensions(&self) -> (u32, u32) {
(self.size_x as u32, self.size_y as u32)
}

fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel {
let idx = (y as usize) * self.size_x + (x as usize);
let pixel = unsafe { (*self.cells.get())[idx] };
let [r, g, b, a] = pixel.to_be_bytes();
Rgba::from([r, g, b, a])
}
}
1 change: 1 addition & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub const WEB_HOST: &str = "127.0.0.1:3000";
pub const IMAGE_SAVE_INTERVAL: Duration = Duration::from_secs(5);
pub const JPEG_UPDATE_INTERVAL: Duration = Duration::from_millis(17);
pub const WEB_UPDATE_INTERVAL: Duration = Duration::from_millis(50);
pub const AUTH_SERVER_URL: &str = "https://test.auth/";

pub const HELP_TEXT: &[u8] = b"Flurry is a pixelflut implementation, this means you can use commands to get and set pixels in the canvas
SIZE returns the size of the canvas
Expand Down
64 changes: 61 additions & 3 deletions src/flutclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@ use std::{
sync::Arc,
};

use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter};

#[cfg(feature = "auth")]
use crate::{blame::User, config::AUTH_SERVER_URL};
use crate::{
get_pixel,
grid::{self, Flut},
increment_counter,
protocols::{BinaryParser, IOProtocol, Parser, Responder, TextParser},
set_pixel_rgba, Canvas, Color, Command, Coordinate, Protocol, ProtocolStatus, Response,
};
#[cfg(feature = "auth")]
use bytes::Buf;
#[cfg(feature = "auth")]
use reqwest::{Client, ClientBuilder};
#[cfg(feature = "auth")]
use tokio::io::AsyncBufReadExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter};

macro_rules! build_parser_type_enum {
($($name:ident: $t:ty: $feat:expr,)*) => {
Expand Down Expand Up @@ -85,6 +92,10 @@ where
grids: Arc<[Flut<u32>]>,
parser: ParserTypes,
counter: u64,
#[cfg(feature = "auth")]
auth_client: Client,
#[cfg(feature = "auth")]
user: User,
}

impl<R, W> FlutClient<R, W>
Expand Down Expand Up @@ -140,7 +151,15 @@ where
}
Color::W8(white) => u32::from_be_bytes([*white, *white, *white, 0xff]),
};
set_pixel_rgba(self.grids.as_ref(), canvas, x, y, c);
set_pixel_rgba(
self.grids.as_ref(),
canvas,
x,
y,
c,
#[cfg(feature = "auth")]
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is annoying but needed to have the tests work with --features auth as well

self.user,
);
self.counter += 1;
}

Expand Down Expand Up @@ -174,10 +193,49 @@ where
grids,
parser: ParserTypes::default(),
counter: 0,
#[cfg(feature = "auth")]
auth_client: ClientBuilder::new().https_only(true).build().unwrap(),
#[cfg(feature = "auth")]
user: 0,
}
}

pub async fn process_socket(&mut self) -> io::Result<()> {
// Handle the auth flow
#[cfg(feature = "auth")]
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be redone at some point, or at least depend on the other side

{
let mut buf = Vec::new();
let chars = self.reader.read_until(b' ', &mut buf).await?;
if chars != 5 {
return Err(Error::from(ErrorKind::PermissionDenied));
}
if buf != b"AUTH " {
return Err(Error::from(ErrorKind::PermissionDenied));
}

buf.clear();
let token_length = self.reader.read_until(b'\n', &mut buf).await?;

if token_length > 100 {
return Err(Error::from(ErrorKind::PermissionDenied));
}

let request = self
.auth_client
.post(AUTH_SERVER_URL)
.body(buf)
.build()
.unwrap();
let response = self.auth_client.execute(request).await.unwrap();
if response.status() != 200 {
return Err(Error::from(ErrorKind::PermissionDenied));
}

let user = response.bytes().await.unwrap().get_u32();

tracing::info!("User with id {user} authenticated");
self.user = user;
}
loop {
match_parser!(parser: &self.parser.clone() => 'outer: loop {
for _ in 0..1000 {
Expand Down
60 changes: 53 additions & 7 deletions src/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ use std::{
sync::{RwLock, RwLockReadGuard},
};

use image::{GenericImageView, Rgb};

#[cfg(feature = "auth")]
use crate::blame::{BlameMap, User};
use crate::Coordinate;
use image::{GenericImageView, Rgb};

pub trait Grid<I, V> {
fn get(&self, x: I, y: I) -> Option<&V>;
#[allow(dead_code)]
fn get_unchecked(&self, x: I, y: I) -> &V;
fn set(&self, x: I, y: I, value: V);
fn set(&self, x: I, y: I, value: V, #[cfg(feature = "auth")] user: User);
}

pub struct Flut<T> {
Expand All @@ -21,6 +22,10 @@ pub struct Flut<T> {
cells: SyncUnsafeCell<Box<[T]>>,
last_hash: SyncUnsafeCell<u64>,
jpgbuf: RwLock<Vec<u8>>,
#[cfg(feature = "auth")]
blamebuf: RwLock<Vec<u8>>,
#[cfg(feature = "auth")]
blame: BlameMap,
}

impl<T: Clone> Flut<T> {
Expand All @@ -35,6 +40,10 @@ impl<T: Clone> Flut<T> {
cells: vec.into_boxed_slice().into(),
last_hash: 0.into(),
jpgbuf: RwLock::new(Vec::new()),
#[cfg(feature = "auth")]
blamebuf: RwLock::new(Vec::new()),
#[cfg(feature = "auth")]
blame: BlameMap::new(size_x, size_y),
}
}

Expand All @@ -53,7 +62,15 @@ impl<T> Flut<T> {
Some((y * self.size_x) + x)
}
pub fn read_jpg_buffer(&self) -> RwLockReadGuard<'_, Vec<u8>> {
self.jpgbuf.read().expect("RWlock didn't exit nicely")
self.jpgbuf
.read()
.expect("canvas RWlock didn't exit nicely")
}
#[cfg(feature = "auth")]
pub fn read_blame_buffer(&self) -> RwLockReadGuard<'_, Vec<u8>> {
self.blamebuf
.read()
.expect("blame RWlock didn't exit nicely")
}
}

Expand All @@ -63,11 +80,13 @@ impl<T> Grid<Coordinate, T> for Flut<T> {
.map(|idx| unsafe { &(&(*self.cells.get()))[idx] })
}

fn set(&self, x: Coordinate, y: Coordinate, value: T) {
fn set(&self, x: Coordinate, y: Coordinate, value: T, #[cfg(feature = "auth")] user: User) {
match self.index(x, y) {
None => (),
Some(idx) => unsafe { (&mut (*self.cells.get()))[idx] = value },
}
#[cfg(feature = "auth")]
self.blame.set_blame(x, y, user);
}

fn get_unchecked(&self, x: Coordinate, y: Coordinate) -> &T {
Expand Down Expand Up @@ -116,6 +135,21 @@ impl Flut<u32> {
Err(err) => tracing::error!("Error writing jpeg buffer: {:?}", err),
}
}

#[cfg(feature = "auth")]
pub fn update_blame_buffer(&self) {
let mut blamebuf = self.blamebuf.write().expect("Could not get write RWlock");
blamebuf.clear();
let encoder = image::codecs::png::PngEncoder::new(&mut *blamebuf);
let subimage = self
.blame
.view(0, 0, self.width(), self.height())
.to_image();
match subimage.write_with_encoder(encoder) {
Ok(_) => {}
Err(err) => tracing::error!("Error writing png buffer: {:?}", err),
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -167,14 +201,26 @@ mod tests {
#[tokio::test]
async fn test_grid_get() {
let grid = Flut::init(3, 3, 0);
grid.set(1, 2, 222);
grid.set(
1,
2,
222,
#[cfg(feature = "auth")]
0,
);
assert_eq!(grid.get(1, 2), Some(&222));
}

#[tokio::test]
async fn test_grid_get_out_of_range() {
let grid = Flut::init(3, 3, 0);
grid.set(3, 1, 256);
grid.set(
3,
1,
256,
#[cfg(feature = "auth")]
0,
);
assert_eq!(grid.get(3, 1), None);
assert_eq!(grid.get(1, 2), Some(&0));
}
Expand Down
12 changes: 11 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

use std::sync::atomic::AtomicU64;

#[cfg(feature = "auth")]
use blame::User;
pub use color::Color;
use grid::Grid;

pub(crate) mod blame;
pub mod config;
pub mod flutclient;
pub mod grid;
Expand All @@ -31,9 +34,16 @@ fn set_pixel_rgba(
x: Coordinate,
y: Coordinate,
rgb: u32,
#[cfg(feature = "auth")] user: User,
) {
if let Some(grid) = grids.get(canvas as usize) {
grid.set(x, y, rgb);
grid.set(
x,
y,
rgb,
#[cfg(feature = "auth")]
user,
);
}
}

Expand Down
20 changes: 15 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,21 @@ async fn save_image_frames(
loop {
timer.tick().await;
for grid in grids.as_ref() {
let p = base_dir.join(format!(
"{}",
chrono::Local::now().format("%Y-%m-%d_%H-%M-%S.jpg")
let pc = base_dir.join(format!(
"{}-canvas.jpg",
chrono::Local::now().format("%Y-%m-%d_%H-%M-%S")
));
let mut file_writer = File::create(p)?;

let mut file_writer = File::create(pc)?;
file_writer.write_all(&grid.read_jpg_buffer())?;
#[cfg(feature = "auth")]
{
let pb = base_dir.join(format!(
"{}-blame.png",
chrono::Local::now().format("%Y-%m-%d_%H-%M-%S")
));
let mut file_writer = File::create(pb)?;
file_writer.write_all(&grid.read_blame_buffer())?;
}
}
}
}
Expand Down Expand Up @@ -73,6 +81,8 @@ async fn jpeg_update_loop(grids: Arc<[Flut<u32>]>) -> AsyncResult<Never> {
interval.tick().await;
for grid in grids.as_ref() {
grid.update_jpg_buffer();
#[cfg(feature = "auth")]
grid.update_blame_buffer();
}
}
}
Expand Down