feat: a bunch of things
This commit is contained in:
parent
2cd0311a20
commit
2e2b01990e
20 changed files with 16216 additions and 137 deletions
|
|
@ -1,10 +1,158 @@
|
|||
use diesel::{Queryable, Selectable};
|
||||
use std::io::Write;
|
||||
use argon2::{Algorithm, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use argon2::password_hash::rand_core::OsRng;
|
||||
use argon2::password_hash::SaltString;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use bytemuck::bytes_of;
|
||||
use log::{error, warn};
|
||||
use rocket::http::Status;
|
||||
use rocket::{async_trait, Request};
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sha2::digest::FixedOutput;
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::Pool;
|
||||
|
||||
#[derive(Queryable, Selectable)]
|
||||
#[diesel(table_name = crate::db::user)]
|
||||
#[diesel(check_for_backend(diesel::mysql::Mysql))]
|
||||
macro_rules! request_try {
|
||||
($expression:expr) => {
|
||||
match $expression{
|
||||
Ok(v) => v,
|
||||
Err(e) => return Outcome::Error((Status::BadRequest, e))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const INVALID_TOKEN_ERRORS: Errors<'static> = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
message: "Invalid access token",
|
||||
code: "0005"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// optimization note: add token caching
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub hashed_pw: String
|
||||
pub password: String,
|
||||
pub birthdate: chrono::NaiveDate,
|
||||
pub timezone: String,
|
||||
pub email: String,
|
||||
pub gender: String,
|
||||
pub country: String,
|
||||
pub language: String,
|
||||
pub marketing_allowed: bool,
|
||||
pub region: i32,
|
||||
pub mii_identifier: String
|
||||
}
|
||||
|
||||
fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{
|
||||
let mut sha = Sha256::new();
|
||||
|
||||
sha.write_all(&bytes_of(&pid)).unwrap();
|
||||
sha.write_all(&[0x02, 0x65, 0x43 ,0x46]).unwrap();
|
||||
sha.write_all(text_password.as_bytes()).unwrap();
|
||||
|
||||
hex::encode(&sha.finalize()[..])
|
||||
}
|
||||
|
||||
impl User{
|
||||
fn generate_nintendo_hash(&self, text_password: &str) -> String{
|
||||
generate_nintendo_hash(self.id, text_password)
|
||||
}
|
||||
|
||||
pub fn verify_password(&self, cleartext_password: &str) -> (bool, bool){
|
||||
let Ok(hash) = PasswordHash::new(&self.password) else {
|
||||
error!("invalid password in database for user with pid: {}", self.id);
|
||||
|
||||
if self.password == self.generate_nintendo_hash(cleartext_password){
|
||||
return (true, true)
|
||||
}
|
||||
|
||||
return (false, false)
|
||||
};
|
||||
|
||||
let argon = Argon2::default();
|
||||
|
||||
(argon.verify_password(cleartext_password.as_bytes(), &hash).is_ok(), false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_password(pid: i32, cleartext_password: &str) -> Option<String>{
|
||||
let password = generate_nintendo_hash(pid, cleartext_password);
|
||||
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon = Argon2::default();
|
||||
|
||||
let pw = argon.hash_password(password.as_bytes(), &salt).ok()?;
|
||||
|
||||
Some(pw.to_string())
|
||||
}
|
||||
|
||||
|
||||
pub async fn read_basic_auth_token(connection: &mut AsyncMysqlConnection, token: &str) -> Option<User> {
|
||||
let data = BASE64_STANDARD.decode(&token).ok()?;
|
||||
|
||||
let decoded_basic_token = String::from_utf8(data).ok()?;
|
||||
|
||||
let (login_username, login_password) = decoded_basic_token.split_once(' ')?;
|
||||
|
||||
|
||||
|
||||
let mut user: User = users
|
||||
.filter(name.eq(login_username))
|
||||
.select(User::as_select())
|
||||
.first(connection)
|
||||
.await.ok()?;
|
||||
|
||||
let (password_valid, upgrade_password) = user.verify_password(&login_password);
|
||||
|
||||
if password_valid{
|
||||
if upgrade_password{
|
||||
user.password = generate_password(&login_password).unwrap();
|
||||
|
||||
user = connection.update_and_fetch(&user).await.ok()?;
|
||||
}
|
||||
|
||||
Some(user)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_bearer_auth_token(connection: &mut AsyncMysqlConnection, token: &str) -> Option<User> {
|
||||
let data = BASE64_STANDARD.decode(&token).ok()?;
|
||||
|
||||
warn!("bearer token login currently unsupported");
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for User{
|
||||
type Error = Errors<'static>;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let pool: &Pool = request.rocket().state().unwrap();
|
||||
|
||||
let mut connection = pool.get().await.unwrap();
|
||||
|
||||
let auth = request_try!(request.headers().get("Authorization").next().ok_or(INVALID_TOKEN_ERRORS));
|
||||
|
||||
let (auth_type, token) = request_try!(auth.split_once(' ').ok_or(INVALID_TOKEN_ERRORS));
|
||||
|
||||
let user = match auth_type{
|
||||
"Basic" => read_basic_auth_token(&mut connection, token).await,
|
||||
"Bearer" => read_bearer_auth_token(&mut connection, token).await,
|
||||
_ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
|
||||
};
|
||||
|
||||
let Some(user) = user else {
|
||||
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
|
||||
};
|
||||
|
||||
Outcome::Success(user)
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
mod account;
|
||||
pub mod account;
|
||||
16
src/dsresponse.rs
Normal file
16
src/dsresponse.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use std::marker::PhantomData;
|
||||
use rocket::{Request, Response};
|
||||
use rocket::http::{Header, Status};
|
||||
use rocket::response::Responder;
|
||||
use crate::error::Errors;
|
||||
use crate::xml::Xml;
|
||||
|
||||
pub struct Ds<T>(pub T);
|
||||
|
||||
impl<'r, 'o: 'r, T: Responder<'r, 'o>> Responder<'r, 'o> for Ds<T> {
|
||||
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'o> {
|
||||
Response::build_from(self.0.respond_to(request)?)
|
||||
.header(Header::new("Server", "Nintendo 3DS (http)"))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
28
src/error.rs
Normal file
28
src/error.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use log::error;
|
||||
use rocket::http::Status;
|
||||
use rocket::{Request, Response};
|
||||
use rocket::response::content::RawXml;
|
||||
use rocket::response::Responder;
|
||||
use rocket::serde::Serialize;
|
||||
use crate::nnid::timezones::Timezone;
|
||||
use crate::xml::{serialize_with_version, Xml};
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Error<'a>{
|
||||
pub code: &'a str,
|
||||
pub message: &'a str
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename(serialize = "errors"))]
|
||||
pub struct Errors<'a>{
|
||||
pub error: &'a [Error<'a>],
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r> Responder<'r, 'o> for Errors<'r> {
|
||||
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'o> {
|
||||
Response::build_from(Xml(self).respond_to(request)?)
|
||||
.status(Status::BadRequest)
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
46
src/main.rs
46
src/main.rs
|
|
@ -1,26 +1,60 @@
|
|||
use std::env;
|
||||
use diesel::{Connection, MysqlConnection};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use dotenvy::dotenv;
|
||||
use log::info;
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::http::Header;
|
||||
use rocket::routes;
|
||||
use sqlx::Postgres;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
|
||||
mod xml;
|
||||
mod conntest;
|
||||
mod db;
|
||||
mod nnid;
|
||||
mod account;
|
||||
mod error;
|
||||
mod dsresponse;
|
||||
|
||||
type Pool = sqlx::Pool<Postgres>;
|
||||
|
||||
#[rocket::launch]
|
||||
async fn launch() -> _ {
|
||||
dotenv().ok();
|
||||
|
||||
let act_database_url = env::var("ACCOUNT_DATABASE_URL").expect("account database url is not set");
|
||||
|
||||
let conn = MysqlConnection::establish(&act_database_url).expect("unable to connect to database");
|
||||
|
||||
let act_database_url = env::var("DATABASE_URL").expect("account database url is not set");
|
||||
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&act_database_url).await
|
||||
.expect("unable to create pool");
|
||||
|
||||
rocket::build()
|
||||
.manage(pool)
|
||||
.attach(AdHoc::on_response("org", |_, response| Box::pin(async move {
|
||||
response.adjoin_header(Header::new("x-organization", "Nintendo"));
|
||||
//response.adjoin_header(Header::new("x-organization", "Nintendo"));
|
||||
response.adjoin_header(Header::new("x-nintendo-date", SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
.to_string()
|
||||
));
|
||||
|
||||
response.adjoin_header(Header::new("Content-Type", "text/xml; charset=utf-8"));
|
||||
|
||||
|
||||
response.remove_header("x-content-type-options");
|
||||
response.remove_header("x-frame-options");
|
||||
response.remove_header("permissions-policy");
|
||||
})))
|
||||
.mount("/", routes![conntest::conntest])
|
||||
.mount("/", routes![
|
||||
conntest::conntest,
|
||||
nnid::devices::current_device_status,
|
||||
nnid::agreements::get_agreement,
|
||||
nnid::timezones::get_timezone,
|
||||
nnid::person_exists::person_exists,
|
||||
nnid::email::validate
|
||||
])
|
||||
}
|
||||
|
|
|
|||
45
src/nnid/agreements.rs
Normal file
45
src/nnid/agreements.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use std::{env, fs, io};
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::get;
|
||||
use rocket::response::content::RawXml;
|
||||
use tokio::fs::try_exists;
|
||||
use crate::dsresponse::Ds;
|
||||
use crate::nnid::devices::Device;
|
||||
use crate::xml::Xml;
|
||||
|
||||
#[get("/v1/api/content/agreements/Nintendo-Network-EULA/<lang>/@latest")]
|
||||
pub async fn get_agreement(lang: &str) -> io::Result<Ds<RawXml<NamedFile>>>{
|
||||
let base_path = {
|
||||
// if this crashes then something is wrong with the server setup so crashing here is fine imo
|
||||
let mut path = env::current_dir().unwrap();
|
||||
|
||||
path.push("res");
|
||||
path.push("agreement");
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
let requested_file_path = {
|
||||
let mut path = base_path.clone();
|
||||
|
||||
path.push(format!("{}.xml", lang));
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
|
||||
|
||||
if try_exists(&requested_file_path).await.is_ok_and(|v| v == true){
|
||||
Ok(Ds(RawXml(NamedFile::open(&requested_file_path).await?)))
|
||||
} else {
|
||||
let fallback_path = {
|
||||
let mut path = base_path;
|
||||
|
||||
path.push("DEFAULT.xml");
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
Ok(Ds(RawXml(NamedFile::open(&fallback_path).await?)))
|
||||
}
|
||||
}
|
||||
176
src/nnid/create_account.rs
Normal file
176
src/nnid/create_account.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use chrono::NaiveDate;
|
||||
use rocket::{post, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::account::account::{generate_password, User};
|
||||
use crate::error::Errors;
|
||||
use crate::Pool;
|
||||
use crate::xml::{Xml, YesNoVal};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Email{
|
||||
address: Box<str>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Mii{
|
||||
name: Box<str>,
|
||||
primary: YesNoVal,
|
||||
data: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
struct AccountCreationData{
|
||||
birth_date: NaiveDate,
|
||||
user_id: Box<str>,
|
||||
password: Box<str>,
|
||||
country: Box<str>,
|
||||
language: Box<str>,
|
||||
tz_name: Box<str>,
|
||||
email: Email,
|
||||
gender: Box<str>,
|
||||
marketing_flag: YesNoVal,
|
||||
region: i32
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "person"))]
|
||||
struct AccountCreationResponseData{
|
||||
pid: i32
|
||||
}
|
||||
|
||||
#[post("/v1/api/people", data="<data>")]
|
||||
async fn create_account(database: &State<Pool>, data: Xml<AccountCreationData>) -> Result<Xml<AccountCreationResponseData>, Errors>{
|
||||
let database = database.inner();
|
||||
|
||||
let AccountCreationData {
|
||||
user_id,
|
||||
password,
|
||||
birth_date,
|
||||
tz_name,
|
||||
language,
|
||||
email: Email{
|
||||
address
|
||||
},
|
||||
marketing_flag,
|
||||
gender,
|
||||
region,
|
||||
country,
|
||||
..
|
||||
} = *data;
|
||||
|
||||
|
||||
|
||||
let new_account = sqlx::query("
|
||||
INSERT INTO users.users (
|
||||
pid,
|
||||
username,
|
||||
password,
|
||||
birthdate,
|
||||
birthdate,
|
||||
timezone,
|
||||
email,
|
||||
country,
|
||||
language,
|
||||
marketing_allowed,
|
||||
off_device_allowed,
|
||||
region,
|
||||
mii_data
|
||||
) VALUES (
|
||||
?,?,?,?,?,?,?,?,?,?
|
||||
)
|
||||
");
|
||||
|
||||
|
||||
|
||||
let pid = connection.transaction::<_, diesel::result::Error, _>(|conn| Box::pin(async move{
|
||||
use crate::schema::users::dsl::*;
|
||||
|
||||
diesel::insert_into(users)
|
||||
.values(&new_account)
|
||||
.returning(User::as_returning())
|
||||
.get_result(conn)
|
||||
.await?;
|
||||
|
||||
|
||||
Ok(())
|
||||
})).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use chrono::NaiveDate;
|
||||
use crate::nnid::create_account::AccountCreationData;
|
||||
|
||||
const TEST_XML: &str =
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>
|
||||
<person>
|
||||
<birth_date>1991-02-03</birth_date>
|
||||
<user_id>testtest</user_id>
|
||||
<password>[PASSWORD]</password>
|
||||
<country>DE</country>
|
||||
<language>en</language>
|
||||
<tz_name>Europe/Berlin</tz_name>
|
||||
<agreement>
|
||||
<agreement_date>2025-02-24T19:42:45</agreement_date>
|
||||
<country>US</country>
|
||||
<location>https://account.spfn.cc/v1/api/content/agreements/Nintendo-Network-EULA/0300</location>
|
||||
<type>NINTENDO-NETWORK-EULA</type>
|
||||
<version>0300</version>
|
||||
</agreement>
|
||||
<email>
|
||||
<address>tvnebel@gmail.com</address>
|
||||
<owned>N</owned>
|
||||
<parent>N</parent>
|
||||
<primary>Y</primary>
|
||||
<validated>N</validated>
|
||||
<type>DEFAULT</type>
|
||||
</email>
|
||||
<mii>
|
||||
<name>y</name>
|
||||
<primary>Y</primary>
|
||||
<data>
|
||||
AwAAQDrPvmeBxJIQ3j+V8Ip4iCWDvgAAAEB5AAAAIABOAEEATQBFAAAAAAAAAEBAAAAhAQJoRBgm
|
||||
NEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAANzO
|
||||
</data>
|
||||
</mii>
|
||||
<parental_consent>
|
||||
<scope>1</scope>
|
||||
<consent_date>2025-02-24T19:42:45</consent_date>
|
||||
<approval_id>0</approval_id>
|
||||
</parental_consent>
|
||||
<gender>M</gender>
|
||||
<region>1309343744</region>
|
||||
<marketing_flag>N</marketing_flag>
|
||||
<device_attributes>
|
||||
<device_attribute>
|
||||
<name>uuid_account</name>
|
||||
<value>55fdbad0-f2ab-11ef-b648-010144cdca06</value>
|
||||
</device_attribute>
|
||||
<device_attribute>
|
||||
<name>uuid_common</name>
|
||||
<value>898ed052-5e25-11ef-b648-010144cdca06</value>
|
||||
</device_attribute>
|
||||
<device_attribute>
|
||||
<name>persistent_id</name>
|
||||
<value>8000001d</value>
|
||||
</device_attribute>
|
||||
<device_attribute>
|
||||
<name>transferable_id_base</name>
|
||||
<value>0800000444cdca06</value>
|
||||
</device_attribute>
|
||||
<device_attribute>
|
||||
<name>transferable_id_base_common</name>
|
||||
<value>0640000444cdca06</value>
|
||||
</device_attribute>
|
||||
</device_attributes>
|
||||
<off_device_flag>N</off_device_flag>
|
||||
</person>";
|
||||
#[test]
|
||||
fn test(){
|
||||
let data: AccountCreationData = quick_xml::de::from_str(TEST_XML).unwrap();
|
||||
|
||||
assert_eq!(data.birth_date, NaiveDate::from_ymd_opt(1991,02,03).unwrap());
|
||||
assert_eq!(data.user_id.as_ref(), "testtest");
|
||||
}
|
||||
}
|
||||
28
src/nnid/devices.rs
Normal file
28
src/nnid/devices.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use rocket::get;
|
||||
use serde::Serialize;
|
||||
use crate::xml::Xml;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "device"))]
|
||||
pub struct Device;
|
||||
|
||||
#[get("/v1/api/devices/@current/status")]
|
||||
pub fn current_device_status() -> Xml<Device>{
|
||||
Xml(Device)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::from_utf8;
|
||||
use crate::nnid::devices::Device;
|
||||
|
||||
#[test]
|
||||
fn test_device_data(){
|
||||
let text = crate::xml::serialize_with_version(&Device).unwrap();
|
||||
|
||||
|
||||
|
||||
println!("{}", text);
|
||||
|
||||
}
|
||||
}
|
||||
11
src/nnid/email.rs
Normal file
11
src/nnid/email.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use rocket::{post, FromForm};
|
||||
use rocket::form::Form;
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct ValidateEmailInput{
|
||||
email: String,
|
||||
}
|
||||
#[post("/v1/api/support/validate/email", data="<data>")]
|
||||
pub fn validate(data: Form<ValidateEmailInput>){
|
||||
|
||||
}
|
||||
6
src/nnid/mod.rs
Normal file
6
src/nnid/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod devices;
|
||||
pub mod agreements;
|
||||
pub mod timezones;
|
||||
pub mod person_exists;
|
||||
pub mod email;
|
||||
mod create_account;
|
||||
58
src/nnid/person_exists.rs
Normal file
58
src/nnid/person_exists.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use rocket::{get, State};
|
||||
use sqlx::Row;
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::Pool;
|
||||
use crate::xml::Xml;
|
||||
|
||||
#[get("/v1/api/people/<username>")]
|
||||
pub async fn person_exists(database: &State<Pool>, username: &str) -> Result<(), Errors<'static>>{
|
||||
let database = database.inner();
|
||||
|
||||
let exists: bool = sqlx::query_as!(
|
||||
bool,
|
||||
"SELECT EXISTS(SELECT 1 FROM users.users WHERE username = ? )",
|
||||
username
|
||||
).fetch_one(database)
|
||||
.await
|
||||
.unwrap_or(true);
|
||||
|
||||
if exists {
|
||||
Err(
|
||||
Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "0100",
|
||||
message: "Account ID already exists"
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::xml::serialize_with_version;
|
||||
|
||||
#[test]
|
||||
fn test(){
|
||||
let val = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
code: "0100",
|
||||
message: "Account ID already exists"
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
let enc = serialize_with_version(&val).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
enc.as_ref(),
|
||||
"<?xml version=\"1.0\"?><errors><error><code>0100</code><message>Account ID already exists</message></error></errors>"
|
||||
)
|
||||
}
|
||||
}
|
||||
64
src/nnid/timezones.rs
Normal file
64
src/nnid/timezones.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use std::collections::HashMap;
|
||||
use std::{env, fs};
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::get;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::from_slice;
|
||||
use crate::xml::{serialize_with_version, Xml};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename(serialize = "timezone"))]
|
||||
pub struct Timezone{
|
||||
area: String,
|
||||
language: String,
|
||||
name: String,
|
||||
utc_offset: String,
|
||||
order: String
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename(serialize = "timezones"))]
|
||||
pub struct Timezones<'a>{
|
||||
pub timezone: &'a [Timezone],
|
||||
}
|
||||
|
||||
pub static TIMEZONES: Lazy<HashMap<String, HashMap<String, Vec<Timezone>>>> = Lazy::new(||{
|
||||
let path = {
|
||||
// if this crashes then something is wrong with the server setup so crashing here is fine imo
|
||||
let mut path = env::current_dir().unwrap();
|
||||
|
||||
path.push("res");
|
||||
path.push("timezones.json");
|
||||
|
||||
path
|
||||
};
|
||||
serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap()
|
||||
});
|
||||
|
||||
|
||||
#[get("/v1/api/content/time_zones/<zone>/<lang>")]
|
||||
pub fn get_timezone(zone: &str, lang: &str) -> Option<Xml<Timezones<'static>>>{
|
||||
let timezone = (&*TIMEZONES).get(zone)?.get(lang)?;
|
||||
let timezones = Timezones{ timezone };
|
||||
Some(Xml(timezones))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test{
|
||||
use crate::nnid::timezones::{Timezones, TIMEZONES};
|
||||
use crate::xml::serialize_with_version;
|
||||
|
||||
#[test]
|
||||
fn test(){
|
||||
let timezone = (&*TIMEZONES).get("DE").unwrap().get("en").unwrap();
|
||||
let timezones = Timezones{ timezone };
|
||||
let ser = serialize_with_version(&timezones).unwrap();
|
||||
|
||||
println!("{}", ser);
|
||||
|
||||
assert_eq!(
|
||||
ser.as_ref(),
|
||||
"<?xml version=\"1.0\"?><timezones><timezone><area>Europe/Berlin</area><language>en</language><name>Amsterdam, Berlin, Rome</name><utc_offset>3600</utc_offset><order>0</order></timezone></timezones>"
|
||||
)
|
||||
}
|
||||
}
|
||||
132
src/xml.rs
132
src/xml.rs
|
|
@ -1,17 +1,51 @@
|
|||
use std::fmt::Formatter;
|
||||
use std::io::Cursor;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::result;
|
||||
use rocket::http::Status;
|
||||
use rocket::Request;
|
||||
use rocket::{async_trait, Data, Request};
|
||||
use rocket::response::Responder;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use rocket::response::Result;
|
||||
use log::error;
|
||||
use quick_xml::events::{BytesDecl, Event};
|
||||
use quick_xml::se::Serializer;
|
||||
use quick_xml::{DeError, SeError};
|
||||
use rocket::data::{ByteUnit, FromData, Outcome};
|
||||
use rocket::response::content::RawXml;
|
||||
use rocket::response::status::BadRequest;
|
||||
use serde::__private::de::UntaggedUnitVisitor;
|
||||
use serde::de::{DeserializeOwned, Error, Visitor};
|
||||
use thiserror::Error;
|
||||
|
||||
pub fn serialize_with_version(serializable: &impl Serialize) -> result::Result<Box<str>, SeError>{
|
||||
let mut write_dest = "<?xml version=\"1.0\"?>".to_owned();
|
||||
|
||||
|
||||
|
||||
serializable.serialize(Serializer::new(&mut write_dest))?;
|
||||
Ok(write_dest.into_boxed_str())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Xml<T>(pub T);
|
||||
|
||||
impl<T> Deref for Xml<T>{
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for Xml<T>{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r, T: Serialize> Responder<'r, 'o> for Xml<T>{
|
||||
fn respond_to(self, request: &'r Request<'_>) -> Result<'o> {
|
||||
match quick_xml::se::to_string(&self.0){
|
||||
match serialize_with_version(&self.0){
|
||||
Ok(ser) => {
|
||||
RawXml(ser).respond_to(request)
|
||||
},
|
||||
|
|
@ -21,4 +55,96 @@ impl<'r, 'o: 'r, T: Serialize> Responder<'r, 'o> for Xml<T>{
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r, T: DeserializeOwned> FromData<'r> for Xml<T>{
|
||||
type Error = Option<DeError>;
|
||||
|
||||
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
|
||||
let data = data.open(1 * ByteUnit::MB);
|
||||
|
||||
let Ok(data) = data.into_string().await else {
|
||||
return Outcome::Error((Status::BadRequest, None))
|
||||
};
|
||||
|
||||
|
||||
|
||||
match quick_xml::de::from_str(&data){
|
||||
Ok(v) => Outcome::Success(Self(v)),
|
||||
Err(e) => Outcome::Error((Status::BadRequest, Some(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub struct YesNoVal(pub bool);
|
||||
|
||||
struct YesNoVisitor;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("did not find Y or N")]
|
||||
struct NotYNError;
|
||||
|
||||
|
||||
|
||||
impl<'de> Visitor<'de> for YesNoVisitor{
|
||||
type Value = YesNoVal;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "expected Y or N")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> result::Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
match v{
|
||||
"Y" => Ok(YesNoVal(true)),
|
||||
"N" => Ok(YesNoVal(false)),
|
||||
_ => Err(E::custom("didnt get N or Y"))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> result::Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
|
||||
const Y_BYTES: &[u8] = "Y".as_bytes();
|
||||
const N_BYTES: &[u8] = "N".as_bytes();
|
||||
|
||||
match v{
|
||||
Y_BYTES => Ok(YesNoVal(true)),
|
||||
N_BYTES => Ok(YesNoVal(false)),
|
||||
_ => Err(E::custom("didnt get N or Y"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for YesNoVal{
|
||||
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>
|
||||
{
|
||||
deserializer.deserialize_str(YesNoVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for YesNoVal{
|
||||
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_char(
|
||||
match self.0{
|
||||
true => 'Y',
|
||||
false => 'N',
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue