diff --git a/doc/api/auth.zh_CN.md b/doc/api/auth.zh_CN.md index 176e5a7..a3e62d4 100644 --- a/doc/api/auth.zh_CN.md +++ b/doc/api/auth.zh_CN.md @@ -44,3 +44,8 @@ * 方法: `GET` 或 `POST` * RESTful: `PUT /api/auth/token` 或 `PUT /auth/token` * 鉴权:不需要 +## 移除Token +* 路径: `/api/auth/token/delete`、 `/auth/token/delete` +* 方法: `GET` 或 `POST` +* RESTful: `DELETE /api/auth/token` 或 `DELETE /auth/token` +* 鉴权:需要(删除其他用户的Token仅管理员) diff --git a/src/db/sqlite/db.rs b/src/db/sqlite/db.rs index 3a36df5..ee2af44 100644 --- a/src/db/sqlite/db.rs +++ b/src/db/sqlite/db.rs @@ -220,6 +220,12 @@ impl PixivDownloaderSqlite { Ok(()) } + #[cfg(feature = "server")] + fn _delete_token(tx: &Transaction, id: u64) -> Result<(), SqliteError> { + tx.execute("DELETE FROM token WHERE id = ?;", [id])?; + Ok(()) + } + #[cfg(feature = "server")] fn _delete_user(tx: &Transaction, id: u64) -> Result { let af = tx.execute("DELETE FROM users WHERE id = ?;", [id])?; @@ -594,6 +600,15 @@ impl PixivDownloaderDb for PixivDownloaderSqlite { .expect("User not found:")) } + #[cfg(feature = "server")] + async fn delete_token(&self, id: u64) -> Result<(), PixivDownloaderDbError> { + let mut db = self.db.lock().await; + let mut tx = db.transaction()?; + Self::_delete_token(&mut tx, id)?; + tx.commit()?; + Ok(()) + } + #[cfg(feature = "server")] async fn delete_user(&self, id: u64) -> Result { let mut db = self.db.lock().await; diff --git a/src/db/traits.rs b/src/db/traits.rs index a0ad655..3fe41a6 100644 --- a/src/db/traits.rs +++ b/src/db/traits.rs @@ -55,6 +55,10 @@ pub trait PixivDownloaderDb { is_admin: bool, ) -> Result; #[cfg(feature = "server")] + /// Delete a token + /// * `id` - The token ID + async fn delete_token(&self, id: u64) -> Result<(), PixivDownloaderDbError>; + #[cfg(feature = "server")] /// Delete a user /// * `id` - User ID /// # Note diff --git a/src/server/auth/token.rs b/src/server/auth/token.rs index ee61543..b9232c6 100644 --- a/src/server/auth/token.rs +++ b/src/server/auth/token.rs @@ -1,5 +1,7 @@ use super::super::preclude::*; use super::{PASSWORD_ITER, PASSWORD_SALT}; +use crate::db::User; +use crate::ext::json::ToJson2; use crate::ext::try_err::TryErr3; use crate::gettext; use openssl::{hash::MessageDigest, pkcs5::pbkdf2_hmac}; @@ -8,6 +10,8 @@ use openssl::{hash::MessageDigest, pkcs5::pbkdf2_hmac}; pub enum AuthTokenAction { /// Add a new token Add, + /// Delete a token + Delete, } pub struct AuthTokenContext { @@ -30,12 +34,12 @@ impl AuthTokenContext { .get_params() .await .try_err3(-1002, gettext("Failed to get parameters:"))?; - let username = params - .get("username") - .ok_or((1, gettext("No username specified.")))?; match &self.action { Some(s) => match s { AuthTokenAction::Add => { + let username = params + .get("username") + .ok_or((1, gettext("No username specified.")))?; let password = params .get("password") .ok_or((2, gettext("No password specified.")))?; @@ -94,12 +98,54 @@ impl AuthTokenContext { json::object! { "id": token.id, "user_id": token.user_id, "token": b64token, "created_at": token.created_at.timestamp(), "expired_at": token.expired_at.timestamp() }, ) } + AuthTokenAction::Delete => { + let user = self + .ctx + .verify_token(&req, ¶ms) + .await + .try_err3(-403, gettext("Failed to verify the token:"))?; + let ids = params + .get_u64_all("id") + .try_err3( + 10, + &gettext("Failed to parse :").replace("", "id"), + )? + .try_err3(11, gettext("No id specified."))?; + let mut data = json::JsonValue::new_object(); + for id in ids { + data.insert( + &format!("{}", id), + self.revoke_token(id, &user).await.to_json2(), + ) + .try_err3(-1004, gettext("Failed to insert data to JSON:"))?; + } + Ok(data) + } }, None => { panic!("No action specified for AuthTokenContext."); } } } + + async fn revoke_token(&self, id: u64, user: &User) -> JSONResult { + let token = self + .ctx + .db + .get_token(id) + .await + .try_err3(-1001, gettext("Failed to operate the database:"))? + .try_err3(12, gettext("Token not found."))?; + if token.user_id != user.id && !user.is_admin { + return Err((13, gettext("Permission denied.")).into()); + } + self.ctx + .db + .delete_token(id) + .await + .try_err3(-1001, gettext("Failed to operate the database:"))?; + Ok(json::JsonValue::Boolean(true)) + } } #[async_trait] @@ -117,6 +163,7 @@ impl ResponseJsonFor for AuthTokenContext { allow_headers = [CONTENT_TYPE, X_SIGN, X_TOKEN_ID], OPTIONS, PUT, + DELETE, ); builder } else { @@ -144,7 +191,7 @@ pub struct AuthTokenRoute { impl AuthTokenRoute { pub fn new() -> Self { Self { - regex: Regex::new(r"^(/+api)?/+auth/+token(/+add)?$").unwrap(), + regex: Regex::new(r"^(/+api)?/+auth/+token(/+(add|delete))?$").unwrap(), } } } @@ -173,12 +220,16 @@ impl MatchRoute for AuthTokenRoute { let m = m.as_str().trim_start_matches("/"); match m { "add" => Some(AuthTokenAction::Add), + "delete" => Some(AuthTokenAction::Delete), _ => return None, } } None => { - if req.method() == Method::PUT { + let m = req.method(); + if m == Method::PUT { Some(AuthTokenAction::Add) + } else if m == Method::DELETE { + Some(AuthTokenAction::Delete) } else { None } diff --git a/src/server/params.rs b/src/server/params.rs index dfc4e6e..2be9c58 100644 --- a/src/server/params.rs +++ b/src/server/params.rs @@ -74,6 +74,32 @@ impl RequestParams { } } + /// Get all parameters with same name and return it as [u64] + /// * `name` - Parameter name. + pub fn get_u64_all + ?Sized>( + &self, + name: &S, + ) -> Result>, PixivDownloaderError> { + let mut result = Vec::new(); + match self.params.get(name.as_ref()) { + Some(v) => { + for s in v { + let s = s.trim(); + match s.parse::() { + Ok(v) => result.push(v), + Err(_) => { + return Err(gettext("Invalid unsigned 64bit integer value.").into()) + } + } + } + } + None => { + return Ok(None); + } + } + Ok(Some(result)) + } + /// Get parameter and return it as [u64]. /// * `name` - A list of the parameter name. /// # Note diff --git a/src/server/unittest/auth.rs b/src/server/unittest/auth.rs index b35a794..d004cb3 100644 --- a/src/server/unittest/auth.rs +++ b/src/server/unittest/auth.rs @@ -591,5 +591,82 @@ pub async fn test(ctx: &UnitTestContext) -> Result<[(u64, Vec); 2], PixivDow ], } ); + let re = ctx + .request_json2( + "/auth/token/add", + &json::object! { + "username": "test1", + "password": b64_password2.as_str(), + }, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.expect("Failed to add token:"); + assert_eq!(Some(1), result["user_id"].as_u64()); + let token3 = base64::decode(result["token"].as_str().unwrap()).unwrap(); + assert_eq!(token3.len(), 64); + let token3_id = result["id"].as_u64().unwrap(); + let re = ctx + .request_json2_sign( + "/auth/token/delete", + &json::object! { + "id": [token3_id, token_id], + }, + &token2, + token2_id, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.unwrap(); + let result2 = JSONResult::from_json(&result[format!("{}", token3_id)])?.unwrap(); + assert_eq!(result2.as_bool(), Some(true)); + let result2 = JSONResult::from_json(&result[format!("{}", token_id)])?.unwrap_err(); + assert_eq!(result2.code, 13); + let re = ctx + .request_json2( + "/auth/token/add", + &json::object! { + "username": "test1", + "password": b64_password2.as_str(), + }, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.expect("Failed to add token:"); + assert_eq!(Some(1), result["user_id"].as_u64()); + let token3 = base64::decode(result["token"].as_str().unwrap()).unwrap(); + assert_eq!(token3.len(), 64); + let token3_id = result["id"].as_u64().unwrap(); + let re = ctx + .request_json2( + "/auth/token/add", + &json::object! { + "username": "test", + "password": b64_password.as_str(), + }, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.expect("Failed to add token:"); + assert_eq!(Some(0), result["user_id"].as_u64()); + let token4 = base64::decode(result["token"].as_str().unwrap()).unwrap(); + assert_eq!(token4.len(), 64); + let token4_id = result["id"].as_u64().unwrap(); + let re = ctx + .request_json2_sign( + "/auth/token/delete", + &json::object! { + "id": [token3_id, token4_id], + }, + &token, + token_id, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.unwrap(); + let result2 = JSONResult::from_json(&result[format!("{}", token3_id)])?.unwrap(); + assert_eq!(result2.as_bool(), Some(true)); + let result2 = JSONResult::from_json(&result[format!("{}", token4_id)])?.unwrap(); + assert_eq!(result2.as_bool(), Some(true)); Ok([(token_id, token), (token2_id, token2)]) } diff --git a/src/server/unittest/mod.rs b/src/server/unittest/mod.rs index 28987b6..f7828f8 100644 --- a/src/server/unittest/mod.rs +++ b/src/server/unittest/mod.rs @@ -118,31 +118,37 @@ impl UnitTestContext { ) -> Result, PixivDownloaderError> { let mut par = BTreeMap::new(); for (key, obj) in params.entries() { + if !par.contains_key(key) { + par.insert(key.to_string(), Vec::new()); + } + let pp = par.get_mut(key).unwrap(); if let Some(s) = obj.as_str() { - par.insert(key.to_owned(), s.to_owned()); + pp.push(s.to_owned()); } else if obj.is_array() { for s in obj.members() { if let Some(s) = s.as_str() { - par.insert(key.to_owned(), s.to_owned()); + pp.push(s.to_owned()); } else { - par.insert(key.to_owned(), s.dump()); + pp.push(s.dump()); } } } else { - par.insert(key.to_owned(), obj.dump()); + pp.push(obj.dump()); } } let mut sha = openssl::sha::Sha512::new(); sha.update(token); let mut par2 = Vec::new(); for (key, value) in par.iter() { - sha.update(key.as_bytes()); - sha.update(value.as_bytes()); - par2.push(format!( - "{}={}", - urlparse::quote_plus(key, b"")?, - urlparse::quote_plus(value, b"")? - )); + for v in value { + sha.update(key.as_bytes()); + sha.update(v.as_bytes()); + par2.push(format!( + "{}={}", + urlparse::quote_plus(key, b"")?, + urlparse::quote_plus(v, b"")? + )); + } } let par2 = par2.join("&"); let sign = hex::encode(sha.finish());