Compare commits
No commits in common. "main" and "full-rewrite" have entirely different histories.
main
...
full-rewri
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
|
@ -1,17 +1,8 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tracking_pixel"
|
name = "tracking_pixel"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
askama = "0.12.0"
|
|
||||||
base64 = "0.21.0"
|
|
||||||
chrono = "0.4.24"
|
|
||||||
crypto-hash = "0.3.4"
|
|
||||||
oneshot = "0.1.5"
|
|
||||||
rand = "0.8.5"
|
|
||||||
rouille="3.6.1"
|
|
||||||
rusqlite = { version = "0.29.0", features = ["bundled", "chrono"] }
|
|
||||||
thiserror = "1.0.40"
|
|
||||||
|
|
1
README
1
README
|
@ -1 +0,0 @@
|
||||||
This is a small project that runs a webserver with tracking pixels. It's currently more of a proof-of-concept, and many features (the DB connector, the web server itself, password hashing, etc.) could use some work. There's a small start on a rewrite in full-rewrite.
|
|
|
@ -1,11 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Hello</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>
|
|
||||||
Hallo
|
|
||||||
</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
101
src/db.rs
101
src/db.rs
|
@ -1,101 +0,0 @@
|
||||||
use rusqlite::{Connection};
|
|
||||||
use std::{sync::mpsc::{Sender, Receiver, channel, SendError}, thread::{self, JoinHandle}, any::Any};
|
|
||||||
|
|
||||||
enum DBMessage<T> {
|
|
||||||
HangUp,
|
|
||||||
Message(T)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DBSender<MsgT> {
|
|
||||||
sender: Sender<DBMessage<MsgT>>
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[derive(Clone)] requires MsgT to be Clone, but that's not necessary
|
|
||||||
impl<MsgT> Clone for DBSender<MsgT> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {sender: self.sender.clone()}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<MsgT> DBSender<MsgT> {
|
|
||||||
pub fn send(&self, msg: MsgT) -> Result<(), SendError<MsgT>>{
|
|
||||||
if let Err(err) = self.sender.send(DBMessage::Message(msg)) {
|
|
||||||
if let DBMessage::Message(msg) = err.0 {
|
|
||||||
Err(SendError(msg))
|
|
||||||
} else {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hang_up(&self) -> Result<(), SendError<()>> {
|
|
||||||
if let Err(_) = self.sender.send(DBMessage::HangUp) {
|
|
||||||
Err(SendError(()))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DBConnection<MsgT: Send + 'static>
|
|
||||||
{
|
|
||||||
pub sender: DBSender<MsgT>,
|
|
||||||
join_handle: Option<JoinHandle<()>>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl<MsgT: Send + 'static> DBConnection<MsgT>
|
|
||||||
{
|
|
||||||
pub fn init<F: Fn(MsgT, &Connection) + Send + 'static>(connection: Connection, op: F) -> Self {
|
|
||||||
let (sender, receiver) = channel();
|
|
||||||
|
|
||||||
let join_handle = thread::spawn(move || Self::db_thread(receiver, connection, op));
|
|
||||||
let sender = DBSender { sender };
|
|
||||||
|
|
||||||
Self {sender, join_handle: Some(join_handle)}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_thread<F: Fn(MsgT, &Connection) + Send>(receiver: Receiver<DBMessage<MsgT>>, connection: Connection, op: F) {
|
|
||||||
while let Ok(db_msg) = receiver.recv() {
|
|
||||||
// _ = connection.execute("INSERT INTO hits VALUES(?1, ?2, ?3, ?4);", params![hit.pixel_id, hit.ip, hit.user_agent, hit.date]).unwrap();
|
|
||||||
if let DBMessage::Message(msg) = db_msg {
|
|
||||||
op(msg, &connection);
|
|
||||||
}
|
|
||||||
else if let DBMessage::HangUp = db_msg {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute last messages in buffer
|
|
||||||
for db_msg in receiver.try_iter() {
|
|
||||||
if let DBMessage::Message(msg) = db_msg {
|
|
||||||
op(msg, &connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// static is implied, but included for clearness
|
|
||||||
pub fn finish(&mut self) -> Result<(), Box<dyn Any + Send + 'static>> {
|
|
||||||
// let Self{sender, join_handle} = self;
|
|
||||||
if let Err(err) = self.sender.hang_up() {
|
|
||||||
return Err(Box::new(err));
|
|
||||||
}
|
|
||||||
if let Some(handle) = self.join_handle.take() {
|
|
||||||
handle.join()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<MsgT: Send> Drop for DBConnection<MsgT> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.finish().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
use rouille::Response;
|
|
||||||
|
|
||||||
pub fn javascript_redirect(path: &str) -> Response {
|
|
||||||
Response::html(format!("
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Redirecting...</title>
|
|
||||||
<meta http-equiv=\"refresh\" content=\"0;URL='{path}'\">
|
|
||||||
<script>
|
|
||||||
window.location.href = \"{path}\";
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Redirecting...</h1>
|
|
||||||
</body>
|
|
||||||
</html>"))
|
|
||||||
}
|
|
206
src/main.rs
206
src/main.rs
|
@ -1,205 +1 @@
|
||||||
use std::{fs, sync::{Mutex}};
|
fn main() {}
|
||||||
|
|
||||||
use askama::Template;
|
|
||||||
use rouille::{Response, router, input, Request, try_or_400, post_input};
|
|
||||||
|
|
||||||
|
|
||||||
use pixel::{Hit, PixelManager, PixelManagerDelegate, TrackingPixel, User};
|
|
||||||
|
|
||||||
mod db;
|
|
||||||
mod pixel;
|
|
||||||
mod helpers;
|
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let white_1x1_pixel_png = fs::read("assets/white_pixel.png").unwrap();
|
|
||||||
|
|
||||||
let pixel_manager = PixelManager::build("app.db").unwrap();
|
|
||||||
|
|
||||||
let pixel_manager_delegate = PixelManagerDelegate::new(&pixel_manager);
|
|
||||||
// let pixel_id = pixel_manager.create_pixel("Hallo mac").unwrap();
|
|
||||||
// print!("{pixel_id}");
|
|
||||||
// pixel_manager.register_hit(pixel_id, "ai pie", "joeser eedsjent", chrono::offset::Utc::now());
|
|
||||||
|
|
||||||
let delegate_mutex = Mutex::new(pixel_manager_delegate);
|
|
||||||
|
|
||||||
rouille::start_server("0.0.0.0:3737", move |request| {
|
|
||||||
router!(request,
|
|
||||||
(GET) ["/manage"] => {
|
|
||||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
|
||||||
// panicking on poisoned mutex is acceptable
|
|
||||||
manage_index(request, &delegate)
|
|
||||||
},
|
|
||||||
(GET) ["/manage/pixel/{pixel_id}", pixel_id: String] => {
|
|
||||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
|
||||||
manage_pixel(&pixel_id, request, &delegate)
|
|
||||||
},
|
|
||||||
(POST) ["/manage/pixel/create"] => {
|
|
||||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
|
||||||
create_pixel(request, &delegate)
|
|
||||||
},
|
|
||||||
(POST) ["/manage/pixel/delete"] => {
|
|
||||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
|
||||||
delete_pixel(request, &delegate)
|
|
||||||
},
|
|
||||||
(GET) ["/img/{id}/white.png", id: String] => {
|
|
||||||
let delegate = delegate_mutex.lock().unwrap().clone();
|
|
||||||
register_hit(&id, request, &delegate);
|
|
||||||
Response::from_data("image/png", white_1x1_pixel_png.clone())
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
println!("404: {:#?}", request.url());
|
|
||||||
Response::empty_404()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "index.html")]
|
|
||||||
struct IndexTemplate<'a> {
|
|
||||||
username: &'a str,
|
|
||||||
pixels: Vec<(String, String)>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IndexTemplate<'a> {
|
|
||||||
fn new(username: &'a str, pixels: &Vec<TrackingPixel>) -> Self {
|
|
||||||
Self {
|
|
||||||
username,
|
|
||||||
pixels: pixels.iter().map(|pix| (pix.name.clone(), pix.url_safe_encode())).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "manage/pixel.html")]
|
|
||||||
struct PixelTemplate {
|
|
||||||
pixel_name: String,
|
|
||||||
hits: Vec<(String, String, String)>,
|
|
||||||
pixel_url: String,
|
|
||||||
pixel_id: String
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PixelTemplate {
|
|
||||||
fn new(pixel: &TrackingPixel, hits: &Vec<Hit>) -> Self {
|
|
||||||
Self {
|
|
||||||
pixel_name: pixel.name.clone(),
|
|
||||||
hits: hits.iter().map(|hit| (hit.date.to_string(), hit.ip.clone(), hit.user_agent.clone())).collect(),
|
|
||||||
pixel_url: get_url_for_pixel(&pixel),
|
|
||||||
pixel_id: pixel.url_safe_encode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn manage_index(request: &Request, manager_delegate: &PixelManagerDelegate) -> Response {
|
|
||||||
let user = match auth_user(request, &manager_delegate) {
|
|
||||||
Ok(user) => user,
|
|
||||||
Err(response) => return response
|
|
||||||
};
|
|
||||||
|
|
||||||
match manager_delegate.get_manager().get_pixels_for_user(&user) {
|
|
||||||
Ok(pixels) => {
|
|
||||||
let index_template = IndexTemplate::new(&user.username, &pixels);
|
|
||||||
match index_template.render() {
|
|
||||||
Ok(page) => Response::html(page),
|
|
||||||
Err(_) => Response::text("Page error").with_status_code(500)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => Response::text("DB error").with_status_code(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_user(request: &Request, manager_delegate: &PixelManagerDelegate) -> Result<User, Response> {
|
|
||||||
let auth = match input::basic_http_auth(request) {
|
|
||||||
Some(a) => a,
|
|
||||||
None => return Err(Response::basic_http_auth_login_required("manage"))
|
|
||||||
};
|
|
||||||
match manager_delegate.get_manager().get_user(&auth.login, &auth.password) {
|
|
||||||
Ok(user) => Ok(user),
|
|
||||||
Err(_) => return Err(Response::basic_http_auth_login_required("manage"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn manage_pixel(pixel_id: &str, request: &Request, manager_delegate: &PixelManagerDelegate) -> Response {
|
|
||||||
let user = match auth_user(request, &manager_delegate) {
|
|
||||||
Ok(user) => user,
|
|
||||||
Err(response) => return response
|
|
||||||
};
|
|
||||||
|
|
||||||
let pixel = match manager_delegate.get_manager().get_pixel_from_encoded(pixel_id, Some(&user)) {
|
|
||||||
Ok(pixel_opt) => {
|
|
||||||
match pixel_opt {
|
|
||||||
Some(pixel) => pixel,
|
|
||||||
None => return Response::text("Unknown pixel").with_status_code(500)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => return Response::text("Encoding error").with_status_code(500)
|
|
||||||
};
|
|
||||||
|
|
||||||
let hits = match manager_delegate.get_manager().get_hits_for_pixel(&pixel, Some(&user)) {
|
|
||||||
Ok(hits) => hits,
|
|
||||||
Err(_) => return Response::text("DB error").with_status_code(500)
|
|
||||||
};
|
|
||||||
|
|
||||||
let pixel_template = PixelTemplate::new(&pixel, &hits);
|
|
||||||
|
|
||||||
match pixel_template.render() {
|
|
||||||
Ok(page) => Response::html(page),
|
|
||||||
Err(_) => Response::text("Page error").with_status_code(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_pixel(request: &Request, manager_delegate: &PixelManagerDelegate) -> Response {
|
|
||||||
let user = match auth_user(request, &manager_delegate) {
|
|
||||||
Ok(user) => user,
|
|
||||||
Err(response) => return response
|
|
||||||
};
|
|
||||||
let input = try_or_400!(post_input!(request, {
|
|
||||||
pixel_name: String
|
|
||||||
}));
|
|
||||||
let _pixel = match manager_delegate.get_manager().create_pixel(&input.pixel_name, &user) {
|
|
||||||
Ok(pixel) => pixel,
|
|
||||||
Err(_) => return Response::empty_400()
|
|
||||||
};
|
|
||||||
// Response::redirect_303("/manage")
|
|
||||||
helpers::javascript_redirect("/manage")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_pixel(request: &Request, manager_delegate: &PixelManagerDelegate) -> Response {
|
|
||||||
let user = match auth_user(request, &manager_delegate) {
|
|
||||||
Ok(user) => user,
|
|
||||||
Err(response) => return response,
|
|
||||||
};
|
|
||||||
|
|
||||||
let input = try_or_400!(post_input!(request, {
|
|
||||||
pixel_id: String
|
|
||||||
}));
|
|
||||||
|
|
||||||
let pixel = match manager_delegate.get_manager().get_pixel_from_encoded(&input.pixel_id, Some(&user)) {
|
|
||||||
Ok(pixel) => pixel,
|
|
||||||
Err(_) => return Response::text("DB error").with_status_code(500)
|
|
||||||
};
|
|
||||||
if let Some(pixel) = pixel {
|
|
||||||
if manager_delegate.get_manager().delete_pixel(pixel, Some(&user)).is_err() {
|
|
||||||
return Response::text("DB error").with_status_code(500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Response::empty_400(); // could not find pixel
|
|
||||||
}
|
|
||||||
// Response::redirect_303("/manage")
|
|
||||||
helpers::javascript_redirect("/manage")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_hit(pixel_id: &str, request: &Request, manager_delegate: &PixelManagerDelegate) {
|
|
||||||
let ua = request.header("User-Agent").unwrap_or_default();
|
|
||||||
let ip = request.header("X-Forwarded-For").unwrap_or_default();
|
|
||||||
_ = manager_delegate.get_manager().register_hit_with_encoded_pixel(pixel_id, &ip, ua, chrono::offset::Utc::now());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn get_url_for_pixel(pixel: &TrackingPixel) -> String {
|
|
||||||
format!("https://track.willem.page/img/{}/white.png", pixel.url_safe_encode())
|
|
||||||
}
|
|
482
src/pixel.rs
482
src/pixel.rs
|
@ -1,482 +0,0 @@
|
||||||
use std::sync::mpsc;
|
|
||||||
|
|
||||||
use crate::db::{DBConnection, DBSender};
|
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use crypto_hash::{Algorithm, hex_digest};
|
|
||||||
use rusqlite::{params, Connection, OptionalExtension};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Username or password does not exist in database")]
|
|
||||||
InvalidUserData(),
|
|
||||||
#[error("Database did not respond")]
|
|
||||||
RecvError(#[from] oneshot::RecvError),
|
|
||||||
#[error("Could not send data to database")]
|
|
||||||
SendError(#[from] mpsc::SendError<DBConnectionMessage>),
|
|
||||||
#[error("Timeout receiving data from database")]
|
|
||||||
TimeoutError(#[from] oneshot::RecvTimeoutError),
|
|
||||||
#[error("User is not authenticated")]
|
|
||||||
UserNotAuthed(),
|
|
||||||
#[error("No user with this username and password")]
|
|
||||||
InvalidCredentials(),
|
|
||||||
#[error("Data was not added to database correctly")]
|
|
||||||
DBError(),
|
|
||||||
#[error("The username for this user already exists")]
|
|
||||||
NewUserAlreadyExists(),
|
|
||||||
#[error("Invalid encoding in ID")]
|
|
||||||
InvalidEncodedID(#[from] base64::DecodeError),
|
|
||||||
#[error("Pixel not found in users pixels")]
|
|
||||||
PixelNotFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum DBConnectionMessage {
|
|
||||||
RegisterHit(Hit),
|
|
||||||
CreatePixel(TrackingPixel),
|
|
||||||
DeletePixel(i64),
|
|
||||||
AuthUser(User, oneshot::Sender<bool>),
|
|
||||||
GetUserFromName(String, oneshot::Sender<Option<User>>),
|
|
||||||
GetUserFromId(i64, oneshot::Sender<Option<User>>),
|
|
||||||
CreateUser(User),
|
|
||||||
DeleteUser(i64),
|
|
||||||
GetHitsForPixel(TrackingPixel, oneshot::Sender<Option<Vec<Hit>>>, Option<User>),
|
|
||||||
GetPixelsForUser(User, oneshot::Sender<Option<Vec<TrackingPixel>>>),
|
|
||||||
GetPixel(i64, Option<User>, oneshot::Sender<Option<TrackingPixel>>)
|
|
||||||
} // messy, there is probably a better way to do this but it works so oh well
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TrackingPixel {
|
|
||||||
id: i64, // ID may not be omitted when creating new pixel
|
|
||||||
pub name: String,
|
|
||||||
user_id: i64
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrackingPixel {
|
|
||||||
pub fn url_safe_encode(&self) -> String {
|
|
||||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.id.to_be_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn url_safe_decode_id(encoded_id: &str) -> Result<i64, base64::DecodeError> {
|
|
||||||
let bytes_vec = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded_id)?;
|
|
||||||
let mut bytes_arr = [0; 8];
|
|
||||||
|
|
||||||
for i in 0..8 {
|
|
||||||
bytes_arr[i] = bytes_vec.get(i).cloned().unwrap_or_default();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(i64::from_be_bytes(bytes_arr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Hit {
|
|
||||||
id: Option<i64>, // ID is omitted when creating new hit
|
|
||||||
pixel_id: i64,
|
|
||||||
pub ip: String,
|
|
||||||
pub user_agent: String,
|
|
||||||
pub date: DateTime<Utc>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct User {
|
|
||||||
id: Option<i64>, // ID is omitted when creating new user
|
|
||||||
pub username: String,
|
|
||||||
pass_hash: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PixelManager {
|
|
||||||
db_connection_sender: DBSender<DBConnectionMessage>,
|
|
||||||
db_connection: Option<DBConnection<DBConnectionMessage>> // only original manager holds the connection, clones in delegate do not
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PixelManager {
|
|
||||||
pub fn build(db_file: &str) -> Option<Self> {
|
|
||||||
let conn = Connection::open(db_file);
|
|
||||||
if conn.is_err() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let db_connection = DBConnection::init(conn.unwrap(), Self::db_delegate);
|
|
||||||
|
|
||||||
Some(Self{ db_connection_sender: db_connection.sender.clone(), db_connection: Some(db_connection) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_delegate(msg: DBConnectionMessage, conn: &Connection) {
|
|
||||||
_ = match msg { // beware, code folding recommended
|
|
||||||
DBConnectionMessage::CreatePixel(pixel) =>
|
|
||||||
conn.execute("INSERT INTO pixels (id, name, user_id) VALUES (?1, ?2, ?3)", params![pixel.id, pixel.name, pixel.user_id]),
|
|
||||||
DBConnectionMessage::RegisterHit(hit) =>
|
|
||||||
conn.execute("INSERT INTO hits (pixel_id, ip, user_agent, date) VALUES (?1, ?2, ?3, ?4)", params![hit.pixel_id, hit.ip, hit.user_agent, hit.date]),
|
|
||||||
DBConnectionMessage::DeletePixel(pixel_id) =>
|
|
||||||
conn.execute("DELETE FROM pixels WHERE id = ?1", [pixel_id]),
|
|
||||||
DBConnectionMessage::AuthUser(user, response_sender) => {
|
|
||||||
let result: Result<i64, rusqlite::Error>;
|
|
||||||
if let User{ id: Some(id), username, pass_hash} = user {
|
|
||||||
result = conn.query_row("SELECT id FROM users WHERE id = ?1 AND username = ?2 AND pass_hash = ?3", params![id, username, pass_hash],
|
|
||||||
|row| row.get(0));
|
|
||||||
match result.optional() {
|
|
||||||
Ok(id) => {
|
|
||||||
if let Some(_) = id {
|
|
||||||
_ = response_sender.send(true);
|
|
||||||
} else {
|
|
||||||
_ = response_sender.send(false);
|
|
||||||
}
|
|
||||||
Ok(1) // 1 row affected
|
|
||||||
}
|
|
||||||
Err(e) => {Err(e)},
|
|
||||||
}
|
|
||||||
} else { // invalid user object
|
|
||||||
Ok(0) // 0 rows affected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DBConnectionMessage::GetUserFromName(username, response_sender) => {
|
|
||||||
let result = conn.query_row("SELECT id, username, pass_hash FROM users WHERE username = ?1", [username],
|
|
||||||
|row| Ok(
|
|
||||||
User{
|
|
||||||
id: row.get(0)?,
|
|
||||||
username: row.get(1)?,
|
|
||||||
pass_hash: row.get(2)?
|
|
||||||
}));
|
|
||||||
match result.optional() {
|
|
||||||
Ok(user) => {
|
|
||||||
if let Some(user) = user {
|
|
||||||
_ = response_sender.send(Some(user));
|
|
||||||
Ok(1) // 1 row affected
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_ = response_sender.send(None);
|
|
||||||
Ok(0) // no rows affected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
_ = response_sender.send(None);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DBConnectionMessage::GetUserFromId(id, response_sender) => {
|
|
||||||
let result = conn.query_row("SELECT id, username, pass_hash FROM users WHERE id = ?1", [id],
|
|
||||||
|row| Ok(
|
|
||||||
User{
|
|
||||||
id: row.get(0)?,
|
|
||||||
username: row.get(1)?,
|
|
||||||
pass_hash: row.get(2)?
|
|
||||||
}));
|
|
||||||
match result.optional() {
|
|
||||||
Ok(user) => {
|
|
||||||
if let Some(user) = user {
|
|
||||||
_ = response_sender.send(Some(user));
|
|
||||||
Ok(1) // 1 row affected
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_ = response_sender.send(None);
|
|
||||||
Ok(0) // no rows affected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
_ = response_sender.send(None);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DBConnectionMessage::CreateUser(user) =>
|
|
||||||
conn.execute("INSERT INTO users (username, pass_hash) VALUES (?1, ?2)", params![user.username, user.pass_hash]),
|
|
||||||
DBConnectionMessage::DeleteUser(id) =>
|
|
||||||
conn.execute("DELETE from users WHERE id = ?1", [id]),
|
|
||||||
DBConnectionMessage::GetHitsForPixel(pixel, response_sender, user) => 'gethitsforpixel: {
|
|
||||||
let mut stmt = conn.prepare("SELECT id, pixel_id, ip, user_agent, date FROM hits WHERE pixel_id = ?1").unwrap();
|
|
||||||
if let Some(user) = user {
|
|
||||||
if let Some(user_id) = user.id {
|
|
||||||
// check if pixel belongs to user
|
|
||||||
let pixel_id: Result<i64, _> = conn.query_row("SELECT id FROM pixels WHERE id = ?1 AND user_id = ?2", [pixel.id, user_id], |row| {
|
|
||||||
Ok(row.get(0)?)
|
|
||||||
});
|
|
||||||
if !pixel_id.is_ok() {
|
|
||||||
_ = response_sender.send(None);
|
|
||||||
break 'gethitsforpixel Ok(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if this fails something is very wrong with the underlying connection, so panicking the thread is perfectly acceptable
|
|
||||||
let mapped_rows = stmt.query_map([pixel.id], |row| {
|
|
||||||
Ok(Hit {
|
|
||||||
id: Some(row.get(0)?),
|
|
||||||
pixel_id: row.get(1)?,
|
|
||||||
ip: row.get(2)?,
|
|
||||||
user_agent: row.get(3)?,
|
|
||||||
date: row.get(4)?,
|
|
||||||
})
|
|
||||||
}); // processing on the DB thread is a bad idea, but this really isn't very complex and I don't really care
|
|
||||||
|
|
||||||
match mapped_rows {
|
|
||||||
Ok(hits) => {
|
|
||||||
let hits: Vec<Hit> = hits.filter_map(|hit| hit.ok()).collect();
|
|
||||||
let rows_affected = hits.len();
|
|
||||||
_ = response_sender.send(Some(hits));
|
|
||||||
Ok(rows_affected)
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
_ = response_sender.send(None);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DBConnectionMessage::GetPixelsForUser(user, response_sender) => {
|
|
||||||
let mut stmt = conn.prepare("SELECT id,name,user_id FROM pixels WHERE user_id = ?1").unwrap(); // should't fail but if it does connection is bad and panicking is acceptable
|
|
||||||
let mapped_rows = stmt.query_map([user.id], |row| {
|
|
||||||
Ok(TrackingPixel {
|
|
||||||
id: row.get(0)?,
|
|
||||||
name: row.get(1)?,
|
|
||||||
user_id: row.get(2)?
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
match mapped_rows {
|
|
||||||
Ok(pixels) => {
|
|
||||||
let pixels: Vec<TrackingPixel> = pixels.filter_map(|pixel| pixel.ok()).collect();
|
|
||||||
let rows_affected = pixels.len();
|
|
||||||
_ = response_sender.send(Some(pixels));
|
|
||||||
Ok(rows_affected)
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
_ = response_sender.send(None);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
DBConnectionMessage::GetPixel(id, user, response_sender) => {
|
|
||||||
let mut stmt = conn.prepare("SELECT id, name, user_id FROM pixels WHERE id = ?1").unwrap();
|
|
||||||
if let Some(user) = user {
|
|
||||||
if let Some(user_id) = user.id {
|
|
||||||
let sql_str = format!("SELECT id,name,user_id FROM pixels WHERE id = ?1 AND user_id = {}", user_id);
|
|
||||||
stmt = conn.prepare(&sql_str).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let pixel = stmt.query_row([id], |row| {
|
|
||||||
Ok(TrackingPixel {
|
|
||||||
id: row.get(0)?,
|
|
||||||
name: row.get(1)?,
|
|
||||||
user_id: row.get(2)?
|
|
||||||
})
|
|
||||||
}).optional();
|
|
||||||
match pixel {
|
|
||||||
Ok(pixel) => {
|
|
||||||
_ = response_sender.send(pixel);
|
|
||||||
Ok(1) // 1 row affected
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
_ = response_sender.send(None);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_pixel(&self, name: &str, user: &User) -> Result<TrackingPixel, Error> {
|
|
||||||
let id = rand::random::<i64>().abs();
|
|
||||||
// yes I don't check for duplicates. but whatever
|
|
||||||
if user.id.is_none() {
|
|
||||||
return Err(Error::UserNotAuthed());
|
|
||||||
}
|
|
||||||
|
|
||||||
let pixel = TrackingPixel{id, name: name.to_string(), user_id: user.id.unwrap() };
|
|
||||||
self.db_connection_sender.send(DBConnectionMessage::CreatePixel(pixel))?;
|
|
||||||
|
|
||||||
Ok(TrackingPixel { id, name: name.to_string(), user_id: user.id.unwrap() })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_pixel(&self, pixel: TrackingPixel, user: Option<&User>) -> Result<(), Error> {
|
|
||||||
if let Some(user) = user {
|
|
||||||
if let Ok(pixel) = self.get_pixel_from_id(pixel.id, Some(user)) {
|
|
||||||
if pixel.is_none() { // pixel is not withing user's pixels
|
|
||||||
return Err(Error::PixelNotFound());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(self.db_connection_sender.send(DBConnectionMessage::DeletePixel(pixel.id))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_hit_with_encoded_pixel(&self, encoded_pixel: &str, ip: &str, user_agent: &str, date: DateTime<Utc>) -> Result<(), Error> {
|
|
||||||
self.register_hit_with_pixel_id(TrackingPixel::url_safe_decode_id(encoded_pixel)?, ip, user_agent, date)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn register_hit_with_pixel(&self, pixel: &TrackingPixel, ip: &str, user_agent: &str, date: DateTime<Utc>) -> Result<(), Error> {
|
|
||||||
self.register_hit_with_pixel_id(pixel.id, ip, user_agent, date)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_hit_with_pixel_id(&self, id: i64, ip: &str, user_agent: &str, date: DateTime<Utc>) -> Result<(), Error> {
|
|
||||||
Ok(self.db_connection_sender.send(
|
|
||||||
DBConnectionMessage::RegisterHit(
|
|
||||||
Hit {
|
|
||||||
pixel_id: id,
|
|
||||||
ip: ip.to_string(),
|
|
||||||
user_agent: user_agent.to_string(),
|
|
||||||
date, id: None
|
|
||||||
})
|
|
||||||
)?
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pass_hash(password: &str) -> String {
|
|
||||||
hex_digest(Algorithm::SHA256, password.as_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_user(&self, user: &User, password: &str) -> Result<bool, Error> {
|
|
||||||
let (sender, receiver) = oneshot::channel();
|
|
||||||
|
|
||||||
|
|
||||||
let auth_user = User {
|
|
||||||
pass_hash: Self::pass_hash(password),
|
|
||||||
..user.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.db_connection_sender.send(DBConnectionMessage::AuthUser(auth_user, sender))?;
|
|
||||||
|
|
||||||
let auth = receiver.recv()?;
|
|
||||||
|
|
||||||
Ok(auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_user_from_username(&self, username: &str) -> Result<Option<User>, Error> {
|
|
||||||
let (sender, receiver) = oneshot::channel();
|
|
||||||
|
|
||||||
self.db_connection_sender.send(
|
|
||||||
DBConnectionMessage::GetUserFromName(username.to_string(), sender))?;
|
|
||||||
|
|
||||||
Ok(receiver.recv()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_user_from_id(&self, id: i64) -> Result<Option<User>, Error> {
|
|
||||||
let (sender, receiver) = oneshot::channel();
|
|
||||||
|
|
||||||
self.db_connection_sender.send(DBConnectionMessage::GetUserFromId(id, sender))?;
|
|
||||||
|
|
||||||
Ok(receiver.recv()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_user(&self, username: &str, password: &str) -> Result<User, Error>{
|
|
||||||
let user = self.get_user_from_username(username)?;
|
|
||||||
if user.is_some() {
|
|
||||||
return Err(Error::NewUserAlreadyExists());
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_user = User {
|
|
||||||
username: username.to_string(),
|
|
||||||
pass_hash: Self::pass_hash(password),
|
|
||||||
id: None
|
|
||||||
};
|
|
||||||
self.db_connection_sender.send(
|
|
||||||
DBConnectionMessage::CreateUser(new_user))?;
|
|
||||||
|
|
||||||
let user = self.get_user_from_username(username)?;
|
|
||||||
// get user struct that has the id
|
|
||||||
|
|
||||||
if let Some(user) = user {
|
|
||||||
Ok(user)
|
|
||||||
} else {
|
|
||||||
Err(Error::DBError())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_user(&self, user: User) -> Result<(), Error> {
|
|
||||||
let mut id = user.id;
|
|
||||||
if id.is_none() {
|
|
||||||
let db_user = self.get_user_from_username(&user.username)?;
|
|
||||||
|
|
||||||
if let Some(db_user) = db_user {
|
|
||||||
id = db_user.id; // must always be Some because get_user_from_username will always contain an id
|
|
||||||
} else {
|
|
||||||
return Err(Error::InvalidUserData())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.db_connection_sender.send(DBConnectionMessage::DeleteUser(id.unwrap()))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_user(&self, username: &str, password: &str) -> Result<User, Error> {
|
|
||||||
let user = self.get_user_from_username(username)?;
|
|
||||||
if let Some(user) = user {
|
|
||||||
let auth = self.auth_user(&user, password)?;
|
|
||||||
if auth {
|
|
||||||
Ok(user)
|
|
||||||
} else {
|
|
||||||
Err(Error::InvalidCredentials()) // wrong password
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Error::InvalidCredentials()) // username does not exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// optionally include a user
|
|
||||||
pub fn get_hits_for_pixel(&self, pixel: &TrackingPixel, user: Option<&User>) -> Result<Vec<Hit>, Error> {
|
|
||||||
let (sender, receiver) = oneshot::channel();
|
|
||||||
self.db_connection_sender.send(DBConnectionMessage::GetHitsForPixel(pixel.clone(), sender, user.cloned()))?;
|
|
||||||
|
|
||||||
let hits = receiver.recv()? ;
|
|
||||||
|
|
||||||
Ok(hits.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_pixels_for_user(&self, user: &User) -> Result<Vec<TrackingPixel>, Error> {
|
|
||||||
let (sender, receiver) = oneshot::channel();
|
|
||||||
|
|
||||||
self.db_connection_sender.send(DBConnectionMessage::GetPixelsForUser(user.clone(), sender))?;
|
|
||||||
|
|
||||||
let pixels = receiver.recv()?;
|
|
||||||
|
|
||||||
|
|
||||||
Ok(pixels.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_pixel_from_encoded(&self, encoded_id: &str, user: Option<&User>) -> Result<Option<TrackingPixel>, Error> {
|
|
||||||
let id = TrackingPixel::url_safe_decode_id(encoded_id)?;
|
|
||||||
|
|
||||||
self.get_pixel_from_id(id, user)
|
|
||||||
}
|
|
||||||
fn get_pixel_from_id(&self, pixel_id: i64, user: Option<&User>) -> Result<Option<TrackingPixel>, Error> {
|
|
||||||
let (sender, receiver) = oneshot::channel();
|
|
||||||
|
|
||||||
self.db_connection_sender.send(DBConnectionMessage::GetPixel(pixel_id, user.cloned(), sender))?;
|
|
||||||
|
|
||||||
Ok(receiver.recv()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// stores a version of the pixel manager struct with only a sender, which may be cloned and used in multiple threads
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PixelManagerDelegate {
|
|
||||||
manager: PixelManager
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PixelManagerDelegate {
|
|
||||||
pub fn new(pixel_manager: &PixelManager) -> Self {
|
|
||||||
let internal_pixel_manager = PixelManager {
|
|
||||||
db_connection_sender: pixel_manager.db_connection_sender.clone(),
|
|
||||||
db_connection: None
|
|
||||||
};
|
|
||||||
Self {manager: internal_pixel_manager}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_manager(&self) -> &PixelManager {
|
|
||||||
&self.manager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for PixelManagerDelegate {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let internal_pixel_manager = PixelManager {
|
|
||||||
db_connection_sender: self.manager.db_connection_sender.clone(),
|
|
||||||
db_connection: None
|
|
||||||
};
|
|
||||||
Self {manager: internal_pixel_manager}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue