Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logging feature #10

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,3 @@ Code without tests might be rejected or take longer to process.
If you discover a security vulnerability in our code, please inform us privately immediately according to our [Security Policy](./SECURITY.md).

If you wish to fix a vulnerability, please also inform us and stand by for our green light. We would still like to investigate the vulnerability for ourselves to get an overview over the severity and scope of the problem. *Please refrain from publishing a fix until we came back to you or have published a security advisory*.

## Disclaimer

By contributing to this project you agree to surrender your contribution to the Nazara Project.

Your contribution to this project will be subject to the same licensing terms as the rest of the project.

This is a standard practice in open-source projects to ensure that all contributions are compatible with the project's overall license and to maintain consistency in the project's legal framework.

It's important for contributors to be aware of this agreement to ensure that their contributions are properly integrated into the project without any legal conflicts.
#### Thank you for your contribution!
7 changes: 7 additions & 0 deletions isototest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ repository = "https://github.com/ByteOtter/isotest-ng/tree/main/isototest"
license = "GPL-2.0"

[dependencies]
image = "0.25.2"
log = "0.4.22"
tokio = "1.38.1"
vnc-rs = "0.5.1"
env_logger = { version= "0.10", optional=true }

[dev-dependencies]
mockito = "1.4.0"

[features]
# Feature to enable default logging configuration
default-logging = ["env_logger"]
30 changes: 19 additions & 11 deletions isototest/src/action.rs → isototest/src/action/keyboard.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// SPDX-FileCopyrightText: Christopher Hock <[email protected]>
// SPDX-LicenseIdentifier: GPL-2.0-or-later
//! Action
//! # Keyboard Module
//!
//! This module handles interactions between the VncClient and VncServer.
//! This module handles text-based interactions between the VncClient and VncServer.
//!
//! It uses [`X11Event::KeyEvent`](https://docs.rs/vnc-rs/0.5.1/vnc/event/struct.ClientKeyEvent.html) to send
//! individual key press or release events to the VNC server.
//!
//! To view what characters and control sequences are currently supported, see [`crate::types`].
extern crate proc_macro;
use std::{thread::sleep, time::Duration};

use vnc::{client::VncClient, ClientKeyEvent, VncError, X11Event};
use log::info;

use crate::types::{KeyCode, KeyEventType};
use crate::logging::LOG_TARGET;

/// Sleep.
/// Needed to time requests in accordance with the server's framerate to not overwhelm it with
Expand All @@ -25,7 +32,7 @@ macro_rules! wait_for_frame {
};
}

/// Write given text to console
/// Send given text to VNC server.
///
/// Uses `X11Event`s to send keypresses to the server. According to the [RFC](https://www.rfc-editor.org/rfc/rfc6143.html#section-7.5.4)
/// it does not matter whether the X-Window System is running or not.
Expand All @@ -34,6 +41,8 @@ macro_rules! wait_for_frame {
///
/// * client: `&VncClient` - The client to be used for connections
/// * text: `String` - The text to write.
/// * framerate: `Option<f64>` - The framerate of the remote machine. Used to time intervals in
/// which key signals are sent. If `None`, signal intervals are calculated according to a default. (30FPS)
///
/// # Returns
///
Expand All @@ -45,6 +54,7 @@ pub async fn write_to_console(
framerate: Option<f64>,
) -> Result<(), VncError> {
// Translate each character to a keycode
info!(target: LOG_TARGET, "Sending text '{}' with intervall of {}FPS....", text, framerate.unwrap_or(30 as f64));
let mut keycode: u32;

for ch in text.chars() {
Expand All @@ -65,18 +75,13 @@ pub async fn write_to_console(
press_button(client, modifier, KeyEventType::Release, framerate).await?;
}
}
info!(target: LOG_TARGET, "Text '{}' sent.", text);
Ok(())
}

#[allow(unused)]
/// Receive a screenshot of the remote machine.
pub async fn read_screen(client: &VncClient) -> Result<(), VncError> {
todo!("Not implemented yet!")
}

/// Encapsulate the `client.input()` function calls to avoid repitition.
///
/// Press and release a button or release it manually, if it is pressed.
/// Will put the given key into a state according to the [crate::types::KeyEventType] parameter.
///
/// # Parameters
///
Expand Down Expand Up @@ -192,7 +197,10 @@ fn framerate_to_nanos(rate: Option<f64>) -> Result<Duration, VncError> {
}
}

