From 6b68e6374d34f8e62fe3d8cfb98841171036ba3b Mon Sep 17 00:00:00 2001 From: sigseg5 Date: Sun, 3 Dec 2023 16:10:02 +0400 Subject: [PATCH 1/5] Bump version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7632df5..d76e613 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "outline_api" -version = "2.0.4" +version = "2.1.0" edition = "2021" authors = ["sigseg5"] license = "MIT" From 3bf79a94d075f6fb3053b006af042c81fa35daaa Mon Sep 17 00:00:00 2001 From: sigseg5 Date: Sun, 3 Dec 2023 16:24:17 +0400 Subject: [PATCH 2/5] Ref response status code validation --- src/lib.rs | 172 +++++++++++++++++++++++++++++------------------------ 1 file changed, 93 insertions(+), 79 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bf080a9..00f6f39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,15 @@ enum APIError { UnknownError, } +// Endpoints +const NAME_ENDPOINT: &str = "/name"; +const SERVER_ENDPOINT: &str = "/server"; +const HOSTNAME_ENDPOINT: &str = "/server/hostname-for-access-keys"; +const CHANGE_PORT_ENDPOINT: &str = "/server/port-for-new-access-keys"; +const KEY_DATA_LIMIT_ENDPOINT: &str = "/server/access-key-data-limit"; +const METRICS_ENDPOINT: &str = "/metrics"; +const ACCESS_KEYS_ENDPOINT: &str = "/access-keys"; + impl std::fmt::Display for APIError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { @@ -108,6 +117,57 @@ fn handle_json_api_error(response: Response) -> Result`. On successful response status, it returns `Ok(())`. +/// On error, it returns `Err(String)` with an appropriate error message based on the API endpoint +/// and the response status code. +/// +/// # Error Handling +/// +/// This function handles the following `reqwest::StatusCode` variants: +/// - `OK`: Indicates a successful request. +/// - `NO_CONTENT`: Indicates a successful request with no content to return. +/// - `BAD_REQUEST`: Maps to specific API errors based on the `api_path`. +/// - `CONFLICT`: Indicates a port conflict error. +/// - `NOT_FOUND`: Indicates an invalid access key error. +/// - `INTERNAL_SERVER_ERROR`: Indicates an internal server error. +/// - Any other status codes are mapped to an unknown error. +/// +/// Specific errors are derived from the `APIError` enum, translating enum variants to strings. +fn handle_response_status(response: &Response, api_path: &str) -> Result<(), String> { + match response.status() { + reqwest::StatusCode::OK => Ok(()), + reqwest::StatusCode::NO_CONTENT => Ok(()), + reqwest::StatusCode::BAD_REQUEST => match api_path { + NAME_ENDPOINT => Err(APIError::InvalidName.to_string()), + HOSTNAME_ENDPOINT => Err(APIError::InvalidHostname.to_string()), + CHANGE_PORT_ENDPOINT => Err(APIError::InvalidPort.to_string()), + KEY_DATA_LIMIT_ENDPOINT | ACCESS_KEYS_ENDPOINT => { + Err(APIError::InvalidDataLimit.to_string()) + } + _ => Err(APIError::InvalidRequest.to_string()), + }, + reqwest::StatusCode::CONFLICT => Err(APIError::PortConflict.to_string()), + reqwest::StatusCode::NOT_FOUND => Err(APIError::AccessKeyInexistent.to_string()), + reqwest::StatusCode::INTERNAL_SERVER_ERROR => Err(APIError::InternalError.to_string()), + _ => Err(APIError::UnknownError.to_string()), + } +} + /// Represents a client for interacting with the Outline VPN Server API. /// /// The `OutlineVPN` struct provides methods to perform various operations on the Outline VPN server @@ -124,12 +184,6 @@ pub struct OutlineVPN<'a> { request_timeout_in_sec: Duration, } -// Endpoints -const SERVER_ENDPOINT: &str = "/server"; -const HOSTNAME_ENDPOINT: &str = "/server/hostname-for-access-keys"; -const CHANGE_PORT_ENDPOINT: &str = "/server/port-for-new-access-keys"; -const KEY_DATA_LIMIT_ENDPOINT: &str = "/server/access-key-data-limit"; - impl OutlineVPN<'_> { fn call_api( &self, @@ -180,12 +234,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::BAD_REQUEST => Err(APIError::InvalidHostname.to_string()), - reqwest::StatusCode::INTERNAL_SERVER_ERROR => Err(APIError::InternalError.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, HOSTNAME_ENDPOINT) } /// Change default port for newly created access keys. @@ -202,12 +251,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::BAD_REQUEST => Err(APIError::InvalidPort.to_string()), - reqwest::StatusCode::CONFLICT => Err(APIError::PortConflict.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, CHANGE_PORT_ENDPOINT) } /// Set data transfer limit (in bytes) for all access keys. @@ -223,11 +267,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::BAD_REQUEST => Err(APIError::InvalidDataLimit.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, KEY_DATA_LIMIT_ENDPOINT) } /// Remove data transfer limit for all access keys. @@ -245,10 +285,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, KEY_DATA_LIMIT_ENDPOINT) } /// Rename server. @@ -259,16 +296,12 @@ impl OutlineVPN<'_> { /// - `400` – Invalid name. pub fn rename_server(&self, name: &str) -> Result<(), String> { let body = format!(r#"{{ "name": "{}" }}"#, name); - let response = match self.call_api("/name", reqwest::Method::PUT, body) { + let response = match self.call_api(NAME_ENDPOINT, reqwest::Method::PUT, body) { Ok(response) => response, Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::BAD_REQUEST => Err(APIError::InvalidName.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, NAME_ENDPOINT) } /// Create new access key. @@ -277,10 +310,11 @@ impl OutlineVPN<'_> { /// /// - `201` – The newly created access key. pub fn create_access_key(&self) -> Result { - let response = match self.call_api("/access-keys", reqwest::Method::POST, String::new()) { - Ok(response) => response, - Err(_) => return Err(APIError::UnknownServerError.to_string()), - }; + let response = + match self.call_api(ACCESS_KEYS_ENDPOINT, reqwest::Method::POST, String::new()) { + Ok(response) => response, + Err(_) => return Err(APIError::UnknownServerError.to_string()), + }; handle_json_api_error(response) } @@ -291,10 +325,11 @@ impl OutlineVPN<'_> { /// /// - `200` – List of access keys. pub fn list_access_keys(&self) -> Result { - let response = match self.call_api("/access-keys", reqwest::Method::GET, String::new()) { - Ok(response) => response, - Err(_) => return Err(APIError::UnknownServerError.to_string()), - }; + let response = + match self.call_api(ACCESS_KEYS_ENDPOINT, reqwest::Method::GET, String::new()) { + Ok(response) => response, + Err(_) => return Err(APIError::UnknownServerError.to_string()), + }; handle_json_api_error(response) } @@ -307,7 +342,7 @@ impl OutlineVPN<'_> { // /// - 200 – The access key. // /// - 404 – Access key inexistent. // pub fn get_access_key_by_id(&self, id: &u16) -> Result { - // let api_path = format!("/access-keys/{}", id); + // let api_path = format!("{}/{}", ACCESS_KEYS_ENDPOINT, id); // let response = match self.call_api(&api_path, reqwest::Method::GET, String::new()) { // Ok(response) => response, // Err(_) => return Err(APIError::UnknownServerError.to_string()), @@ -323,17 +358,13 @@ impl OutlineVPN<'_> { /// - `204` – Access key deleted successfully. /// - `404` – Access key inexistent. pub fn delete_access_key_by_id(&self, id: &u16) -> Result<(), String> { - let api_path = format!("/access-keys/{}", id); + let api_path = format!("{}/{}", ACCESS_KEYS_ENDPOINT, id); let response = match self.call_api(&api_path, reqwest::Method::DELETE, String::new()) { Ok(response) => response, Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::NOT_FOUND => Err(APIError::AccessKeyInexistent.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, ACCESS_KEYS_ENDPOINT) } /// Change name for access key (by ID). @@ -344,17 +375,13 @@ impl OutlineVPN<'_> { /// - `404` – Access key inexistent. pub fn change_name_for_access_key(&self, id: &u16, username: &str) -> Result<(), String> { let body = format!(r#"{{ "name": "{}" }}"#, username); - let api_path = format!("/access-keys/{}/name", id); + let api_path = format!("{}/{}/name", ACCESS_KEYS_ENDPOINT, id); let response = match self.call_api(&api_path, reqwest::Method::PUT, body) { Ok(response) => response, Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::NOT_FOUND => Err(APIError::AccessKeyInexistent.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, ACCESS_KEYS_ENDPOINT) } /// Set data transfer limit by ID. @@ -366,18 +393,13 @@ impl OutlineVPN<'_> { /// - `404` – Access key inexistent. pub fn set_data_transfer_limit_by_id(&self, id: &u16, byte: &u64) -> Result<(), String> { let body = format!(r#"{{ "limit": {{ "bytes": {} }} }}"#, byte); - let api_path = format!("/access-keys/{}/data-limit", id); + let api_path = format!("{}/{}/data-limit", ACCESS_KEYS_ENDPOINT, id); let response = match self.call_api(&api_path, reqwest::Method::PUT, body) { Ok(response) => response, Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::BAD_REQUEST => Err(APIError::InvalidDataLimit.to_string()), - reqwest::StatusCode::NOT_FOUND => Err(APIError::AccessKeyInexistent.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, ACCESS_KEYS_ENDPOINT) } /// Remove data transfer limit by ID. @@ -387,17 +409,13 @@ impl OutlineVPN<'_> { /// - `204` – Access key limit deleted successfully. /// - `404` – Access key inexistent. pub fn del_data_transfer_limit_by_id(&self, id: &u16) -> Result<(), String> { - let api_path = format!("/access-keys/{}/data-limit", id); + let api_path = format!("{}/{}/data-limit", ACCESS_KEYS_ENDPOINT, id); let response = match self.call_api(&api_path, reqwest::Method::DELETE, String::new()) { Ok(response) => response, Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::NOT_FOUND => Err(APIError::AccessKeyInexistent.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, ACCESS_KEYS_ENDPOINT) } /// Get data transfer stats for each access key in bytes. @@ -406,8 +424,8 @@ impl OutlineVPN<'_> { /// /// - `200` – The data transferred by each access key. pub fn get_each_access_key_data_transferred(&self) -> Result { - let response = match self.call_api("/metrics/transfer", reqwest::Method::GET, String::new()) - { + let api_path = format!("{}/transfer", METRICS_ENDPOINT); + let response = match self.call_api(&api_path, reqwest::Method::GET, String::new()) { Ok(response) => response, Err(_) => return Err(APIError::UnknownServerError.to_string()), }; @@ -431,8 +449,8 @@ impl OutlineVPN<'_> { /// /// - `200` – The metrics enabled setting. pub fn get_whether_metrics_is_being_shared(&self) -> Result { - let response = match self.call_api("/metrics/enabled", reqwest::Method::GET, String::new()) - { + let api_path = format!("{}/enabled", METRICS_ENDPOINT); + let response = match self.call_api(&api_path, reqwest::Method::GET, String::new()) { Ok(response) => response, Err(_) => return Err(APIError::UnknownServerError.to_string()), }; @@ -458,17 +476,13 @@ impl OutlineVPN<'_> { /// - `400` – Invalid request. pub fn enable_or_disable_sharing_metrics(&self, metrics_enabled: bool) -> Result<(), String> { let body = format!(r#"{{ "metricsEnabled": {} }}"#, metrics_enabled); - - let response = match self.call_api("/metrics/enabled", reqwest::Method::PUT, body) { + let api_path = format!("{}/enabled", METRICS_ENDPOINT); + let response = match self.call_api(&api_path, reqwest::Method::PUT, body) { Ok(response) => response, Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::BAD_REQUEST => Err(APIError::InvalidRequest.to_string()), - _ => Err(APIError::UnknownError.to_string()), - } + handle_response_status(&response, ACCESS_KEYS_ENDPOINT) } } @@ -523,7 +537,7 @@ pub fn new<'a>( reqwest::header::HeaderValue::from_str(cert_sha256).unwrap(), ); - // .danger_accept_invalid_certs(true) use is safe because it uses a self-issued encryption certificate when the server is created + // .danger_accept_invalid_certs(true) is safe to use because it uses a self-issued encryption certificate when the server is created let session = Client::builder() .danger_accept_invalid_certs(true) .default_headers(headers) From 12d72456d7684290f0ddfde1ce6138912cbb8852 Mon Sep 17 00:00:00 2001 From: sigseg5 Date: Sun, 3 Dec 2023 16:37:23 +0400 Subject: [PATCH 3/5] Use handle_json_api_error instead of pattern matching in other cases --- src/lib.rs | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 00f6f39..f624270 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -430,17 +430,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::OK => { - let response_body = response - .text() - .map_err(|_| "Error reading response body".to_string())?; - let json_value: serde_json::Value = serde_json::from_str(&response_body) - .map_err(|_| "Error deserializing JSON".to_string())?; - Ok(json_value) - } - _ => Err(APIError::UnknownError.to_string()), - } + handle_json_api_error(response) } /// Get 'Share anonymous metrics' status. @@ -455,17 +445,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - match response.status() { - reqwest::StatusCode::OK => { - let response_body = response - .text() - .map_err(|_| "Error reading response body".to_string())?; - let json_value: serde_json::Value = serde_json::from_str(&response_body) - .map_err(|_| "Error deserializing JSON".to_string())?; - Ok(json_value) - } - _ => Err(APIError::UnknownError.to_string()), - } + handle_json_api_error(response) } /// Enable or disable 'Share anonymous metrics' setting. From 3e2540e17cf750cc12e9ba7e5ea95df712942412 Mon Sep 17 00:00:00 2001 From: sigseg5 Date: Sun, 3 Dec 2023 16:47:56 +0400 Subject: [PATCH 4/5] Rename handle_json_api_error -> handle_json_api_result --- src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f624270..03df99c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,7 @@ impl std::fmt::Display for APIError { /// an internal server error. /// - `APIError::UnknownError`: If the response status code is not `200 OK` or `500 Internal Server Error`, /// indicating an unknown error occurred. -fn handle_json_api_error(response: Response) -> Result { +fn handle_json_api_result(response: Response) -> Result { match response.status() { reqwest::StatusCode::OK => { let response_body = response @@ -217,7 +217,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - handle_json_api_error(response) + handle_json_api_result(response) } /// Change hostname for access keys. @@ -316,7 +316,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - handle_json_api_error(response) + handle_json_api_result(response) } /// Display complete list of the access keys. @@ -331,7 +331,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - handle_json_api_error(response) + handle_json_api_result(response) } // /// Incorrect API specification, this method is defined in the API, but is not actually supported by the server!!! @@ -430,7 +430,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - handle_json_api_error(response) + handle_json_api_result(response) } /// Get 'Share anonymous metrics' status. @@ -445,7 +445,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - handle_json_api_error(response) + handle_json_api_result(response) } /// Enable or disable 'Share anonymous metrics' setting. From 210d467eabdbf3590dd8eb35fb76c12e63a10baf Mon Sep 17 00:00:00 2001 From: sigseg5 Date: Sun, 3 Dec 2023 16:48:33 +0400 Subject: [PATCH 5/5] Fix wrong endpoint for handle_response_status in enable_or_disable_sharing_metrics --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 03df99c..7c561c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -462,7 +462,7 @@ impl OutlineVPN<'_> { Err(_) => return Err(APIError::UnknownServerError.to_string()), }; - handle_response_status(&response, ACCESS_KEYS_ENDPOINT) + handle_response_status(&response, METRICS_ENDPOINT) } }