Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/auth/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ pub struct GithubErrorResponse {
error_uri: String,
}

#[derive(Clone)]
pub struct GithubClient {
client_id: String,
client_secret: String,
http_client: Client,
}

#[derive(Serialize)]
Expand Down Expand Up @@ -77,13 +79,18 @@ pub struct GitHubFetchedUser {
}

impl GithubClient {
pub fn new(client_id: String, client_secret: String) -> GithubClient {
pub fn new(client_id: String, client_secret: String, http_client: Client) -> GithubClient {
GithubClient {
client_id,
client_secret,
http_client,
}
}

pub fn client_id(&self) -> &str {
&self.client_id
}

pub async fn start_polling_auth(
&self,
ip: IpNetwork,
Expand All @@ -98,7 +105,8 @@ impl GithubClient {
}
}

let res = Client::new()
let res = self
.http_client
.post("https://github.com/login/device/code")
.header("Accept", HeaderValue::from_static("application/json"))
.basic_auth(&self.client_id, Some(&self.client_secret))
Expand Down Expand Up @@ -172,7 +180,8 @@ impl GithubClient {
}
};

let resp = Client::new()
let resp = self
.http_client
.post("https://github.com/login/oauth/access_token")
.header("Accept", HeaderValue::from_str("application/json").unwrap())
.header(
Expand Down Expand Up @@ -203,7 +212,8 @@ impl GithubClient {
.to_string())
}
pub async fn get_user(&self, token: &str) -> Result<GitHubFetchedUser, AuthenticationError> {
let resp = Client::new()
let resp = self
.http_client
.get("https://api.github.com/user")
.header("Accept", HeaderValue::from_str("application/json").unwrap())
.header("User-Agent", "geode_index")
Expand Down Expand Up @@ -234,8 +244,8 @@ impl GithubClient {
&self,
token: &str,
) -> Result<GitHubFetchedUser, AuthenticationError> {
let client = Client::new();
let resp = client
let resp = self
.http_client
.get("https://api.github.com/installation/repositories")
.header("Accept", HeaderValue::from_str("application/json").unwrap())
.header("User-Agent", "geode_index")
Expand Down
40 changes: 12 additions & 28 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use moka::future::Cache;
use tokio::sync::mpsc::Sender;

use crate::{
auth::github::GithubClient,
dns::ValidateDnsResolver,
endpoints::mods::IndexQueryParams,
s3_worker::S3WorkerTask,
Expand All @@ -22,7 +23,7 @@ pub struct AppData {
db: sqlx::postgres::PgPool,
app_url: String,
front_url: String,
github: GitHubClientData,
github_client: GithubClient,
webhook_url: String,
index_admin_webhook_url: String,
static_storage: PublicDisk,
Expand All @@ -41,12 +42,6 @@ pub struct AppData {
s3_sender: OnceLock<Sender<S3WorkerTask>>,
}

#[derive(Clone)]
pub struct GitHubClientData {
client_id: String,
client_secret: String,
}

pub async fn build_config() -> anyhow::Result<AppData> {
let env_url = dotenvy::var("DATABASE_URL")?;

Expand Down Expand Up @@ -94,14 +89,17 @@ pub async fn build_config() -> anyhow::Result<AppData> {
.timeout(Duration::from_secs(30))
.build()?;

let http_client = reqwest::Client::builder()
.pool_max_idle_per_host(4)
.connect_timeout(Duration::from_secs(10))
.read_timeout(Duration::from_secs(30))
.build()?;

Ok(AppData {
db: pool,
app_url: app_url.clone(),
front_url,
github: GitHubClientData {
client_id: github_client,
client_secret: github_secret,
},
github_client: GithubClient::new(github_client, github_secret, http_client.clone()),
webhook_url,
index_admin_webhook_url,
static_storage: PublicDisk::new(
Expand All @@ -119,26 +117,12 @@ pub async fn build_config() -> anyhow::Result<AppData> {
port,
debug,
mods_cache,
http_client: reqwest::Client::builder()
.pool_max_idle_per_host(4)
.connect_timeout(Duration::from_secs(10))
.read_timeout(Duration::from_secs(30))
.build()?,
http_client,
check_dns_http_client,
s3_sender: OnceLock::new(),
})
}

impl GitHubClientData {
pub fn client_id(&self) -> &str {
&self.client_id
}

pub fn client_secret(&self) -> &str {
&self.client_secret
}
}

impl AppData {
pub fn db(&self) -> &sqlx::postgres::PgPool {
&self.db
Expand All @@ -152,8 +136,8 @@ impl AppData {
&self.front_url
}

pub fn github(&self) -> &GitHubClientData {
&self.github
pub fn github_client(&self) -> &GithubClient {
&self.github_client
}

pub fn webhook_url(&self) -> &str {
Expand Down
28 changes: 8 additions & 20 deletions src/endpoints/auth/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::database::repository::{
};
use crate::endpoints::ApiError;
use crate::endpoints::auth::TokensResponse;
use crate::{auth::github, types::api::ApiResponse};
use crate::types::api::ApiResponse;

#[derive(Deserialize, ToSchema)]
struct PollParams {
Expand Down Expand Up @@ -46,10 +46,7 @@ pub async fn start_github_login(
info: ConnectionInfo,
) -> Result<impl Responder, ApiError> {
let mut pool = data.db().acquire().await?;
let client = github::GithubClient::new(
data.github().client_id().to_string(),
data.github().client_secret().to_string(),
);
let client = data.github_client();

let Some(ip) = info.realip_remote_addr() else {
return Err(ApiError::InternalError(
Expand Down Expand Up @@ -83,12 +80,13 @@ pub async fn start_github_web_login(data: web::Data<AppData>) -> Result<impl Res
let mut pool = data.db().acquire().await?;

let secret = github_web_logins::create_unique(&mut pool).await?;
let client = data.github_client();

Ok(web::Json(ApiResponse {
error: "".into(),
payload: format!(
"https://github.com/login/oauth/authorize?client_id={}&redirect_uri={}/login/github/callback&scope=read:user&state={}",
data.github().client_id(),
client.client_id(),
data.front_url(),
secret
),
Expand Down Expand Up @@ -124,11 +122,7 @@ pub async fn github_web_callback(

github_web_logins::remove(parsed, &mut pool).await?;

let client = github::GithubClient::new(
data.github().client_id().to_string(),
data.github().client_secret().to_string(),
);

let client = data.github_client();
let token = client
.poll_github(&json.code, false, Some(data.front_url()))
.await?;
Expand Down Expand Up @@ -207,10 +201,7 @@ pub async fn poll_github_login(

let mut tx = pool.begin().await?;

let client = github::GithubClient::new(
data.github().client_id().to_string(),
data.github().client_secret().to_string(),
);
let client = data.github_client();
github_login_attempts::poll_now(uuid, &mut tx).await?;
let token = client.poll_github(&attempt.device_code, true, None).await?;
github_login_attempts::remove(uuid, &mut tx).await?;
Expand Down Expand Up @@ -276,15 +267,12 @@ pub async fn github_token_login(
json: web::Json<TokenLoginParams>,
data: web::Data<AppData>,
) -> Result<impl Responder, ApiError> {
let client = github::GithubClient::new(
data.github().client_id().to_string(),
data.github().client_secret().to_string(),
);
let client = data.github_client();

let user = match client.get_user(&json.token).await {
Err(_) => client.get_installation(&json.token).await.map_err(|e| {
tracing::error!(error = ?e, "invalid access token");
ApiError::BadRequest(format!("Invalid access token: {}", json.token))
ApiError::BadRequest("Invalid access token".to_owned())
})?,

Ok(u) => u,
Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ pub async fn get_many(
platform: query.platform,
prerelease: query.prerelease.unwrap_or_default(),
},
query.per_page.unwrap_or(10),
query.per_page.unwrap_or(10).min(50),
query.page.unwrap_or(1),
&mut pool,
)
Expand Down
86 changes: 86 additions & 0 deletions src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{
types::{api::ApiResponse, models::mod_gd_version::PlatformParseError},
};
use actix_web::{HttpResponse, http::StatusCode};
use validator::{ValidationError, ValidationErrors};

pub mod auth;
pub mod deprecations;
Expand Down Expand Up @@ -77,3 +78,88 @@ impl actix_web::ResponseError for ApiError {
HttpResponse::build(self.status_code()).json(self.as_response())
}
}

// validator errors

impl From<ValidationError> for ApiError {
fn from(value: ValidationError) -> Self {
ApiError::BadRequest(format_validation_error(&value))
}
}

impl From<ValidationErrors> for ApiError {
fn from(value: ValidationErrors) -> Self {
ApiError::BadRequest(format_validation_errors(&value))
}
}

pub fn format_validation_error(e: &validator::ValidationError) -> String {
if let Some(msg) = &e.message {
return msg.to_string();
}

match e.code.as_ref() {
"length" => {
let min = e
.params
.get("min")
.and_then(|v| v.as_u64())
.map(|v| v.to_string());

let max = e
.params
.get("max")
.and_then(|v| v.as_u64())
.map(|v| v.to_string());

match (min, max) {
(Some(min), Some(max)) => {
format!("length must be between {min} and {max} characters")
}
(Some(min), None) => format!("length must be at least {min} characters"),
(None, Some(max)) => format!("length must be at most {max} characters"),
(None, None) => "invalid length".to_string(),
}
}

e => e.to_owned(),
}
}

pub fn format_validation_errors(e: &validator::ValidationErrors) -> String {
use validator::ValidationErrorsKind;

let mut str_errors = Vec::new();

for (field, err) in e.errors() {
match err {
ValidationErrorsKind::Struct(s) => {
str_errors.push(format!(
"field '{field}' is invalid ({})",
format_validation_errors(s)
));
}

ValidationErrorsKind::Field(errors) => {
let joined = errors
.iter()
.map(format_validation_error)
.collect::<Vec<_>>()
.join(", ");

str_errors.push(format!("field '{field}' is invalid: {}", joined));
}

ValidationErrorsKind::List(map) => {
for (index, errors) in map {
str_errors.push(format!(
"field '{field}' at index {index} is invalid ({})",
format_validation_errors(errors)
));
}
}
}
}

str_errors.join(", ")
}
14 changes: 13 additions & 1 deletion src/endpoints/mod_version_submissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::webhook::discord::DiscordWebhook;
use actix_multipart::Multipart;
use actix_web::{HttpResponse, Responder, delete, get, post, put, web};
use futures::StreamExt;
use image::{DynamicImage, ImageReader, Limits};
use serde::Deserialize;
use sqlx::{Acquire, PgConnection};
use std::collections::HashMap;
Expand Down Expand Up @@ -839,7 +840,7 @@ pub async fn upload_attachments(
images
.into_iter()
.map(|raw| {
let img = image::load_from_memory(&raw)
let img = safe_image_decode(&raw)
.map_err(|e| ApiError::BadRequest(format!("Invalid image: {e}")))?;
let mut webp_bytes: Vec<u8> = Vec::new();
img.write_to(
Expand Down Expand Up @@ -999,3 +1000,14 @@ pub async fn delete_attachment(

Ok(HttpResponse::NoContent())
}

fn safe_image_decode(raw: &[u8]) -> Result<DynamicImage, image::ImageError> {
let mut limits = Limits::default();
limits.max_image_width = Some(2048);
limits.max_image_height = Some(2048);
limits.max_alloc = Some(64 * 1024 * 1024);

let mut reader = ImageReader::new(std::io::Cursor::new(raw));
reader.limits(limits);
reader.with_guessed_format()?.decode()
}
Loading
Loading