first commit
This commit is contained in:
commit
19dfc51360
85 files changed
+25504
No files matched your search
+276
@@ -0,0 +1,276 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use rand::RngCore;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub fn now() -> i64 {
|
||||
chrono::Utc::now().timestamp()
|
||||
}
|
||||
|
||||
pub fn random_token(bytes: usize) -> String {
|
||||
let mut b = vec![0u8; bytes];
|
||||
rand::rngs::OsRng.fill_bytes(&mut b);
|
||||
hex::encode(b)
|
||||
}
|
||||
|
||||
pub fn sha256_hex(data: &[u8]) -> String {
|
||||
hex::encode(Sha256::digest(data))
|
||||
}
|
||||
|
||||
pub fn ct_eq(a: &[u8], b: &[u8]) -> bool {
|
||||
use subtle::ConstantTimeEq;
|
||||
a.len() == b.len() && bool::from(a.ct_eq(b))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Cidr {
|
||||
addr: IpAddr,
|
||||
prefix: u8,
|
||||
}
|
||||
|
||||
impl Cidr {
|
||||
pub fn parse(s: &str) -> Option<Cidr> {
|
||||
let s = s.trim();
|
||||
let (a, p) = match s.split_once('/') {
|
||||
Some((a, p)) => (a, Some(p)),
|
||||
None => (s, None),
|
||||
};
|
||||
let addr: IpAddr = a.parse().ok()?;
|
||||
let max = if addr.is_ipv4() { 32 } else { 128 };
|
||||
let prefix = match p {
|
||||
Some(p) => p.parse().ok().filter(|x: &u8| *x <= max)?,
|
||||
None => max,
|
||||
};
|
||||
Some(Cidr { addr, prefix })
|
||||
}
|
||||
|
||||
pub fn contains(&self, ip: &IpAddr) -> bool {
|
||||
let ip = normalize(*ip);
|
||||
match (self.addr, ip) {
|
||||
(IpAddr::V4(n), IpAddr::V4(i)) => {
|
||||
let mask = if self.prefix == 0 {
|
||||
0
|
||||
} else {
|
||||
u32::MAX << (32 - self.prefix)
|
||||
};
|
||||
(u32::from(n) & mask) == (u32::from(i) & mask)
|
||||
}
|
||||
(IpAddr::V6(n), IpAddr::V6(i)) => {
|
||||
let mask = if self.prefix == 0 {
|
||||
0
|
||||
} else {
|
||||
u128::MAX << (128 - self.prefix)
|
||||
};
|
||||
(u128::from(n) & mask) == (u128::from(i) & mask)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize(ip: IpAddr) -> IpAddr {
|
||||
match ip {
|
||||
IpAddr::V6(v6) => v6.to_ipv4_mapped().map(IpAddr::V4).unwrap_or(ip),
|
||||
v4 => v4,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ip_in_any(ip: &IpAddr, nets: &[String]) -> bool {
|
||||
nets.iter()
|
||||
.filter_map(|n| Cidr::parse(n))
|
||||
.any(|c| c.contains(ip))
|
||||
}
|
||||
|
||||
pub fn html_escape(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'&' => out.push_str("&"),
|
||||
'<' => out.push_str("<"),
|
||||
'>' => out.push_str(">"),
|
||||
'"' => out.push_str("""),
|
||||
'\'' => out.push_str("'"),
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn xml_escape(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'&' => out.push_str("&"),
|
||||
'<' => out.push_str("<"),
|
||||
'>' => out.push_str(">"),
|
||||
'"' => out.push_str("""),
|
||||
'\'' => out.push_str("'"),
|
||||
c if (c as u32) < 0x20 && c != '\n' && c != '\r' && c != '\t' => {}
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Truncate a string to at most `n` bytes on a char boundary.
|
||||
pub fn truncate_bytes(s: &str, n: usize) -> &str {
|
||||
if s.len() <= n {
|
||||
return s;
|
||||
}
|
||||
let mut i = n;
|
||||
while i > 0 && !s.is_char_boundary(i) {
|
||||
i -= 1;
|
||||
}
|
||||
&s[..i]
|
||||
}
|
||||
|
||||
/// Convert HTML to readable plain text (used when a device wants text but only HTML exists).
|
||||
pub fn html_to_text(html: &str) -> String {
|
||||
let mut out = String::with_capacity(html.len() / 2);
|
||||
let lower = html.to_ascii_lowercase();
|
||||
let bytes = html.as_bytes();
|
||||
let mut i = 0;
|
||||
let mut skip_until: Option<&str> = None;
|
||||
while i < bytes.len() {
|
||||
if let Some(end) = skip_until {
|
||||
if lower[i..].starts_with(end) {
|
||||
skip_until = None;
|
||||
i += end.len();
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if bytes[i] == b'<' {
|
||||
let close = match html[i..].find('>') {
|
||||
Some(c) => i + c,
|
||||
None => break,
|
||||
};
|
||||
let tag = lower[i + 1..close].trim_start_matches('/');
|
||||
let name: String = tag
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_alphanumeric())
|
||||
.collect();
|
||||
match name.as_str() {
|
||||
"script" => skip_until = Some("</script>"),
|
||||
"style" => skip_until = Some("</style>"),
|
||||
"head" => skip_until = Some("</head>"),
|
||||
"br" => out.push('\n'),
|
||||
"p" | "div" | "tr" | "li" | "h1" | "h2" | "h3" | "h4" | "blockquote" | "table" => {
|
||||
if !out.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
if name == "li" && !lower[i + 1..].starts_with('/') {
|
||||
out.push_str("• ");
|
||||
}
|
||||
}
|
||||
"td" | "th" => out.push('\t'),
|
||||
_ => {}
|
||||
}
|
||||
i = close + 1;
|
||||
continue;
|
||||
}
|
||||
if bytes[i] == b'&' {
|
||||
if let Some(semi) = html[i..].find(';').filter(|s| *s < 10) {
|
||||
let ent = &html[i + 1..i + semi];
|
||||
let rep = match ent {
|
||||
"nbsp" => Some(' '),
|
||||
"amp" => Some('&'),
|
||||
"lt" => Some('<'),
|
||||
"gt" => Some('>'),
|
||||
"quot" => Some('"'),
|
||||
"apos" | "#39" => Some('\''),
|
||||
e if e.starts_with("#x") => u32::from_str_radix(&e[2..], 16)
|
||||
.ok()
|
||||
.and_then(char::from_u32),
|
||||
e if e.starts_with('#') => e[1..].parse().ok().and_then(char::from_u32),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(c) = rep {
|
||||
out.push(c);
|
||||
i += semi + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let ch = html[i..].chars().next().unwrap_or(' ');
|
||||
if ch == '\r' || ch == '\n' || ch == '\t' {
|
||||
if !out.ends_with(' ') && !out.ends_with('\n') {
|
||||
out.push(' ');
|
||||
}
|
||||
} else {
|
||||
out.push(ch);
|
||||
}
|
||||
i += ch.len_utf8();
|
||||
}
|
||||
// collapse 3+ newlines
|
||||
let mut res = String::with_capacity(out.len());
|
||||
let mut nl = 0;
|
||||
for c in out.chars() {
|
||||
if c == '\n' {
|
||||
nl += 1;
|
||||
if nl > 2 {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
nl = 0;
|
||||
}
|
||||
res.push(c);
|
||||
}
|
||||
res.trim().to_string()
|
||||
}
|
||||
|
||||
pub fn text_to_html(text: &str) -> String {
|
||||
let mut out = String::from("<html><head><meta charset=\"utf-8\"></head><body><div style=\"white-space:pre-wrap;font-family:sans-serif\">");
|
||||
out.push_str(&html_escape(text));
|
||||
out.push_str("</div></body></html>");
|
||||
out
|
||||
}
|
||||
|
||||
/// Validate a login name received from a device.
|
||||
pub fn valid_login(s: &str) -> bool {
|
||||
!s.is_empty()
|
||||
&& s.len() <= 254
|
||||
&& s.chars().all(|c| {
|
||||
!c.is_control()
|
||||
&& !c.is_whitespace()
|
||||
&& !matches!(c, '/' | '\\' | '"' | '\'' | '<' | '>' | '%' | '*' | '?')
|
||||
})
|
||||
}
|
||||
|
||||
pub fn format_ts(ts: i64) -> String {
|
||||
chrono::DateTime::from_timestamp(ts, 0)
|
||||
.map(|d| d.format("%Y-%m-%d %H:%M:%S").to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cidr() {
|
||||
let c = Cidr::parse("10.0.0.0/8").unwrap();
|
||||
assert!(c.contains(&"10.2.3.4".parse().unwrap()));
|
||||
assert!(!c.contains(&"11.2.3.4".parse().unwrap()));
|
||||
let l = Cidr::parse("127.0.0.1").unwrap();
|
||||
assert!(l.contains(&"::ffff:127.0.0.1".parse().unwrap()));
|
||||
assert!(Cidr::parse("::1/128")
|
||||
.unwrap()
|
||||
.contains(&"::1".parse().unwrap()));
|
||||
assert!(Cidr::parse("1.2.3.4/33").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html2text() {
|
||||
let t = html_to_text("<html><head><style>x{}</style></head><body><p>Hello <b>World</b></p><br>a & b</body></html>");
|
||||
assert_eq!(t, "Hello World\n\na & b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logins() {
|
||||
assert!(valid_login("jan.kowalski@firma.pl"));
|
||||
assert!(!valid_login("a b"));
|
||||
assert!(!valid_login("../etc"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user