samjeston / openhub Public
Sam Jeston Working vertical 9aa3c45 · History
145 lines· 4.3 KB ·Rust
Raw
1//! Input validation for names and keys.
2
3use russh::keys::ssh_key::{HashAlg, PublicKey};
4
5/// Usernames that would collide with top-level routes.
6const RESERVED: &[&str] = &[
7 "admin",
8 "api",
9 "assets",
10 "explore",
11 "favicon.svg",
12 "git",
13 "help",
14 "login",
15 "logout",
16 "new",
17 "register",
18 "root",
19 "settings",
20 "signin",
21 "signup",
22 "static",
23 "system",
24];
25
26pub fn username(name: &str) -> Result<(), &'static str> {
27 if name.is_empty() || name.len() > 39 {
28 return Err("Username must be between 1 and 39 characters.");
29 }
30 if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
31 return Err("Username may only contain letters, numbers and hyphens.");
32 }
33 if name.starts_with('-') || name.ends_with('-') || name.contains("--") {
34 return Err("Username cannot start or end with a hyphen or contain consecutive hyphens.");
35 }
36 if RESERVED.contains(&name.to_ascii_lowercase().as_str()) {
37 return Err("That username is reserved.");
38 }
39 Ok(())
40}
41
42pub fn repo_name(name: &str) -> Result<(), &'static str> {
43 if name.is_empty() || name.len() > 100 {
44 return Err("Repository name must be between 1 and 100 characters.");
45 }
46 if !name
47 .chars()
48 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
49 {
50 return Err("Repository name may only contain letters, numbers, '-', '_' and '.'.");
51 }
52 if name.starts_with('.') || name.to_ascii_lowercase().ends_with(".git") {
53 return Err("Repository name cannot start with '.' or end with '.git'.");
54 }
55 Ok(())
56}
57
58pub fn email(email: &str) -> Result<(), &'static str> {
59 let ok = email.len() <= 254
60 && email
61 .split_once('@')
62 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
63 && !email.chars().any(char::is_whitespace);
64 if ok {
65 Ok(())
66 } else {
67 Err("Please enter a valid email address.")
68 }
69}
70
71pub fn password(password: &str) -> Result<(), &'static str> {
72 if password.chars().count() < 8 {
73 return Err("Password must be at least 8 characters.");
74 }
75 if password.len() > 256 {
76 return Err("Password is too long.");
77 }
78 Ok(())
79}
80
81pub struct ParsedKey {
82 pub normalized: String,
83 pub fingerprint: String,
84 pub key_type: String,
85 pub comment: String,
86}
87
88/// Parses an OpenSSH public key line, e.g. `ssh-ed25519 AAAA... me@laptop`.
89pub fn ssh_public_key(input: &str) -> Result<ParsedKey, &'static str> {
90 let line = input.trim();
91 if line.contains('\n') {
92 return Err("Please paste a single public key.");
93 }
94 if line.contains("PRIVATE KEY") {
95 return Err("That looks like a private key. Paste the contents of the .pub file instead.");
96 }
97 let key = PublicKey::from_openssh(line)
98 .map_err(|_| "Key is invalid. Paste an OpenSSH public key such as the contents of ~/.ssh/id_ed25519.pub.")?;
99 let comment = key.comment().as_str_lossy().to_string();
100 let mut bare = key.clone();
101 bare.set_comment("");
102 let normalized = bare.to_openssh().map_err(|_| "Key could not be encoded.")?;
103 Ok(ParsedKey {
104 normalized: normalized.trim().to_string(),
105 fingerprint: key.fingerprint(HashAlg::Sha256).to_string(),
106 key_type: key.algorithm().as_str().to_string(),
107 comment,
108 })
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn usernames() {
117 assert!(username("alice").is_ok());
118 assert!(username("a-b-c").is_ok());
119 assert!(username("-alice").is_err());
120 assert!(username("al--ice").is_err());
121 assert!(username("settings").is_err());
122 assert!(username("ali ce").is_err());
123 }
124
125 #[test]
126 fn repo_names() {
127 assert!(repo_name("openhub").is_ok());
128 assert!(repo_name("my_repo.rs").is_ok());
129 assert!(repo_name("..").is_err());
130 assert!(repo_name("x.git").is_err());
131 assert!(repo_name("a/b").is_err());
132 }
133
134 #[test]
135 fn parses_ed25519_key() {
136 let k = ssh_public_key(
137 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMys5dTzxVmfRH761T646Bpe3nX9Hx86atVkOotoyxWQ me@host",
138 )
139 .unwrap();
140 assert!(k.fingerprint.starts_with("SHA256:"));
141 assert_eq!(k.key_type, "ssh-ed25519");
142 assert_eq!(k.comment, "me@host");
143 assert!(ssh_public_key("not a key").is_err());
144 }
145}