Skip to content

Commit

Permalink
Merge pull request #59 from Chloe-Woahie/dev-main
Browse files Browse the repository at this point in the history
v0.31.0
  • Loading branch information
fekie authored May 27, 2023
2 parents 29c03f5 + 5dc0749 commit 1f662da
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 45 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = "MIT"
name = "roboat"
readme = "README.md"
repository = "https://github.com/Chloe-Woahie/roboat"
version = "0.30.0"
version = "0.31.0"

[dependencies]
reqwest = { version = "0.11.14", default-features=false, features = ["rustls-tls", "json"] }
Expand All @@ -17,6 +17,7 @@ serde = {version="1.0.136", features=["derive"]}
serde_json = "1.0.94"
tokio = { version = "1.27.0", features = ["full"] }
uuid = {version="1.3.1", features=["fast-rng", "v4"]}
base64 = "0.13.1"

[dev-dependencies]
clap = { version = "4.1.13", features = ["derive"] }
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Alternatively, you can add a specific version of roboat to your project by addin

```toml
[dependencies]
roboat = "0.30.0"
roboat = "0.31.0"
```

# Quick Start Examples
Expand Down
1 change: 1 addition & 0 deletions src/economy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub enum PurchaseTradableLimitedError {
UnknownRobloxErrorMsg(String),
}

// todo: change this to User maybe
/// A reseller of a resale listing.
#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
Expand Down
11 changes: 10 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,19 @@ pub enum RoboatError {
/// allow async recursion without making a type signature extremely messy.
#[error("Invalid Xcsrf. New Xcsrf Contained In Error.")]
InvalidXcsrf(String),
/// Used when an endpoint returns a 403 status code, but the response does not contain
/// Used when an endpoint returns a 403 status code, doesn't need a challenge, but the response does not contain
/// a new xcsrf.
#[error("Missing Xcsrf")]
XcsrfNotReturned,
/// Used when an endpoint returns a 403 status code, but not because of an invalid xcsrf.
/// The string inside this error variant is a challenge id, which can be used to complete the challenge
/// (which can be either a captcha or a two step verification code).
#[error("Challenge Required. A captcha or two step authentication must be completed using challenge id {0}.")]
ChallengeRequired(String),
/// Used when an endpoint returns a 403 status code, can be parsed into a roblox error,
/// but the error message is incorrect or the challenge id is not returned. This also means that no xcsrf was returned.
#[error("Unknown Status Code 403 Format. If this occurs often it may be a bug. Please report it to the issues page.")]
UnknownStatus403Format,
/// Custom Roblox errors sometimes thrown when the user calls [`Client::purchase_tradable_limited`].
#[error("{0}")]
PurchaseTradableLimitedError(PurchaseTradableLimitedError),
Expand Down
114 changes: 72 additions & 42 deletions src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,96 @@ use serde::{Deserialize, Serialize};
#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
struct RobloxErrorResponse {
errors: Vec<RobloxErrorRaw>,
pub errors: Vec<RobloxErrorRaw>,
}

#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
struct RobloxErrorRaw {
code: u16,
message: String,
pub code: u16,
pub message: String,
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ChallengeMetadata {
pub user_id: String,
pub challenge_id: String,
pub should_show_remember_device_checkbox: bool,
pub remember_device: bool,
pub session_cookie: String,
pub verification_token: String,
pub action_type: String,
pub request_path: String,
pub request_method: String,
}

impl Client {
/// Used to process a 403 response from an endpoint. This requires new xcsrf to be
/// pulled and returned inside an error
/// Used to process a 403 response from an endpoint. This status is returned when a challenge is needed
/// or when the xcsrf is invalid.
async fn process_403(request_response: Response) -> RoboatError {
let headers = request_response.headers().clone();
let xcsrf = headers
.get(XCSRF_HEADER)
.map(|x| x.to_str().unwrap().to_string());

match xcsrf {
// If the xcsrf exists, we can send back invalid xcsrfs.
Some(xcsrf) => {
// If the response cannot be parsed, and the xcsrf exists, we return an invalid xcsrf error.
let error_response = match request_response.json::<RobloxErrorResponse>().await {
Ok(x) => x,
Err(_) => {
return RoboatError::InvalidXcsrf(xcsrf);

// We branch here depending on whether it can parse into a `RobloxErrorResponse` or not.
// If it can, it means a challenge is required and we return a `RoboatError::ChallengeRequired(_)`.
// Otherwise, we return an xcsrf related error.

match request_response.json::<RobloxErrorResponse>().await {
Ok(x) => {
// We make sure the first error exists and is a challenge required error.
match x.errors.first() {
Some(error) => {
if error.message != "Challenge is required to authorize the request" {
return RoboatError::UnknownRobloxErrorCode {
code: error.code,
message: error.message.clone(),
};
}
}
None => {
return RoboatError::UnknownStatus403Format;
}
}

// For some really really *stupid* reason, the header `rblx-challenge-id` is not the real challenge id.
// The challenge id is actually inside the header `rblx-challenge-metadata`, which is encoding in base64.

// We get the challenge metadata from the headers, and error if we cant.
let metadata_encoded = match headers
.get("rblx-challenge-metadata")
.map(|x| x.to_str().unwrap().to_string())
{
Some(x) => x,
None => {
return RoboatError::UnknownStatus403Format;
}
};

match error_response.errors.first() {
Some(error) => match error.code {
0 => RoboatError::InvalidXcsrf(xcsrf),
_ => RoboatError::UnknownRobloxErrorCode {
code: error.code,
message: error.message.clone(),
},
},
None => RoboatError::InvalidXcsrf(xcsrf),
}
}
// Otherwise, we parse the error knowing it doesn't exist
None => {
// If the response cannot be parsed, and the xcsrf does not exist, we return an xcsrf not returned error.
let error_response = match request_response.json::<RobloxErrorResponse>().await {
// We can unwrap here because we're kinda screwed if it's spitting out other stuff and the library would need to be fixed.
let metadata =
String::from_utf8(base64::decode(metadata_encoded).unwrap()).unwrap();

// We parse the metadata into a struct, and error if we cant.
let metadata_struct: ChallengeMetadata = match serde_json::from_str(&metadata) {
Ok(x) => x,
Err(_) => {
return RoboatError::XcsrfNotReturned;
return RoboatError::UnknownStatus403Format;
}
};

match error_response.errors.first() {
Some(error) => match error.code {
0 => RoboatError::XcsrfNotReturned,
_ => RoboatError::UnknownRobloxErrorCode {
code: error.code,
message: error.message.clone(),
},
},
None => RoboatError::MalformedResponse,
// We return the challenge required error.
RoboatError::ChallengeRequired(metadata_struct.challenge_id)
}
Err(_) => {
// If we're down here, it means that the response is not a challenge required error and we
// can return xcsrf if it exists
let xcsrf = headers
.get(XCSRF_HEADER)
.map(|x| x.to_str().unwrap().to_string());

match xcsrf {
Some(x) => RoboatError::InvalidXcsrf(x),
None => RoboatError::XcsrfNotReturned,
}
}
}
Expand Down

0 comments on commit 1f662da

Please sign in to comment.