Skip to content

Commit 36b5563

Browse files
committed
AI fix problems
1 parent db8f160 commit 36b5563

File tree

9 files changed

+148
-32
lines changed

9 files changed

+148
-32
lines changed

.env.example

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,21 @@ SERVER_PORT=3000
5252
# Default is 28800 seconds (8 hours)
5353
# This should be relatively short since username<->uuid mappings are unreliable
5454
USERNAME_CACHE_SECONDS=28800
55+
56+
# Hash-based Endpoint Cache Configuration
57+
# Cache lifetime in seconds for the /download/:hash endpoint
58+
# Default is 1209600 seconds (14 days)
59+
# This can be longer since texture hashes don't change
60+
HASH_CACHE_SECONDS=1209600
61+
62+
# CORS Configuration
63+
# Comma-separated list of allowed origins for CORS
64+
# Use "*" to allow all origins (NOT recommended for production)
65+
# Example: CORS_ALLOWED_ORIGINS=https://example.com,https://app.example.com
66+
# If not set, defaults to allowing all origins (development mode)
67+
CORS_ALLOWED_ORIGINS=*
68+
69+
# Use Database Username in Mojang Requests
70+
# If true, attempts to look up username from database and resolve via Mojang API
71+
# Default is true
72+
USE_DATABASE_USERNAME_IN_MOJANG_REQUESTS=true

Cargo.lock

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ base64 = "0.22"
5353
# Logging
5454
tracing = "0.1"
5555
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
56-
dotenv = "0.15.0"
5756

5857
[features]
5958
default = ["s3"]

src/auth.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,23 @@ where
165165
.strip_prefix("admin_token:")
166166
.unwrap_or(&admin_token);
167167

168-
if token != expected_token {
168+
// Use constant-time comparison to prevent timing attacks
169+
// Convert both tokens to fixed-size arrays for comparison
170+
let token_bytes = token.as_bytes();
171+
let expected_bytes = expected_token.as_bytes();
172+
173+
// Compare lengths first in constant time
174+
if token_bytes.len() != expected_bytes.len() {
175+
return Err((StatusCode::UNAUTHORIZED, "Invalid admin token".to_string()));
176+
}
177+
178+
// Compare byte by byte in constant time
179+
let mut result = 0u8;
180+
for (a, b) in token_bytes.iter().zip(expected_bytes.iter()) {
181+
result |= a ^ b;
182+
}
183+
184+
if result != 0 {
169185
return Err((StatusCode::UNAUTHORIZED, "Invalid admin token".to_string()));
170186
}
171187

src/config.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub struct Config {
2020
pub username_cache_seconds: u64,
2121
pub hash_cache_seconds: u64,
2222
pub use_database_username_in_mojang_requests: bool,
23+
pub cors_allowed_origins: Option<String>,
2324
}
2425

2526
#[derive(Debug, Deserialize, Clone, PartialEq)]
@@ -75,7 +76,6 @@ impl Config {
7576

7677
Ok(Config {
7778
database_url: env::var("DATABASE_URL")
78-
.or_else(|_| env::var("DATABASE_URL"))
7979
.map_err(|_| anyhow::anyhow!("DATABASE_URL must be set"))?,
8080
jwt_public_key: env::var("JWT_PUBLIC_KEY")
8181
.map_err(|_| anyhow::anyhow!("JWT_PUBLIC_KEY must be set"))?,
@@ -110,6 +110,7 @@ impl Config {
110110
.unwrap_or_else(|_| "true".to_string()) // 14 days default
111111
.parse()
112112
.map_err(|e| anyhow::anyhow!("Invalid USE_DATABASE_USERNAME_IN_MOJANG_REQUESTS: {}", e))?,
113+
cors_allowed_origins: env::var("CORS_ALLOWED_ORIGINS").ok(),
113114
})
114115
}
115116

src/handlers.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ use sqlx::PgPool;
1717
use std::sync::Arc;
1818
use uuid::Uuid;
1919

20+
/// Maximum file size for uploads (1 MB)
21+
/// PNG texture files for Minecraft skins/capes should never exceed this
22+
const MAX_FILE_SIZE: usize = 1_048_576; // 1 MB in bytes
23+
2024
#[derive(Clone)]
2125
pub struct AppState {
2226
pub db: PgPool,
@@ -144,6 +148,18 @@ pub async fn upload_texture(
144148
)
145149
})?;
146150

151+
// Validate file size
152+
if data.len() > MAX_FILE_SIZE {
153+
return Err((
154+
StatusCode::BAD_REQUEST,
155+
format!(
156+
"File size {} bytes exceeds maximum allowed size of {} bytes (1 MB)",
157+
data.len(),
158+
MAX_FILE_SIZE
159+
),
160+
));
161+
}
162+
147163
// Validate PNG
148164
if !is_png(&data) {
149165
return Err((
@@ -331,6 +347,18 @@ pub async fn admin_upload_texture(
331347
)
332348
})?;
333349

350+
// Validate file size
351+
if data.len() > MAX_FILE_SIZE {
352+
return Err((
353+
StatusCode::BAD_REQUEST,
354+
format!(
355+
"File size {} bytes exceeds maximum allowed size of {} bytes (1 MB)",
356+
data.len(),
357+
MAX_FILE_SIZE
358+
),
359+
));
360+
}
361+
334362
// Validate PNG
335363
if !is_png(&data) {
336364
return Err((

src/main.rs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use std::net::SocketAddr;
1717
use std::sync::Arc;
1818
use storage::create_storage;
1919
use tower_http::cors::{Any, CorsLayer};
20+
use tracing::warn;
2021
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
2122

2223
use crate::auth::decode_key;
@@ -94,12 +95,7 @@ async fn main() -> anyhow::Result<()> {
9495
state.clone(),
9596
add_public_key_to_state,
9697
))
97-
.layer(
98-
CorsLayer::new()
99-
.allow_origin(Any)
100-
.allow_methods(Any)
101-
.allow_headers(Any),
102-
)
98+
.layer(build_cors_layer(&config))
10399
.with_state(state);
104100

105101
// Start server
@@ -130,3 +126,43 @@ async fn add_public_key_to_state(
130126

131127
next.run(request).await
132128
}
129+
130+
/// Build CORS layer based on configuration
131+
/// If CORS_ALLOWED_ORIGINS is set, use those specific origins
132+
/// Otherwise, allow all origins (for development)
133+
fn build_cors_layer(config: &Config) -> CorsLayer {
134+
if let Some(ref allowed_origins) = config.cors_allowed_origins {
135+
// Parse comma-separated list of origins
136+
let origins: Vec<&str> = allowed_origins.split(',').map(|s| s.trim()).collect();
137+
138+
if origins.is_empty() || (origins.len() == 1 && origins[0] == "*") {
139+
// Allow all origins
140+
tracing::warn!("CORS configured to allow all origins - this should not be used in production!");
141+
CorsLayer::new()
142+
.allow_origin(Any)
143+
.allow_methods(Any)
144+
.allow_headers(Any)
145+
} else {
146+
// Allow specific origins
147+
tracing::info!("CORS configured to allow specific origins: {:?}", origins);
148+
let mut cors = CorsLayer::new();
149+
150+
for origin in origins {
151+
cors = cors.allow_origin(origin.parse::<axum::http::HeaderValue>().unwrap_or_else(|_| {
152+
tracing::warn!("Invalid origin '{}', skipping", origin);
153+
axum::http::HeaderValue::from_static("")
154+
}));
155+
}
156+
157+
cors.allow_methods(Any)
158+
.allow_headers(Any)
159+
}
160+
} else {
161+
// Default: allow all origins (development mode)
162+
tracing::warn!("CORS_ALLOWED_ORIGINS not set, allowing all origins - configure this for production!");
163+
CorsLayer::new()
164+
.allow_origin(Any)
165+
.allow_methods(Any)
166+
.allow_headers(Any)
167+
}
168+
}

src/retrieval/mojang.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,21 +246,32 @@ impl TextureRetriever for MojangRetriever {
246246
"#,
247247
user_uuid
248248
).fetch_optional(db).await {
249-
Ok(e) => {
250-
if let Some(record) = e {
251-
fetch_uuid = self
252-
.resolve_username_to_uuid(&record.username)
253-
.await
254-
.map_err(|_| anyhow!("Failed to lookup username from mojang"))?
255-
.ok_or(anyhow!("Failed to lookup username from mojang"))?;
249+
Ok(Some(record)) => {
250+
match self.resolve_username_to_uuid(&record.username).await {
251+
Ok(Some(resolved_uuid)) => {
252+
fetch_uuid = resolved_uuid;
253+
}
254+
Ok(None) => {
255+
tracing::warn!(
256+
"Username '{}' not found in Mojang API, using original UUID",
257+
record.username
258+
);
259+
// Keep using the original UUID
260+
}
261+
Err(e) => {
262+
tracing::error!("Failed to resolve username from Mojang: {}", e);
263+
// Continue with original UUID
264+
}
256265
}
257266
}
267+
Ok(None) => {
268+
tracing::debug!("No username mapping found for UUID {}", user_uuid);
269+
}
258270
Err(e) => {
259-
tracing::error!("Failed to lookup username: {}", e);
260-
return Err(e)
261-
.map_err(|_| anyhow!("Failed to lookup username from mojang"));
271+
tracing::error!("Failed to lookup username from database: {}", e);
272+
// Continue with original UUID
262273
}
263-
};
274+
}
264275
}
265276
}
266277

src/storage/s3.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ impl S3Storage {
3838
#[cfg(feature = "s3")]
3939
{
4040
use aws_config::BehaviorVersion;
41-
use aws_sdk_s3::config::Credentials;
41+
use aws_sdk_s3::config::{Builder, Credentials, Region};
42+
use aws_sdk_s3::Config;
4243

43-
let mut config_loader = aws_config::defaults(BehaviorVersion::latest());
44+
let region = Region::new(self.region.clone());
45+
let mut builder = Builder::new().region(region.clone());
4446

4547
// Add credentials if provided
4648
if let Some(creds) = &self.credentials {
47-
config_loader = config_loader.credentials_provider(Credentials::new(
49+
builder = builder.credentials_provider(Credentials::new(
4850
&creds.access_key,
4951
&creds.secret_key,
5052
None,
@@ -53,8 +55,20 @@ impl S3Storage {
5355
));
5456
}
5557

56-
let config = config_loader.load().await;
57-
Ok(aws_sdk_s3::Client::new(&config))
58+
// Configure custom endpoint if provided (for S3-compatible services)
59+
let config = if let Some(endpoint) = &self.endpoint {
60+
// For custom S3-compatible services (MinIO, Wasabi, etc.)
61+
builder.endpoint_url(endpoint).build()
62+
} else {
63+
// Use standard AWS S3 configuration
64+
let aws_config = aws_config::defaults(BehaviorVersion::latest())
65+
.region(region)
66+
.load()
67+
.await;
68+
builder.build()
69+
};
70+
71+
Ok(aws_sdk_s3::Client::from_conf(config))
5872
}
5973

6074
#[cfg(not(feature = "s3"))]

0 commit comments

Comments
 (0)