use crate::proxy::HSTS;
use crate::Config;
use axum::extract::{Path, State};
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
use diesel_async::RunQueryDsl;
use hyper;
use hyper::header::*;
use hyper::StatusCode;
use rand::distr::StandardUniform;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaChaRng;
use serde_derive::*;
use std;
use std::f64::consts::SQRT_2;
use std::io::Write;
use tracing::*;
const N_POINTS: usize = 10;
const MARGIN: f64 = 10.;
const SCALE: f64 = 200.;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Size {
Full,
Small,
}
const QUESTION: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-circle" viewBox="1 1 14 14">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
</svg>"#;
pub fn identicon() -> axum::Router<Config> {
axum::Router::new()
.route("/{login}", axum::routing::get(identicon_full))
.route("/{login}/small", axum::routing::get(identicon_small))
}
#[derive(Debug, Deserialize)]
struct Identicon {
login: String,
}
type Body = http_body_util::Full<bytes::Bytes>;
#[axum::debug_handler]
async fn identicon_full(
config: State<Config>,
name: Path<Identicon>,
req: axum::extract::Request,
) -> hyper::Response<Body> {
identicon_(Size::Full, config, name, req).await.unwrap()
}
#[axum::debug_handler]
async fn identicon_small(
config: State<Config>,
name: Path<Identicon>,
req: axum::extract::Request,
) -> hyper::Response<Body> {
identicon_(Size::Small, config, name, req).await.unwrap()
}
async fn identicon_(
size: Size,
State(config): State<Config>,
Path(name): Path<Identicon>,
req: axum::extract::Request,
) -> Result<hyper::Response<Body>, crate::Error> {
use crate::db::users::dsl as users;
let mut db_ = config.db.get().await.unwrap();
let uid = if let Some(user) = users::users
.filter(users::login.eq(&name.login))
.select(users::id)
.get_result::<uuid::Uuid>(&mut db_)
.await
.optional()
.unwrap()
{
user
} else {
if let Some(ifmodified) = req
.headers()
.get(IF_MODIFIED_SINCE)
.and_then(|ifmodified| ifmodified.to_str().ok())
.and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
{
if config.version_time <= ifmodified + std::time::Duration::from_secs(1) {
let now = std::time::SystemTime::now();
return Ok(hyper::Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(LAST_MODIFIED, &config.version_time_str)
.header(DATE, httpdate::fmt_http_date(now))
.body("".into())?);
}
}
return Ok(hyper::Response::builder()
.status(StatusCode::OK)
.header(LAST_MODIFIED, config.version_time_str.as_str())
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(CONTENT_TYPE, "image/svg+xml")
.body(QUESTION.into())?);
};
debug!("id: {:?}", uid);
use crate::db::profile_pics::dsl as profile_pics;
if let Some((mime, bytes, modif)) = (if let Size::Full = size {
profile_pics::profile_pics
.find(uid)
.select((
profile_pics::format,
profile_pics::image,
profile_pics::updated,
))
.get_result(&mut db_)
} else {
profile_pics::profile_pics
.find(uid)
.select((
profile_pics::format,
profile_pics::small,
profile_pics::updated,
))
.get_result::<(String, Vec<u8>, chrono::DateTime<chrono::Utc>)>(&mut db_)
})
.await
.optional()?
{
let modif: std::time::SystemTime = modif.into();
if let Some(ifmodified) = req
.headers()
.get(IF_MODIFIED_SINCE)
.and_then(|ifmodified| ifmodified.to_str().ok())
.and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
{
if modif > ifmodified {
let now = std::time::SystemTime::now();
return Ok(hyper::Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(LAST_MODIFIED, httpdate::fmt_http_date(modif))
.header(DATE, httpdate::fmt_http_date(now))
.body("".into())?);
}
}
return Ok(hyper::Response::builder()
.status(StatusCode::OK)
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(CONTENT_TYPE, &mime)
.header(LAST_MODIFIED, httpdate::fmt_http_date(modif))
.body(bytes.into())?);
}
let now = std::time::SystemTime::now();
if let Some(ifmodified) = req
.headers()
.get(IF_MODIFIED_SINCE)
.and_then(|ifmodified| ifmodified.to_str().ok())
.and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
{
if config.version_time <= ifmodified + std::time::Duration::from_secs(1) {
return Ok(hyper::Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(LAST_MODIFIED, &config.version_time_str)
.header(DATE, httpdate::fmt_http_date(now))
.body("".into())?);
}
}
Ok(hyper::Response::builder()
.status(StatusCode::OK)
.header(LAST_MODIFIED, config.version_time_str.as_str())
.header(CONTENT_TYPE, "image/svg+xml")
.header(STRICT_TRANSPORT_SECURITY, HSTS)
.header(DATE, httpdate::fmt_http_date(now))
.body(make_identicon(name.login.as_bytes()).into())?)
}
fn make_identicon(user: &[u8]) -> Vec<u8> {
let seed = seed_bytes(user);
let rng: ChaChaRng = SeedableRng::from_seed(seed);
let mut points = [(0., 0.); N_POINTS];
for ((x, y), p) in rng
.sample_iter(&StandardUniform)
.scan(
(0., 0.),
|&mut (ref mut x0, ref mut y0), (x, y): (f64, f64)| {
let x: f64 = (x * 2.) - 1.;
let y: f64 = (y * 2.) - 1.;
let d = (x * x + y * y).sqrt();
*x0 = *x0 + x / d;
*y0 = *y0 + y / d;
Some((*x0, *y0))
},
)
.zip(points.iter_mut())
{
*p = (x, y)
}
let (extr_x, extr_y) = bezier_extr(points[0], points[1], points[2]);
let mut min_x = points[0].0.min(points[2].0).min(extr_x);
let mut min_y = points[0].1.min(points[2].1).min(extr_y);
let mut max_x = points[0].0.max(points[2].0).max(extr_x);
let mut max_y = points[0].1.max(points[2].1).max(extr_y);
let mut mid = points[1];
for (&(x0, y0), &(x1, y1)) in points.iter().skip(2).zip(points.iter().skip(3)) {
mid.0 = x0 + (x0 - mid.0);
mid.1 = y0 + (y0 - mid.1);
let (m0, m1) = bezier_extr((x0, y0), mid, (x1, y1));
min_x = min_x.min(m0).min(x1);
min_y = min_y.min(m1).min(y1);
max_x = max_x.max(m0).max(x1);
max_y = max_y.max(m1).max(y1);
}
let max = (max_x - min_x).max(max_y - min_y);
let scale = SQRT_2 * max;
let margin_x = (max - (max_x - min_x)) / 2. - min_x;
let margin_y = (max - (max_y - min_y)) / 2. - min_y;
for &mut (ref mut x, ref mut y) in points.iter_mut() {
*x = MARGIN + SCALE * ((1. - 1. / SQRT_2) / 2. + ((margin_x + *x) / scale));
*y = MARGIN + SCALE * ((1. - 1. / SQRT_2) / 2. + ((margin_y + *y) / scale));
}
let mut s:Vec<u8> = format!(r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision"><path d=""#, SCALE + MARGIN * 2., SCALE + MARGIN * 2.).into();
write!(
s,
"M {} {} Q {} {}, {} {} ",
points[0].0.round(),
points[0].1.round(),
points[1].0.round(),
points[1].1.round(),
points[2].0.round(),
points[2].1.round()
)
.unwrap();
for &(x, y) in points.iter().skip(3) {
write!(s, "T {} {} ", x.round(), y.round()).unwrap();
}
write!(
s,
"\" stroke=\"black\" stroke-width=\"3\" fill=\"transparent\"/></svg>"
)
.unwrap();
debug!("s: {:?}", std::str::from_utf8(&s));
s
}
fn bezier_extr(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> (f64, f64) {
(bezier_extr_1(a.0, b.0, c.0), bezier_extr_1(a.1, b.1, c.1))
}
fn bezier_extr_1(a: f64, b: f64, c: f64) -> f64 {
let u = a - 2. * b + c;
let v = 2. * (b - a);
let w = a;
let t = ((-v) / (2. * u)).min(1.).max(0.);
u * t * t + v * t + w
}
fn seed_bytes(from: &[u8]) -> [u8; 32] {
let mut u = [0; 32];
let l = from.len();
for i in 0..32 {
u[i] = from[i % l];
}
u
}