Skip to content

Commit

Permalink
Merge pull request #46 from EFForg/framebuffer
Browse files Browse the repository at this point in the history
Device screen UI
  • Loading branch information
wgreenberg authored Jun 18, 2024
2 parents 3c9862f + 60cbdef commit dd48d89
Show file tree
Hide file tree
Showing 9 changed files with 845 additions and 6 deletions.
681 changes: 678 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ _ _ _ _ _ _ _ _
\ | apc '._|
\__;
```
![Tests](https://github.com/EFForg/rayhunter/actions/workflows/rust.yml/badge.svg)
![Tests](https://github.com/EFForg/rayhunter/actions/workflows/check-and-test.yml/badge.svg)

Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.

Expand Down
1 change: 1 addition & 0 deletions bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ tokio-stream = "0.1.14"
futures = "0.3.30"
clap = { version = "4.5.2", features = ["derive"] }
serde_json = "1.0.114"
image = "0.25.1"
4 changes: 4 additions & 0 deletions bin/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ struct ConfigFile {
qmdl_store_path: Option<String>,
port: Option<u16>,
readonly_mode: Option<bool>,
ui_level: Option<u8>,
}

#[derive(Debug)]
pub struct Config {
pub qmdl_store_path: String,
pub port: u16,
pub readonly_mode: bool,
pub ui_level: u8,
}

impl Default for Config {
Expand All @@ -22,6 +24,7 @@ impl Default for Config {
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
port: 8080,
readonly_mode: false,
ui_level: 1,
}
}
}
Expand All @@ -34,6 +37,7 @@ pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef
if let Some(path) = parsed_config.qmdl_store_path { config.qmdl_store_path = path }
if let Some(port) = parsed_config.port { config.port = port }
if let Some(readonly_mode) = parsed_config.readonly_mode { config.readonly_mode = readonly_mode }
if let Some(ui_level) = parsed_config.ui_level { config.ui_level = ui_level }
}
Ok(config)
}
Expand Down
65 changes: 63 additions & 2 deletions bin/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod server;
mod stats;
mod qmdl_store;
mod diag;
mod framebuffer;

use crate::config::{parse_config, parse_args};
use crate::diag::run_diag_read_thread;
Expand All @@ -13,6 +14,7 @@ use crate::server::{ServerState, get_qmdl, serve_static};
use crate::pcap::get_pcap;
use crate::stats::get_system_stats;
use crate::error::RayhunterError;
use crate::framebuffer::Framebuffer;

use axum::response::Redirect;
use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage};
Expand All @@ -22,12 +24,16 @@ use axum::routing::{get, post};
use axum::Router;
use stats::get_qmdl_manifest;
use tokio::sync::mpsc::{self, Sender};
use tokio::sync::oneshot::error::TryRecvError;
use tokio::task::JoinHandle;
use tokio_util::task::TaskTracker;
use std::net::SocketAddr;
use std::thread::sleep;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::{RwLock, oneshot};
use std::sync::Arc;
use include_dir::{include_dir, Dir};

// Runs the axum server, taking all the elements needed to build up our
// ServerState and a oneshot Receiver that'll fire when it's time to shutdown
Expand Down Expand Up @@ -88,6 +94,7 @@ fn run_ctrl_c_thread(
task_tracker: &TaskTracker,
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
server_shutdown_tx: oneshot::Sender<()>,
ui_shutdown_tx: oneshot::Sender<()>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>
) -> JoinHandle<Result<(), RayhunterError>> {
task_tracker.spawn(async move {
Expand All @@ -102,6 +109,9 @@ fn run_ctrl_c_thread(

server_shutdown_tx.send(())
.expect("couldn't send server shutdown signal");
info!("sending UI shutdown");
ui_shutdown_tx.send(())
.expect("couldn't send ui shutdown signal");
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
.expect("couldn't send Exit message to diag thread");
},
Expand All @@ -113,6 +123,56 @@ fn run_ctrl_c_thread(
})
}

async fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>){
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
let display_level = config.ui_level;
if display_level == 0 {
info!("Invisible mode, not spawning UI.");
}

task_tracker.spawn_blocking(move || {
let mut fb: Framebuffer = Framebuffer::new();
// this feels wrong, is there a more rusty way to do this?
let mut img: Option<&[u8]> = None;
if display_level == 2 {
img = Some(IMAGE_DIR.get_file("orca.gif").expect("failed to read orca.gif").contents());
} else if display_level == 3 {
img = Some(IMAGE_DIR.get_file("eff.png").expect("failed to read eff.png").contents());
}
loop {
match ui_shutdown_rx.try_recv() {
Ok(_) => {
info!("received UI shutdown");
break;
},
Err(TryRecvError::Empty) => {},
Err(e) => panic!("error receiving shutdown message: {e}")

}
match display_level {
2 => {
fb.draw_gif(img.unwrap());
},
3 => {
fb.draw_img(img.unwrap())
},
128 => {
fb.draw_line(framebuffer::Color565::Cyan, 128);
fb.draw_line(framebuffer::Color565::Pink, 102);
fb.draw_line(framebuffer::Color565::White, 76);
fb.draw_line(framebuffer::Color565::Pink, 50);
fb.draw_line(framebuffer::Color565::Cyan, 25);
},
1 | _ => {
fb.draw_line(framebuffer::Color565::Green, 2);
},
};
sleep(Duration::from_millis(100));
}
}).await.unwrap();

}

#[tokio::main]
async fn main() -> Result<(), RayhunterError> {
env_logger::init();
Expand All @@ -134,10 +194,11 @@ async fn main() -> Result<(), RayhunterError> {

run_diag_read_thread(&task_tracker, dev, rx, qmdl_store_lock.clone());
}

let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, qmdl_store_lock.clone());
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, ui_shutdown_tx, qmdl_store_lock.clone());
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx).await;
update_ui(&task_tracker, &config, ui_shutdown_rx).await;

task_tracker.close();
task_tracker.wait().await;
Expand Down
92 changes: 92 additions & 0 deletions bin/src/framebuffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use image::{codecs::gif::GifDecoder, imageops::FilterType, AnimationDecoder, DynamicImage};
use std::{io::Cursor, time::Duration};

const FB_PATH:&str = "/dev/fb0";

#[derive(Copy, Clone)]
// TODO actually poll for this, maybe w/ fbset?
struct Dimensions {
height: u32,
width: u32,
}

#[allow(dead_code)]
pub enum Color565 {
Red = 0b1111100000000000,
Green = 0b0000011111100000,
Blue = 0b0000000000011111,
White = 0b1111111111111111,
Black = 0b0000000000000000,
Cyan = 0b0000011111111111,
Yellow = 0b1111111111100000,
Pink = 0b1111010010011111,
}

#[derive(Copy, Clone)]
pub struct Framebuffer<'a> {
dimensions: Dimensions,
path: &'a str,
}

impl Framebuffer<'_>{
pub const fn new() -> Self {
Framebuffer{
dimensions: Dimensions{height: 128, width: 128},
path: FB_PATH,
}
}

fn write(&mut self, img: DynamicImage) {
let mut width = img.width();
let mut height = img.height();
let resized_img: DynamicImage;
if height > self.dimensions.height ||
width > self.dimensions.width {
resized_img = img.resize( self.dimensions.width, self.dimensions.height, FilterType::CatmullRom);
width = self.dimensions.width.min(resized_img.width());
height = self.dimensions.height.min(resized_img.height());
} else {
resized_img = img;
}
let img_rgba8 = resized_img.as_rgba8().unwrap();
let mut buf = Vec::new();
for y in 0..height {
for x in 0..width {
let px = img_rgba8.get_pixel(x, y);
let mut rgb565: u16 = (px[0] as u16 & 0b11111000) << 8;
rgb565 |= (px[1] as u16 & 0b11111100) << 3;
rgb565 |= (px[2] as u16) >> 3;
buf.extend(rgb565.to_le_bytes());
}
}
std::fs::write(self.path, &buf).unwrap();
}

pub fn draw_gif(&mut self, img_buffer: &[u8]) {
// this is dumb and i'm sure there's a better way to loop this
let cursor = Cursor::new(img_buffer);
let decoder = GifDecoder::new(cursor).unwrap();
for maybe_frame in decoder.into_frames() {
let frame = maybe_frame.unwrap();
let (numerator, _) = frame.delay().numer_denom_ms();
let img = DynamicImage::from(frame.into_buffer());
self.write(img);
std::thread::sleep(Duration::from_millis(numerator as u64));
}
}

pub fn draw_img(&mut self, img_buffer: &[u8]) {
let img = image::load_from_memory(img_buffer).unwrap();
self.write(img);
}

pub fn draw_line(&mut self, color: Color565, height: u32){
let px_num= height * self.dimensions.width;
let color: u16 = color as u16;
let mut buffer: Vec<u8> = Vec::new();
for _ in 0..px_num {
buffer.extend(color.to_le_bytes());
}
std::fs::write(self.path, &buffer).unwrap();
}
}
Binary file added bin/static/images/eff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added bin/static/images/orca.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions dist/config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@
qmdl_store_path = "/data/rayhunter/qmdl"
port = 8080
readonly_mode = false
# UI Levels:
# 0 = invisible mode, no indicator that rayhunter is running
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
# 2 = Demo Mode, display a fun orca gif
# 3 = display the EFF logo
ui_level = 1

0 comments on commit dd48d89

Please sign in to comment.