Compare commits

...

21 Commits

Author SHA1 Message Date
9fa66899aa remove error 2024-10-22 09:53:56 +02:00
8a48ca0ad6 the real users fix 2024-10-22 09:51:54 +02:00
26301ac344 more fix 2024-10-22 09:33:29 +02:00
81023f2042 Add user status & fix get user 2024-10-22 09:29:43 +02:00
f5f35855ce Del duplicates in code 2024-07-15 08:43:36 +03:00
7018adf98d Little refactor 2024-07-15 08:37:45 +03:00
a60713a4b0 Fix all Errors n Warns 2024-07-15 08:30:27 +03:00
9ef4176091 Fatal broke (refactor) 2024-07-14 21:15:00 +03:00
6cd6b12630 Move a lot funcs... 2024-05-20 14:08:03 +05:00
2a9ac271a7 One more commit 2024-05-20 11:07:56 +05:00
8a7ff047b6 Move work to notebook 2024-04-13 12:52:15 +05:00
e28a8c2893 Worked v0 API 2024-04-12 21:49:53 +05:00
73f5edf77f Add session lifetime 2024-04-02 19:57:50 +05:00
c12d7fb01c Add session lifetime 2024-04-02 19:51:28 +05:00
e3229f41a4 Make working api auth 2024-03-25 22:33:03 +05:00
c877b02286 Finished refresh token and remove all warns 2024-03-23 22:45:32 +05:00
030dd43ddf I broke all :3 2024-03-22 19:41:12 +05:00
b577904d75 Correct time 2024-03-22 16:34:06 +05:00
aeceb98ad5 Minimize warnin's 2024-03-22 16:30:49 +05:00
6a64f87bc5 Add authorize page 2024-03-22 16:27:06 +05:00
0ad0a8cf0b API return OK (need make dynamic) 2024-03-22 15:24:15 +05:00
20 changed files with 1406 additions and 874 deletions

835
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "BitAuth"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -16,10 +16,11 @@ jsonwebtoken = "9.2.0"
rand = "0.8.5"
rsa = "0.9.6"
serde_json = "1.0.111"
skytable = "0.8.6"
skytable = "0.8.10"
tokio = { version = "1.35.1", features = ["full"] }
urlencoding = "2.1.3"
uuid = { version = "1.6.1", features = ["v4", "v5"] }
webhook = "2.1.2"
[[bin]]

27
TODO
View File

@ -1,3 +1,6 @@
Forward user back when set `back_url`
Auth using tokens
Auth using uniq id on device
Auth using QR code
@ -6,8 +9,26 @@ Auth using email
##### API v0
##### API v0 (unsafe)
# Auth link (device check url with this session and get data)
/auth?session=<UNIQUE>
/api/v0/auth?session=<UNIQUE>&back_url=<OPTIONAL>
# Get auth data
/auth_finish?session=<UNIQUE>
/api/v0/auth_finish?session=<UNIQUE>
##### API v1 (semi-safe)
# Auth link
/api/v1/auth?session=<UNIQUE>&back_url=<OPTIONAL>
# Get token
/api/v1/auth_finish?session=<UNIQUE>
# Refresh token
/api/v1/refresh?token=<REFRESH>
##### API v2 (safe)
# Auth link
/api/v2/auth?site_id=<REQUIRED>&session=<UNIQUE>&back_url=<OPTIONAL>&webhook=<OPTIONAL>
# Get token
/api/v2/auth_finish?site_id=<REQUIRED>&session=<UNIQUE>
# Refresh token
/api/v2/refresh?site_id=<REQUIRED>&token=<REFRESH>

View File

@ -1,42 +0,0 @@
use {
hyper::{
Request,
body::{
Incoming,
},
},
serde_json::{
Value as Json,
json,
},
skytable::pool::ConnectionMgrTcp,
bb8::Pool,
std::sync::Arc,
crate::double_split,
};
type Res<T, E> = std::result::Result<T, E>;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
type DBPool = Arc<Pool<ConnectionMgrTcp>>;
pub async fn api(req: Request<Incoming>, pool: DBPool) -> Json {
let uri: &str = req.uri().path().as_ref();
match &uri[7..uri.len()] {
"/test" => json!({"error": false, "msg": "test"}),
"/auth" => auth(req, pool.clone()).await,
"/auth_get" => auth_get(req, pool.clone()).await,
_ => json!({"error": true, "msg": "No endpoint"})
}
}
async fn auth(req: Request<Incoming>, pool: DBPool) -> Json {
json!({"error": false, "msg": "test auth endpoint v0"})
}
async fn auth_get(req: Request<Incoming>, pool: DBPool) -> Json {
let query = req.uri().query().or(Some("")).unwrap();
let query = double_split(query.to_string(), "&", "=");
println!("{:?}", query);
json!({"error": false, "msg": "test auth_get endpoint v0"})
}

417
src/funcs.rs Normal file
View File