/// Assign a given character its corresponding `VirtualKeyCode`.
/// Assign a given character its corresponding [`crate::types::KeyCode`].
///
/// Will return the u32 representation of the actualkeycode as this is required by
/// [`vnc-rs`](https://docs.rs/vnc-rs/0.5.1/vnc/event/struct.ClientKeyEvent.html)
///
/// # Parameters
///
Expand Down
3 changes: 3 additions & 0 deletions isototest/src/action/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! This module is used to interact with the VNC server in any capacity.
pub mod keyboard;
pub mod view;
148 changes: 148 additions & 0 deletions isototest/src/action/view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//! # View module
use image::{GenericImage, ImageFormat, Rgba};
use std::{
path::Path,
time::{Duration, Instant},
};

use image::ImageBuffer;

use image::DynamicImage::ImageRgba8;
use vnc::{Rect, VncClient, VncError, VncEvent, X11Event};

use log::{error, info, warn};

use crate::logging::LOG_TARGET;

/// Receive a screenshot of the remote machine.
///
/// # Parameters
///
/// * client: `&VncClient` - The client instance used for connection.
/// * file_path: `&str` - A file path you want to save your screenshot under as a `str`.
/// * resolution: `Option<u32, u32>` - The resolution of the VNC session.
/// * timeout: `Duration` - The `Duration` this function should wait for a `VncEvent` before it
/// continues.
///
/// **NOTE**: The `resolution` must be passed to all calls of `read_screen` except the first one.
/// If it is not passed, the function will attempt to detect the resolution from the VNC server.
/// This only works for the first time though. The client cannot retrieve the resolution a second
/// time by itself as long as it has not changed. We recommend to save the `Ok()` return value of
/// the function so you have a global resolution state to return to when calling.
///
/// # Returns
///
/// * `Ok((u32, u32))` - The resolution of the VNC machine we connect to.
/// * `Err(VncError)` - Variation of `VncError` if something goes wrong.
pub async fn read_screen(
client: &VncClient,
file_path: &str,
resolution: Option<(u32, u32)>,
timeout: Duration,
) -> Result<(u32, u32), VncError> {
info!(target: LOG_TARGET, "Requesting screenshot...");
// Request screen update.
client.input(X11Event::Refresh).await?;

let mut img_parts: Vec<(Rect, Vec<u8>)> = Vec::new();
let mut width: Option<u32>;
let mut height: Option<u32>;

// Try to detect screen resolution of the remote machine if it has not been passed.
// **This will cause issues, if you try to use this functionality a second time.**
match resolution {
Some((x, y)) => {
info!(target: LOG_TARGET, "Resolution provided; proceeding...");
width = Some(x);
height = Some(y);
}
None => match client.recv_event().await? {
VncEvent::SetResolution(screen) => {
info!(target: LOG_TARGET, "Resolution received. Screen resolution: {}x{}", screen.width, screen.height);
width = Some(screen.width as u32);
height = Some(screen.height as u32);

client.input(X11Event::Refresh).await?;
}
_ => {
error!(target: LOG_TARGET, "Failed to retrieve screen resolution. Aborting...");
return Err(VncError::General(
"[error] No resolution found!".to_string(),
))
}
},
}

let path: &Path = Path::new(file_path);
let idle_timer: Instant = Instant::now();

loop {
// Poll new vnc events.
match client.poll_event().await? {
Some(x) => match x {
VncEvent::SetResolution(screen) => {
println!("Screen resolution: {}x{}", screen.width, screen.height);
width = Some(screen.width as u32);
height = Some(screen.height as u32);

client.input(X11Event::Refresh).await?;
}
VncEvent::RawImage(rect, data) => {
img_parts.push((rect, data));
}
VncEvent::Error(e) => {
eprintln!("[error] {}", e);
return Err(VncError::General(e));
}
x => {
warn!(target: LOG_TARGET,
"Function 'read_screen' got unexpected event '{:?}'.",
x
);
break;
}
},
None => {
if idle_timer.elapsed() >= timeout {
warn!(target: LOG_TARGET, "Timeout while waiting for VNC Event.");
break;
}
}
}
}

let mut image: ImageBuffer<Rgba<u8>, _> = ImageBuffer::new(width.unwrap(), height.unwrap());

// Reconstruct image from snippets sent by VNC server.
for (rect, data) in img_parts {
let mut view = image.sub_image(
rect.x as u32,
rect.y as u32,
rect.width as u32,
rect.height as u32,
);
let image_buffer: ImageBuffer<Rgba<u8>, _> =
ImageBuffer::from_raw(rect.width as u32, rect.height as u32, data.to_vec())
.ok_or("Failed to create image buffer!")
.unwrap();

for x in 0..rect.width {
for y in 0..rect.height {
view.put_pixel(
x as u32,
y as u32,
image_buffer.get_pixel(x as u32, y as u32).to_owned(),
);
}
}
}

// Save image to file system in PNG format.
// NOTE: If the image color encoding is changed here, you must also change it in connection.rs!
ImageRgba8(image)
.save_with_format(path, ImageFormat::Png)
.unwrap();

info!(target: LOG_TARGET, "Screenshot saved to '{}'", file_path);
Ok((width.unwrap(), height.unwrap()))
}
48 changes: 37 additions & 11 deletions isototest/src/connection.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// SPDX-FileCopyrightTest: Christopher Hock <[email protected]>
// SPDX-License-Identifier: GPL-2.0-or-later

//! # Connection Module
//!
//! This module handles the VncClient and its connection to the VncServer.
use log::{debug, error, info};
use tokio::{self, net::TcpStream};
use vnc::{PixelFormat, VncClient, VncConnector, VncError};

use crate::logging::LOG_TARGET;

/// Create a new VNC client.
///
/// During the connection process the connection to the VNC server is
Expand All @@ -31,35 +32,53 @@ pub async fn create_vnc_client(
target_ip: String,
mut psw: Option<String>,
) -> Result<VncClient, VncError> {
info!(target: LOG_TARGET, "Creating VNC client for target IP: '{}'", target_ip);

if psw.is_none() {
debug!("No password provided; using empty password.");
psw = Some(String::new());
}

let tcp: TcpStream = match TcpStream::connect(target_ip).await {
Ok(tcp) => tcp,
Err(e) => {
error!(target: LOG_TARGET, "Failed to connect: {}", e);
let err = VncError::IoError(e);
return Err(err);
}
};

let vnc: VncClient = VncConnector::new(tcp)
let vnc: VncClient = match VncConnector::new(tcp)
.set_auth_method(async move { Ok(psw.unwrap()) })
.add_encoding(vnc::VncEncoding::Tight)
.add_encoding(vnc::VncEncoding::Zrle)
.add_encoding(vnc::VncEncoding::CopyRect)
.add_encoding(vnc::VncEncoding::Raw)
.add_encoding(vnc::VncEncoding::Trle)
.add_encoding(vnc::VncEncoding::CursorPseudo)
.add_encoding(vnc::VncEncoding::DesktopSizePseudo)
.allow_shared(true)
.set_pixel_format(PixelFormat::bgra())
.build()?
.try_start()
.await?
.finish()?;
// NOTE: If the color encoding is changed in the following line, you must also change it in
// view.rs to avoid the saved screenshots from having swapped colors.
.set_pixel_format(PixelFormat::rgba())
.build()
{
Ok(vnc) => vnc,
Err(e) => {
error!(target: LOG_TARGET, "Failed to build VNC client: {}", e);
return Err(e);
}
}
.try_start()
.await?
.finish()?;

info!("VNC Client successfully built and started.");

Ok(vnc)
}

/// Stop VNC engine, release all resources
/// Stop VNC engine, release all resources.
///
/// # Parameters
///
Expand All @@ -71,10 +90,17 @@ pub async fn create_vnc_client(
/// * `Err(VncError)` - Escalates the `VncError` upwards, if the `.close()` function of `vnc-rs`
/// returns an error.
pub async fn kill_client(client: VncClient) -> Result<(), VncError> {
info!(target: LOG_TARGET, "Closing connection...");
match client.close().await {
Ok(_) => {}
Err(e) => return Err(e),
Ok(_) => {
info!(target: LOG_TARGET, "Connection closed.");
}
Err(e) => {
error!(target: LOG_TARGET, "Unable to close connection: {}", e);
return Err(e);
}
};
drop(client);
info!(target: LOG_TARGET, "Client dropped.");
Ok(())
}
Loading
Loading