@ -0,0 +1,417 @@
use {
std::{
collections::HashMap,
any::type_name,
time::{
SystemTime,
UNIX_EPOCH,
},
},
webhook::client::WebhookClient,
urlencoding::decode as url_decode,
uuid::Uuid,
chrono::DateTime,
serde_json::{
Value as Json,
Map as JsonMap,
json,
},
rsa::{
pkcs1::{
EncodeRsaPublicKey,
EncodeRsaPrivateKey,
},
RsaPrivateKey,
RsaPublicKey,
},
jsonwebtoken as jwt,
jwt::{
Header,
Algorithm,
TokenData,
Validation,
EncodingKey,
DecodingKey,
encode as jwt_encode,
decode as jwt_decode,
},
skytable::{
query,
error::Error as SkyError,
},
http_body_util::BodyExt,
hyper::{
Request,
HeaderMap,
body::Incoming,
header::{
HeaderValue,
},
},
crate::{
Res,
Result,
DBPool,
REFRESH_LIFETIME,
TOKEN_LIFETIME,
html::*,
Users,
Sites,
APIV0_LIFETIME,
},
};
#[allow(dead_code)]
pub fn type_of<T>(_: T) -> &'static str {
type_name::<T>()
}
pub fn uuid_v4() -> Uuid {
Uuid::new_v4()
}
pub fn build_html(body: &str) -> String {
format!("{}{}{}{}", HEADER_HTML, CSS3, body, FOOTER_HTML)
}
pub async fn init_tables(pool: DBPool) -> Res<(), SkyError> {
let mut con = pool.get().await.unwrap();
let _ = con.query_parse::<()>(&query!("CREATE SPACE IF NOT EXISTS bitauth")).await;
let _ = con.query_parse::<()>(&query!(r#"
CREATE MODEL IF NOT EXISTS bitauth.users(
uuid: string,
login: string,
password: string,
email: string,
tokens: list {type: string}
)
"#)).await;
let _ = con.query_parse::<()>(&query!(r#"
CREATE MODEL IF NOT EXISTS bitauth.users_uuid(
login: string,
uuid: string
)
"#)).await;
let _ = con.query_parse::<()>(&query!(r#"
CREATE MODEL IF NOT EXISTS bitauth.sites(
uuid: string,
uid: string,
domain: string,
pkey: binary,
skey: binary
)
"#)).await;
let _ = con.query_parse::<()>(&query!(r#"
CREATE MODEL IF NOT EXISTS bitauth.tokens(
uuid: string,
uid: string,
sid: string,
ref: string,
refend: uint32
)
"#)).await;
let _ = con.query_parse::<()>(&query!(r#"
CREATE MODEL IF NOT EXISTS bitauth.v0(
session: string,
login: string,
uuid: string,
expire: uint32
)
"#)).await;
let q = con.query_parse::<Sites>(&query!("SELECT * FROM bitauth.sites WHERE uuid = 0")).await;
if q.is_err() {
let (skey, pkey) = rsa_gen();
let _ = con.query_parse::<()>(&query!(
"INSERT INTO bitauth.sites { uuid: ?, uid: ?, domain: ?, pkey: ?, skey: ? }",
"0", "0", "", pkey, skey
)).await;
};
Ok(())
}
pub fn double_split(body: String, first: &str, second: &str) -> HashMap<String, String> {
body.split(first)
.filter_map(|c| {
c.split_once(second)
.map(|(l, r)| (
l.trim().to_owned(),
format!("{}", url_decode(r).expect("UTF-8")).trim().to_owned()
))
})
.collect::<HashMap<String, String>>()
}
pub fn time() -> u32 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as u32
}
pub fn time_mcs() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros()
}
pub async fn discord_wh_send(text: String) {
let url: &str = "https://discord.com/api/webhooks/1228566682902204537/RXaWpZplEGzU88O8c4mD4qzXp1PDBrrp93nGvdijaY7mBXp27xc0EsThHUeU0431PQOZ";
let client: WebhookClient = WebhookClient::new(url);
client.send(|message| message
.username("Bit.Auth")
// .avatar_url("")
.embed(|embed| embed
.title("Error")
.description(&text)
// .footer("Footer", Some(String::from(IMAGE_URL)))
// .image(IMAGE_URL)
// .thumbnail(IMAGE_URL)
// .author("bitheaven", Some(String::from(IMAGE_URL)), Some(String::from(IMAGE_URL)))
// .field("name", "value", false)
)
).await.unwrap();
}
pub fn set_cookie(headers: &mut HeaderMap, key: &str, value: &str) {
let time = DateTime::from_timestamp((time() + REFRESH_LIFETIME) as i64, 0)
.expect("REASON")
.to_rfc2822();
let time = time.replace("+0000", "GMT");
headers.append(
hyper::header::SET_COOKIE,
HeaderValue::from_str(format!("{}={}; HttpOnly; Expires={}", key, value, time).as_str())
.unwrap()
);
}
pub fn set_location(headers: &mut HeaderMap, location: &str) {
headers.append(
hyper::header::LOCATION,
HeaderValue::from_str(format!("{}", location).as_str()).unwrap()
);
}
pub async fn get_body_from_request(mut req: Request<Incoming>) -> Result<Vec<u8>> {
let mut body: Vec<u8> = vec![];
while let Some(next) = req.frame().await {
let frame = next?;
if let Some(chunk) = frame.data_ref() {
body.extend_from_slice(chunk);
}
}
Ok(body)
}
pub async fn login_user(pool: DBPool, data: HashMap<String, String>) -> Res<(String, String), SkyError> {
let mut ret: (String, String) = Default::default();
let mut con = pool.get().await.unwrap();
let login = &data.get("login").unwrap().trim().to_lowercase();
let pass = data.get("password").unwrap().trim();
let q = con
.query_parse::<(String,)>(&query!("SELECT uuid FROM bitauth.users_uuid WHERE login = ?", login))
.await;
if q.is_err() { println!("{:?}", q.err()); return Ok(ret); };
let (uuid,) = q.unwrap();
let q = con
.query_parse::<Users>(&query!("SELECT * FROM bitauth.users WHERE uuid = ?", uuid.clone()))
.await;
if q.is_err() {
discord_wh_send(
format!("Пизда тут, ну тип, да, какая-то ебень с uuid, чел потерялся...\n`{}`\n**{}**", uuid, login)
).await;
return Ok(ret);
}
// TODO: Say to user about trouble.
let q = q.unwrap();
if bcrypt::verify(pass, q.password.as_str()).unwrap() {
let token = format!("{}", uuid_v4().as_hyphenated());
let reftoken = format!("{}", uuid_v4().as_hyphenated());
let time = time();
let uuid: String = q.uuid;
let login: String = q.login;
let _ = con.query_parse::<()>(&query!(
"INSERT INTO bitauth.tokens { uuid: ?, uid: ?, sid: ?, ref: ?, refend: ? }",
token.clone(), uuid.clone(), "0", reftoken.clone(), time + REFRESH_LIFETIME
)).await;
let _ = con.query_parse::<()>(&query!(
"UPDATE bitauth.users SET tokens += ? WHERE login = ?",
token.clone(), login.clone()
)).await;
ret = (
jwt_sign(pool.clone(), json!({
"login": login.clone(),
"uuid": uuid.clone(),
"iat": time,
"exp": time + TOKEN_LIFETIME
})).await.unwrap(),
jwt_sign(pool.clone(), json!({
"uuid": token.clone(),
"iat": time,
"ref": reftoken.clone(),
"exp": time + REFRESH_LIFETIME
})).await.unwrap()
);
}
Ok(ret)
}
pub async fn jwt_sign(pool: DBPool, data: Json) -> Result<String> {
let mut con = pool.get().await.unwrap();
let q = con
.query_parse::<Sites>(&query!("SELECT * FROM bitauth.sites WHERE uuid = ?", "0"))
.await;
if q.is_err() { return Ok("".to_owned()) };
let skey = q.unwrap().skey;
let skey = EncodingKey::from_rsa_der(&skey);
let header = Header::new(Algorithm::RS512);
let token = jwt_encode(&header, &data, &skey)?;
Ok(token)
}
pub fn rsa_gen() -> (Vec<u8>, Vec<u8>) {
let mut rng = rand::thread_rng();
let bits = 2048;
let skey = RsaPrivateKey::new(&mut rng, bits).expect("RSA err");
let pkey = RsaPublicKey::from(&skey);
(
skey.to_pkcs1_der().unwrap().as_bytes().to_vec(),
pkey.to_pkcs1_der().unwrap().as_bytes().to_vec()
)
}
pub async fn jwt_verify(pool: DBPool, token: &str) -> Result<TokenData<Json>> {
let mut con = pool.get().await.unwrap();
let mut ret = TokenData { header: Header::new(Algorithm::RS512), claims: json![{}] };
let q = con
.query_parse::<Sites>(&query!("SELECT * FROM bitauth.sites WHERE uuid = ?", "0"))
.await;
if q.is_err() { return Ok(ret) };
let pkey = q.unwrap().pkey;
let pkey = DecodingKey::from_rsa_der(&pkey);
let token = jwt_decode::<Json>(&token, &pkey, &Validation::new(Algorithm::RS512));
match token.is_ok() {
true => ret = token.clone().unwrap(),
_ => {}
}
Ok(ret)
}
pub async fn create_user(pool: DBPool, data: HashMap<String, String>) -> Res<bool, SkyError> {
let mut ret = true;
let mut con = pool.get().await.unwrap();
let login = &data.get("login").unwrap().trim().to_lowercase();
let email = &data.get("email").unwrap().trim().to_lowercase();
let pass = data.get("password").unwrap().trim();
let pass2 = data.get("password2").unwrap().trim();
if pass != pass2 { ret = false };
if pass.len() < 8 { ret = false };
let q = con
.query_parse::<(String,)>(&query!("SELECT uuid FROM bitauth.users_uuid WHERE login = ?", login))
.await;
if q.is_ok() { ret = false };
if ret {
let uuid = format!("{}", uuid_v4().as_hyphenated());
let pass = bcrypt::hash(pass, 12).unwrap();
let q = con.query_parse::<()>(&query!(
r#"INSERT INTO bitauth.users {
uuid: ?,
login: ?,
password: ?,
email: ?,
tokens: []
}"#,
uuid.clone(),
login.clone(),
pass,
email,
)).await;
if q.is_err() { ret = false }
let q = con.query_parse::<()>(&query!(
r#"INSERT INTO bitauth.users_uuid {
login: ?,
uuid: ?
}"#,
login.clone(),
uuid.clone()
)).await;
if q.is_err() { ret = false }
}
Ok(ret)
}
pub async fn get_user(pool: DBPool, login: String) -> Res<(String,), SkyError> {
let mut con = pool.get().await.unwrap();
let q = con
.query_parse::<(String,)>(&query!("SELECT uuid FROM bitauth.users_uuid WHERE login = ?", login))
.await;
q
}
pub fn get_cookies(headers: HeaderMap) -> HashMap<String, String> {
let header = headers.get(hyper::header::COOKIE);
let cookies = match header.is_none() {
false => header.unwrap().to_str().unwrap(),
_ => ""
};
double_split(cookies.to_owned(), ";", "=")
}
pub async fn authorize_user(pool: DBPool, token: String, session: String) {
let mut con = pool.get().await.unwrap();
let data: JsonMap<String, Json> = jwt_verify(pool.clone(), &token)
.await
.unwrap()
.claims
.as_object()
.unwrap()
.clone();
let login = data.get("login").unwrap().as_str();
let uuid = data.get("uuid").unwrap().as_str();
let _ = con.query_parse::<()>(&query!(
r#"INSERT INTO bitauth.v0 {
session: ?,
login: ?,
uuid: ?,
expire: ?
}"#,
session,
login,
uuid,
time() + APIV0_LIFETIME
)).await;
}

View File

@ -7,6 +7,7 @@ pub const HEADER_HTML: &str = r#"
<a href="/">index</a>
<a href="/login">login</a>
<a href="/register">register</a>
<span>{USER_STATUS}</span>
<hr>
</header>
<main>
@ -16,7 +17,8 @@ pub const FOOTER_HTML: &str = r#"
</main>
<footer>
<hr>
Render time: RENDER_TIME&micro;s. Made by BitHeaven.
Page time: {PAGE_TIME}&micro;s.
Made by <a href="//bitheaven.ru/">BitHeaven</a>.
</footer>
</body>
</html>
@ -36,6 +38,15 @@ pub const LOGIN_HTML: &str = r#"
</form>
"#;
pub const AUTHORIZE_HTML: &str = r#"
<h1>authorize</h1>
<h2>you authorizing in unknown service</h2>
<h3>yes?</h3>
<form method="POST">
<button type="submit">yes</button>
</form>
"#;
pub const REG_HTML: &str = r#"
<h1>register</h1>
<form method="POST">
@ -58,7 +69,7 @@ pub const RECOVER_HTML: &str = "<h1>recover</h1>";
pub const NF_HTML: &str = "<h1>404</h1>think about it.";
pub const CSS3: &str = r#"<style>
:root { color-scheme: dark; }
:root { color-scheme: dark; font-family: monospace; font-size: 16px; }
body { margin: auto; max-width: 768px; }
footer { text-align: right; }
</style>"#;

View File

@ -1,6 +1,7 @@
mod funcs;
mod types;
mod html;
mod api;
mod url;
use {
std::{
@ -8,23 +9,14 @@ use {
net::SocketAddr,
collections::HashMap,
process::exit,
time::{
SystemTime,
UNIX_EPOCH,
},
},
chrono::{
DateTime,
},
http_body_util::{
Full,
BodyExt,
},
hyper::{
StatusCode,
Request,
Response,
Method,
HeaderMap,
header::HeaderValue,
body::{
@ -40,35 +32,10 @@ use {
tokio::{
net::TcpListener,
},
rsa::{
pkcs1::{
EncodeRsaPublicKey,
EncodeRsaPrivateKey,
},
RsaPrivateKey,
RsaPublicKey,
},
urlencoding::{
decode as url_decode,
},
jsonwebtoken as jwt,
jwt::{
Header,
Algorithm,
TokenData,
Validation,
EncodingKey,
DecodingKey,
encode as jwt_encode,
decode as jwt_decode,
},
serde_json::{
Value as Json,
json,
},
uuid::{
Uuid,
},
skytable::{
query,
Config,
@ -81,12 +48,16 @@ use {
bb8::{
Pool,
},
urlencoding::{
encode as url_encode,
},
crate::{
types::{
users::Users,
sites::Sites,
},
html::*,
funcs::*,
},
};
@ -96,17 +67,19 @@ type DBPool = Arc<Pool<ConnectionMgrTcp>>;
type FullBytes = Result<Response<Full<Bytes>>>;
const PORT: u16 = 8083;
const PORT: u16 = 8051;
const DB_POOL: u32 = 32;
const DB_ADDR: &str = "192.168.1.49";
const DB_ADDR: &str = "127.0.0.1";
const DB_PORT: u16 = 2003;
const DB_USER: &str = "root";
const DB_PASS: &str = "rootpass12345678";
const DB_PASS: &str = "dBk6wUAynGRRLsSF";
const TOKEN_LIFETIME: u32 = 300;
const REFRESH_LIFETIME: u32 = 2_678_400;
const APIV0_LIFETIME: u32 = 120;
#[tokio::main]
async fn main() -> Result<()> {
@ -159,7 +132,7 @@ async fn main() -> Result<()> {
}
async fn handle_connection(req: Request<Incoming>, pool: DBPool, ip: String) -> FullBytes {
let t = time_ns();
let t = time_mcs();
if ip == "1.1.1.1" {
}
@ -172,392 +145,134 @@ async fn handle_connection(req: Request<Incoming>, pool: DBPool, ip: String) ->
let mut headers = parts.headers.clone();
let cookies = get_cookies(req.headers().clone());
let token = cookies.get("token");
let token = match token.is_none() {
false => token.unwrap(),
_ => ""
let mut token = match cookies.get("token") {
x if x.is_none() => "".to_owned(),
x => x.unwrap().to_owned()
};
let mut logged = false;
match <str as AsRef<str>>::as_ref(req.uri().path()) {
x if x.starts_with("/api/") => {}
_ => {
// println!("{}", token);
x if x.starts_with("/api/") => {},
_ => 'jwt_check: {
if token == "" { break 'jwt_check; }
if token != "" && jwt_verify(pool.clone(), token)
.await?.claims.as_object().unwrap().len() == 0
{
println!("Invalid suka");
parts.status = StatusCode::FOUND;
set_cookie(&mut headers, "token", "");
set_location(&mut headers, "/");
parts.headers = headers;
return Ok(Response::from_parts(parts, Full::new(Bytes::new())));
let is_live = jwt_verify(pool.clone(), &token)
.await?
.claims
.as_object()
.unwrap()
.len() != 0;
match is_live {
true => { logged = true; },
_ => 'ref_check: {
let reftoken = cookies.get("refresh");
let reftoken = match reftoken.is_none() {
false => reftoken.unwrap(),
_ => ""
};
if reftoken == "" { break 'ref_check; }
let ref_data: HashMap<String, Json> = jwt_verify(pool.clone(), reftoken)
.await?
.claims
.as_object()
.unwrap()
.clone()
.into_iter()
.collect();
if ref_data.clone().len() != 0 {
let mut con = pool.get().await.unwrap();
let tokenid = ref_data.get("uuid").unwrap().as_str().unwrap();
let newref = format!("{}", uuid_v4().as_hyphenated());
let time = time();
let q = con.query_parse::<(String,)>(&query!(
r#"
SELECT uid
FROM bitauth.tokens
WHERE uuid = ?
"#,
tokenid
)).await;
if q.is_err() { break 'ref_check; }
let (uuid,) = q.unwrap();
let (login,) = con.query_parse::<(String,)>(&query!(
r#"
SELECT login
FROM bitauth.users
WHERE uuid = ?
"#,
uuid.clone()
)).await?;
let _ = con.query_parse::<()>(&query!(
r#"
UPDATE bitauth.tokens
SET ref = ?, refend = ?
WHERE uuid = ?
"#,
newref.clone(), time + REFRESH_LIFETIME, tokenid
)).await;
token = jwt_sign(pool.clone(), json!({
"login": login.clone(),
"uuid": uuid.clone(),
"iat": time,
"exp": time + TOKEN_LIFETIME
})).await.unwrap();
set_cookie(&mut headers, "token", &token);
set_cookie(&mut headers, "refresh",
&jwt_sign(pool.clone(), json!({
"uuid": tokenid,
"iat": time,
"ref": newref.clone(),
"exp": time + REFRESH_LIFETIME
})).await.unwrap()
);
logged = true;
}
}
}
}
}
(body, parts.status, restype) = match req.uri().path().as_ref() {
"/" => uri_index(),
"/cabinet" => uri_login(req, pool.clone(), &mut headers).await?,
"/login" => uri_login(req, pool.clone(), &mut headers).await?,
"/register" => uri_register(req, pool.clone(), &mut headers).await?,
"/recover" => uri_recover(),
x if x.starts_with("/@") => uri_user(req, pool.clone()).await?,
x if x.starts_with("/api/") => api::endpoint(req, pool.clone()).await,
_ => uri_404()
"/" => url::index::index(),
"/cabinet" => url::login::login(req, pool.clone(), &mut headers).await?,
"/login" => url::login::login(req, pool.clone(), &mut headers).await?,
x if x == "/authorize" && logged => url::authorize::authorize(req, pool.clone(), token).await?,
"/authorize" => uri_auth_required(req, &mut headers).await?,
"/register" => url::register::register(req, pool.clone(), &mut headers).await?,
"/recover" => url::recover::recover(),
x if x.starts_with("/@") => url::user::user(req, pool.clone()).await?,
x if x.starts_with("/api/") => url::api::endpoint(req, pool.clone()).await,
_ => url::nf::nf()
};
headers.insert(hyper::header::CONTENT_TYPE, restype);
parts.headers = headers;
let body = body.replace("RENDER_TIME", &format!("{}", time_ns() - t));
let user_status = match logged {
true => "AUTHORIZED",
_ => "NOT AUTHORIZED"
};
let body = body.replace("{USER_STATUS}", user_status);
let body = body.replace("{PAGE_TIME}", &format!("{}", time_mcs() - t));
Ok(Response::from_parts(parts, Full::new(Bytes::from(body))))
}
fn build_html(body: &str) -> String {
format!("{}{}{}{}", HEADER_HTML, CSS3, body, FOOTER_HTML)
}
fn set_cookie(headers: &mut HeaderMap, key: &str, value: &str) {
let time = DateTime::from_timestamp((time() + REFRESH_LIFETIME) as i64, 0)
.expect("REASON")
.to_rfc2822();
let time = time.replace("+0000", "GMT");
headers.append(
hyper::header::SET_COOKIE,
HeaderValue::from_str(format!("{}={}; HttpOnly; Expires={}", key, value, time).as_str())
.unwrap()
);
}
async fn uri_auth_required(req: Request<Incoming>, headers: &mut HeaderMap) -> Result<(String, StatusCode, HeaderValue)> {
let url = url_encode(req.uri().path_and_query().unwrap().as_str());
fn get_cookies(headers: HeaderMap) -> HashMap<String, String> {
let header = headers.get(hyper::header::COOKIE);
let cookies = match header.is_none() {
false => header.unwrap().to_str().unwrap(),
_ => ""
};
double_split(cookies.to_owned(), ";", "=")
}
fn set_location(headers: &mut HeaderMap, location: &str) {
headers.append(
hyper::header::LOCATION,
HeaderValue::from_str(format!("{}", location).as_str()).unwrap()
);
}
async fn uri_login(req: Request<Incoming>, pool: DBPool, headers: &mut HeaderMap) -> Result<(String, StatusCode, HeaderValue)> {
if *req.method() == Method::POST {
let body = get_body_from_request(req).await?;
let body = String::from_utf8(body).unwrap();
let body = double_split(body, "&", "=");
let (access, refresh) = login_user(pool.clone(), body).await?;
set_cookie(headers, "token", &access);
set_cookie(headers, "refresh", &refresh);
}
set_location(headers, format!("/login?q={}", url).as_str());
let restype: HeaderValue = "text/html".parse().unwrap();
Ok((build_html(LOGIN_HTML), StatusCode::OK, restype))
}
fn uri_index() -> (String, StatusCode, HeaderValue) {
let restype: HeaderValue = "text/html".parse().unwrap();
(build_html(INDEX_HTML), StatusCode::OK, restype)
}
async fn uri_register(req: Request<Incoming>, pool: DBPool, headers: &mut HeaderMap) -> Result<(String, StatusCode, HeaderValue)> {
let mut body = "".to_owned();
let mut status = StatusCode::OK;
let restype: HeaderValue = "text/html".parse().unwrap();
if *req.method() == Method::POST {
let request = get_body_from_request(req).await?;
let request = String::from_utf8(request).unwrap();
let request = double_split(request, "&", "=");
match create_user(pool.clone(), request).await? {
true => {
println!("Created");
set_location(headers, "/login");
},
_ => {
println!("Failed");
set_location(headers, "/register");
}
}
status = StatusCode::FOUND;
}
else {
body = build_html(REG_HTML);
}
Ok((body, status, restype))
}
fn uri_recover() -> (String, StatusCode, HeaderValue) {
let restype: HeaderValue = "text/html".parse().unwrap();
(build_html(RECOVER_HTML), StatusCode::OK, restype)
}
async fn uri_user(req: Request<Incoming>, pool: DBPool) -> Res<(String, StatusCode, HeaderValue), SkyError> {
let uri: &str = req.uri().path().as_ref();
let login = &uri[2..uri.len()];
let body: String;
let user = get_user(pool, login.to_string()).await;
if user.is_ok() {
body = format!("{}", user?.uuid);
}
else {
body = "Not fond :(".to_owned();
}
let restype: HeaderValue = "text/html".parse().unwrap();
Ok((build_html(&body), StatusCode::OK, restype))
}
fn uri_404() -> (String, StatusCode, HeaderValue) {
let restype: HeaderValue = "text/html".parse().unwrap();
(build_html(NF_HTML), StatusCode::NOT_FOUND, restype)
}
async fn init_tables(pool: DBPool) -> Res<(), SkyError> {
let mut con = pool.get().await.unwrap();
let _ = con.query_parse::<()>(&query!("CREATE SPACE IF NOT EXISTS bitauth")).await;
let _ = con.query_parse::<()>(&query!(r#"
CREATE MODEL IF NOT EXISTS bitauth.users(
login: string,
uuid: string,
password: string,
email: string,
tokens: list {type: string}
)
"#)).await;
let _ = con.query_parse::<()>(&query!(r#"
CREATE MODEL IF NOT EXISTS bitauth.sites(
uuid: string,
uid: string,
domain: string,
pkey: binary,
skey: binary
)
"#)).await;
let _ = con.query_parse::<()>(&query!(r#"
CREATE MODEL IF NOT EXISTS bitauth.tokens(
uuid: string,
uid: string,
sid: string,
ref: string,
refend: uint32
)
"#)).await;
let q = con.query_parse::<Sites>(&query!("SELECT * FROM bitauth.sites WHERE uuid = 0")).await;
if q.is_err() {
let (skey, pkey) = rsa_gen();
let _ = con.query_parse::<()>(&query!(
"INSERT INTO bitauth.sites { uuid: ?, uid: ?, domain: ?, pkey: ?, skey: ? }",
"0", "0", "", pkey, skey
)).await;
};
Ok(())
}
async fn get_user(pool: DBPool, login: String) -> Res<Users, SkyError> {
let mut con = pool.get().await.unwrap();
let q = con
.query_parse::<Users>(&query!("SELECT * FROM bitauth.users WHERE login = ?", login))
.await;
Ok(q?)
}
async fn login_user(pool: DBPool, data: HashMap<String, String>) -> Res<(String, String), SkyError> {
let mut err = false;
let mut ret: (String, String) = Default::default();
let mut con = pool.get().await.unwrap();
let login = &data.get("login").unwrap().trim().to_lowercase();
let pass = data.get("password").unwrap().trim();
let q = con
.query_parse::<Users>(&query!("SELECT * FROM bitauth.users WHERE login = ?", login))
.await;
if q.is_err() { err = true };
if !err {
let q = q.unwrap();
if bcrypt::verify(pass, q.password.as_str()).unwrap() {
let token = format!("{}", uuid_v4().as_hyphenated());
let reftoken = format!("{}", uuid_v4().as_hyphenated());
let time = time();
let uuid: String = q.uuid;
let login: String = q.login;
let _ = con.query_parse::<()>(&query!(
"INSERT INTO bitauth.tokens { uuid: ?, uid: ?, sid: ?, ref: ?, refend: ? }",
token.clone(), uuid.clone(), "0", reftoken.clone(), time + REFRESH_LIFETIME
)).await;
let _ = con.query_parse::<()>(&query!(
"UPDATE bitauth.users SET tokens += ? WHERE login = ?",
token.clone(), login.clone()
)).await;
ret = (
jwt_sign(pool.clone(), json!({
"login": login.clone(),
"uuid": uuid.clone(),
"iat": time,
"exp": time + TOKEN_LIFETIME
})).await.unwrap(),
jwt_sign(pool.clone(), json!({
"uuid": token.clone(),
"iat": time,
"ref": reftoken.clone(),
"exp": time + REFRESH_LIFETIME
})).await.unwrap()
);
}
}
Ok(ret)
}
async fn create_user(pool: DBPool, data: HashMap<String, String>) -> Res<bool, SkyError> {
let mut ret = true;
let mut con = pool.get().await.unwrap();
let login = &data.get("login").unwrap().trim().to_lowercase();
let email = &data.get("email").unwrap().trim().to_lowercase();
let pass = data.get("password").unwrap().trim();
let pass2 = data.get("password2").unwrap().trim();
if pass != pass2 { ret = false };
if pass.len() < 8 { ret = false };
let q = con
.query_parse::<(String,)>(&query!("SELECT uuid FROM bitauth.users WHERE login = ?", login))
.await;
if q.is_ok() { ret = false };
if ret {
let uuid = format!("{}", uuid_v4().as_hyphenated());
let pass = bcrypt::hash(pass, 12).unwrap();
let q = con.query_parse::<()>(&query!(
r#"INSERT INTO bitauth.users {
login: ?,
uuid: ?,
password: ?,
email: ?,
tokens: []
}"#,
login,
uuid,
pass,
email,
)).await;
if q.is_err() { ret = false }
}
Ok(ret)
}
async fn get_body_from_request(mut req: Request<Incoming>) -> Result<Vec<u8>> {
let mut body: Vec<u8> = vec![];
while let Some(next) = req.frame().await {
let frame = next?;
if let Some(chunk) = frame.data_ref() {
body.extend_from_slice(chunk);
}
}
Ok(body)
}
fn double_split(body: String, first: &str, second: &str) -> HashMap<String, String> {
body.split(first)
.filter_map(|c| {
c.split_once(second)
.map(|(l, r)| (
l.trim().to_owned(),
format!("{}", url_decode(r).expect("UTF-8")).trim().to_owned()
))
})
.collect::<HashMap<String, String>>()
}
fn rsa_gen() -> (Vec<u8>, Vec<u8>) {
let mut rng = rand::thread_rng();
let bits = 2048;
let skey = RsaPrivateKey::new(&mut rng, bits).expect("RSA err");
let pkey = RsaPublicKey::from(&skey);
(
skey.to_pkcs1_der().unwrap().as_bytes().to_vec(),
pkey.to_pkcs1_der().unwrap().as_bytes().to_vec()
)
}
async fn jwt_sign(pool: DBPool, data: Json) -> Result<String> {
let mut con = pool.get().await.unwrap();
let q = con
.query_parse::<Sites>(&query!("SELECT * FROM bitauth.sites WHERE uuid = ?", "0"))
.await;
if q.is_err() { return Ok("".to_owned()) };
let skey = q.unwrap().skey;
let skey = EncodingKey::from_rsa_der(&skey);
let header = Header::new(Algorithm::RS512);
let token = jwt_encode(&header, &data, &skey)?;
Ok(token)
}
async fn jwt_verify(pool: DBPool, token: &str) -> Result<TokenData<Json>> {
let mut con = pool.get().await.unwrap();
let mut ret = TokenData { header: Header::new(Algorithm::RS512), claims: json![{}] };
let q = con
.query_parse::<Sites>(&query!("SELECT * FROM bitauth.sites WHERE uuid = ?", "0"))
.await;
if q.is_err() { return Ok(ret) };
let pkey = q.unwrap().pkey;
let pkey = DecodingKey::from_rsa_der(&pkey);
let token = jwt_decode::<Json>(&token, &pkey, &Validation::new(Algorithm::RS512));
match token.is_ok() {
true => ret = token.clone().unwrap(),
_ => {}
}
Ok(ret)
}
fn uuid_v4() -> Uuid {
Uuid::new_v4()
}
fn time() -> u32 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as u32
}
fn time_ns() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros()
Ok(("".to_owned(), StatusCode::FOUND, restype))
}

View File

@ -10,18 +10,18 @@ use skytable::{
pub struct Users {
pub login: String,
pub uuid: String,
pub login: String,
pub password: String,
pub email: String,
pub tokens: Vec<Value>,
}
impl Users {
pub fn new(login: String, uuid: String, password: String, email: String, tokens: Vec<Value>) -> Self {
pub fn new(uuid: String, login: String, password: String, email: String, tokens: Vec<Value>) -> Self {
Self {
login,
uuid,
login,
password,
email,
tokens,
@ -31,8 +31,8 @@ impl Users {
impl SQParam for Users {
fn append_param(&self, q: &mut Vec<u8>) -> usize {
self.login.append_param(q)
+ self.uuid.append_param(q)
self.uuid.append_param(q)
+ self.login.append_param(q)
+ self.password.append_param(q)
+ self.email.append_param(q)
}
@ -40,7 +40,7 @@ impl SQParam for Users {
impl FromResponse for Users {
fn from_response(resp: Response) -> ClientResult<Self> {
let (login, uuid, password, email, tokens) = FromResponse::from_response(resp)?;
Ok(Self::new(login, uuid, password, email, tokens))
let (uuid, login, password, email, tokens) = FromResponse::from_response(resp)?;
Ok(Self::new(uuid, login, password, email, tokens))
}
}

View File

@ -1,36 +1,30 @@
mod v0;
mod v1;
use {
hyper::{
StatusCode,
Request,
header::HeaderValue,
body::{
Incoming,
},
body::Incoming,
},
serde_json::{
Value as Json,
json,
},
skytable::pool::ConnectionMgrTcp,
bb8::Pool,
std::sync::Arc,
crate::DBPool,
};
type Res<T, E> = std::result::Result<T, E>;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
type DBPool = Arc<Pool<ConnectionMgrTcp>>;
pub async fn endpoint(req: Request<Incoming>, pool: DBPool) -> (String, StatusCode, HeaderValue) {
let uri: &str = req.uri().path().as_ref();
let res: Json = match &uri[4..uri.len()] {
"/test" => json!({"error": false, "msg": "test"}),
x if x.starts_with("/v0/") => v0::api(req, pool.clone()).await,
x if x.starts_with("/v1/") => v1::api(req, pool.clone()).await,
_ => json!({"error": true, "msg": "No endpoint"})
};
let restype: HeaderValue = "application/json".parse().unwrap();
(res.to_string(), StatusCode::IM_A_TEAPOT, restype)
(res.to_string(), StatusCode::OK, restype)
}

75
src/url/api/v0.rs Normal file
View File

@ -0,0 +1,75 @@
use {
hyper::{
Request,
body::Incoming,
},
serde_json::{
Value as Json,
json,
},
skytable::query,
crate::{
DBPool,
time,
double_split,
},
};
pub async fn api(req: Request<Incoming>, pool: DBPool) -> Json {
let uri: &str = req.uri().path().as_ref();
match &uri[7..uri.len()] {
"/auth" => auth(req, pool.clone()).await,
"/auth_get" => auth_get(req, pool.clone()).await,
_ => json!({"error": true, "msg": "No endpoint"})
}
}
async fn auth(req: Request<Incoming>, _pool: DBPool) -> Json {
let query = req.uri().query().or(Some("")).unwrap();
let query = double_split(query.to_string(), "&", "=");
let sess = std::string::String::from(query
.get("session")
.or(Some(&"".to_string()))
.unwrap());
match sess.as_str() {
"" => json!({"error": true, "msg": "No session in request"}),
x if x.len() > 128 => json!({"error": true, "msg": "Session len is too long"}),
_ => json!({
"error": false,
"link": format!("https://auth.bitheaven.ru/authorize?v=0&session={}", sess)
})
}
}
async fn auth_get(req: Request<Incoming>, pool: DBPool) -> Json {
let mut con = pool.get().await.unwrap();
let query = req.uri().query().or(Some("")).unwrap();
let query = double_split(query.to_string(), "&", "=");
let session = match query.get("session").is_none() {
false => query.get("session").unwrap(),
_ => ""
};
let res = con.query_parse::<(String, String, u32)>(&query!(
"SELECT login, uuid, expire FROM bitauth.v0 WHERE session = ?",
session
)).await;
let _ = con.query_parse::<()>(&query!(
"DELETE FROM bitauth.v0 WHERE session = ?",
session
)).await;
let (login, uuid, exp) = match res.is_ok() {
false => ("".to_owned(), "".to_owned(), 0),
_ => res.unwrap()
};
if login.as_str() == "" || exp < time() {
json!({"error": true, "msg": "Not auth yet"})
}
else {
json!({"error": false, "login": login, "uuid": uuid})
}
}

75
src/url/api/v1.rs Normal file
View File

@ -0,0 +1,75 @@
use {
hyper::{
Request,
body::Incoming,
},
serde_json::{
Value as Json,
json,
},
skytable::query,
crate::{
DBPool,
time,
double_split,
},
};
pub async fn api(req: Request<Incoming>, pool: DBPool) -> Json {
let uri: &str = req.uri().path().as_ref();
match &uri[7..uri.len()] {
"/auth" => auth(req, pool.clone()).await,
"/auth_get" => auth_get(req, pool.clone()).await,
_ => json!({"error": true, "msg": "No endpoint"})
}
}
async fn auth(req: Request<Incoming>, _pool: DBPool) -> Json {
let query = req.uri().query().or(Some("")).unwrap();
let query = double_split(query.to_string(), "&", "=");
let sess = std::string::String::from(query
.get("session")
.or(Some(&"".to_string()))
.unwrap());
match sess.as_str() {
"" => json!({"error": true, "msg": "No session in request"}),
x if x.len() > 128 => json!({"error": true, "msg": "Session len is too long"}),
_ => json!({
"error": false,
"link": format!("https://auth.bitheaven.ru/authorize?v=0&session={}", sess)
})
}
}
async fn auth_get(req: Request<Incoming>, pool: DBPool) -> Json {
let mut con = pool.get().await.unwrap();
let query = req.uri().query().or(Some("")).unwrap();
let query = double_split(query.to_string(), "&", "=");
let session = match query.get("session").is_none() {
false => query.get("session").unwrap(),
_ => ""
};
let res = con.query_parse::<(String, String, u32)>(&query!(
"SELECT login, uuid, expire FROM bitauth.v0 WHERE session = ?",
session
)).await;
let _ = con.query_parse::<()>(&query!(
"DELETE FROM bitauth.v0 WHERE session = ?",
session
)).await;
let (login, uuid, exp) = match res.is_ok() {
false => ("".to_owned(), "".to_owned(), 0),
_ => res.unwrap()
};
if login.as_str() == "" || exp < time() {
json!({"error": true, "msg": "Not auth yet"})
}
else {
json!({"error": false, "login": login, "uuid": uuid})
}
}

38
src/url/authorize.rs Normal file
View File

@ -0,0 +1,38 @@
use {
hyper::{
Request,
body::Incoming,
Method,
StatusCode,
header::HeaderValue,
},
crate::{
AUTHORIZE_HTML,
DBPool,
Result,
build_html,
double_split,
authorize_user,
},
};
pub async fn authorize(req: Request<Incoming>, pool: DBPool, token: String) -> Result<(String, StatusCode, HeaderValue)> {
// TODO: Forward for versions.
if *req.method() == Method::POST {
let r = double_split(req.uri().query().or(Some("")).unwrap().to_owned(), "&", "=");
let session = r.get("session");
let session = match session.is_none() {
false => session.unwrap().to_owned(),
_ => "".to_owned()
};
if session != "" && session.len() <= 128 {
authorize_user(pool.clone(), token, session).await;
}
}
let restype: HeaderValue = "text/html".parse().unwrap();
Ok((build_html(AUTHORIZE_HTML), StatusCode::OK, restype))
}

16
src/url/index.rs Normal file
View File

@ -0,0 +1,16 @@
use {
hyper::{
StatusCode,
header::HeaderValue,
},
crate::{
INDEX_HTML,
build_html,
},
};
pub fn index() -> (String, StatusCode, HeaderValue) {
let restype: HeaderValue = "text/html".parse().unwrap();
(build_html(INDEX_HTML), StatusCode::OK, restype)
}

53
src/url/login.rs Normal file
View File

@ -0,0 +1,53 @@
use {
hyper::{
StatusCode,
Request,
Method,
HeaderMap,
header::HeaderValue,
body::{
Incoming,
},
},
urlencoding::{
decode as url_decode,
},
crate::{
Result,
DBPool,
html::*,
funcs::*,
},
};
pub async fn login(req: Request<Incoming>, pool: DBPool, headers: &mut HeaderMap) -> Result<(String, StatusCode, HeaderValue)> {
let mut body = build_html(LOGIN_HTML);
let mut status = StatusCode::OK;
let restype: HeaderValue = "text/html".parse().unwrap();
if *req.method() == Method::POST {
let r = double_split(req.uri().query().or(Some("")).unwrap().to_owned(), "&", "=");
let post = get_body_from_request(req).await?;
let post = String::from_utf8(post).unwrap();
let post = double_split(post, "&", "=");
let (access, refresh) = login_user(pool.clone(), post).await?;
set_cookie(headers, "token", &access);
set_cookie(headers, "refresh", &refresh);
let r = r.get("q");
match r.is_some() {
true => {
status = StatusCode::FOUND;
body = "".to_owned();
set_location(headers, format!("{}", url_decode(r.unwrap())?).as_str());
},
_ => {}
}
}
Ok((body, status, restype))
}

8
src/url/mod.rs Normal file
View File

@ -0,0 +1,8 @@
pub mod api;
pub mod login;
pub mod index;
pub mod authorize;
pub mod nf;
pub mod register;
pub mod user;
pub mod recover;

16
src/url/nf.rs Normal file
View File

@ -0,0 +1,16 @@
use {
hyper::{
StatusCode,
header::HeaderValue,
},
crate::{
NF_HTML,
build_html,
},
};
pub fn nf() -> (String, StatusCode, HeaderValue) {
let restype: HeaderValue = "text/html".parse().unwrap();
(build_html(NF_HTML), StatusCode::NOT_FOUND, restype)
}

16
src/url/recover.rs Normal file
View File

@ -0,0 +1,16 @@
use {
hyper::{
StatusCode,
header::HeaderValue,
},
crate::{
build_html,
RECOVER_HTML,
},
};
pub fn recover() -> (String, StatusCode, HeaderValue) {
let restype: HeaderValue = "text/html".parse().unwrap();
(build_html(RECOVER_HTML), StatusCode::OK, restype)
}

51
src/url/register.rs Normal file
View File

@ -0,0 +1,51 @@
use {
hyper::{
body::Incoming,
HeaderMap,
Method,
Request,
header::HeaderValue,
StatusCode,
},
crate::{
Result,
DBPool,
REG_HTML,
build_html,
set_location,
create_user,
double_split,
get_body_from_request,
},
};
pub async fn register(req: Request<Incoming>, pool: DBPool, headers: &mut HeaderMap) -> Result<(String, StatusCode, HeaderValue)> {
let mut body = "".to_owned();
let mut status = StatusCode::OK;
let restype: HeaderValue = "text/html".parse().unwrap();
if *req.method() == Method::POST {
let request = get_body_from_request(req).await?;
let request = String::from_utf8(request).unwrap();
let request = double_split(request, "&", "=");
match create_user(pool.clone(), request).await? {
true => {
println!("Created");
set_location(headers, "/login");
},
_ => {
println!("Failed");
set_location(headers, "/register");
}
}
status = StatusCode::FOUND;
}
else {
body = build_html(REG_HTML);
}
Ok((body, status, restype))
}

34
src/url/user.rs Normal file
View File

@ -0,0 +1,34 @@
use {
hyper::{
Request,
StatusCode,
body::Incoming,
header::HeaderValue,
},
crate::{
Res,
DBPool,
SkyError,
get_user,
build_html,
},
};
pub async fn user(req: Request<Incoming>, pool: DBPool) -> Res<(String, StatusCode, HeaderValue), SkyError> {
let uri: &str = req.uri().path().as_ref();
let login = &uri[2..uri.len()];
let body: String;
let user = get_user(pool, login.to_string()).await;
if user.is_ok() {
let (uuid,) = user.unwrap();
body = format!("{}", uuid);
}
else {
body = "Not fond :(".to_owned();
}
let restype: HeaderValue = "text/html".parse().unwrap();
Ok((build_html(&body), StatusCode::OK, restype))
}