Skip to content

Commit

Permalink
Cors + rate limiter + cache headers
Browse files Browse the repository at this point in the history
  • Loading branch information
dcadenas committed Sep 19, 2024
1 parent bb92cec commit 4b7b203
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 133 deletions.
89 changes: 89 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ config_rs = { version = "0.14", package = "config", features = ["yaml"] }
env_logger = "0.11.5"
futures = "0.3.30"
gcloud-sdk = { version = "0.25.6", features = ["google-pubsub-v1"] }
hyper = "1.4.1"
log = "0.4.22"
metrics = "0.23.0"
metrics-exporter-prometheus = "0.15.3"
Expand All @@ -35,7 +36,9 @@ tokio = { version = "1.40.0", features = ["full", "test-util"] }
tokio-cron-scheduler = "0.11.0"
tokio-retry = "0.3.0"
tokio-util = { version = "0.7.11", features = ["rt"] }
tower-http = { version = "0.5.2", features = ["timeout", "trace"] }
tower = "0.5.1"
tower-http = { version = "0.5.2", features = ["cors", "timeout", "trace"] }
tower_governor = "0.4.2"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "tracing-log"] }

Expand Down
10 changes: 4 additions & 6 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ followers:
worker_timeout_secs: 15
google_project_id: "pub-verse-app"
google_topic: "follow-changes"
# how often to flush the buffer to generate messages
flush_period_seconds: 60
# 30 minutes, so no more than 2 messages per hour
min_seconds_between_messages: 1800
# Daily at midnight
pagerank_cron_expression: "0 0 0 * * *"
flush_period_seconds: 60 # how often to flush the buffer to generate messages
min_seconds_between_messages: 1800 # 30 minutes, so no more than 2 messages per hour
pagerank_cron_expression: "0 0 0 * * *" # Daily at midnight
http_cache_seconds: 86400 # 24 hours
1 change: 1 addition & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct Settings {
pub tcp_importer_port: u16,
pub http_port: u16,
pub pagerank_cron_expression: String,
pub http_cache_seconds: u32,
}

impl Configurable for Settings {
Expand Down
24 changes: 16 additions & 8 deletions src/http_server.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
mod handlers;
mod router;
use crate::repo::{Recommendation, Repo, RepoTrait};

use crate::{
config::Settings,
repo::{Recommendation, Repo, RepoTrait},
};
use anyhow::{Context, Result};
use axum::Router;
use moka::future::Cache;
Expand Down Expand Up @@ -47,14 +52,14 @@ pub struct HttpServer;
impl HttpServer {
pub fn start(
task_tracker: TaskTracker,
http_port: u16,
settings: &Settings,
repo: Arc<Repo>,
cancellation_token: CancellationToken,
) -> Result<()> {
let state = Arc::new(AppState::new(repo));
let router = create_router(state)?;
let router = create_router(state, settings)?;

start_http_server(task_tracker, http_port, router, cancellation_token);
start_http_server(task_tracker, settings.http_port, router, cancellation_token);

Ok(())
}
Expand All @@ -76,10 +81,13 @@ fn start_http_server(

let token_clone = cancellation_token.clone();
let server_future = tokio::spawn(async {
axum::serve(listener, router)
.with_graceful_shutdown(shutdown_hook(token_clone))
.await
.context("Failed to start HTTP server")
axum::serve(
listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown_hook(token_clone))
.await
.context("Failed to start HTTP server")
});

await_shutdown(cancellation_token, server_future).await;
Expand Down
96 changes: 96 additions & 0 deletions src/http_server/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use super::router::ApiError;
use super::AppState;
use crate::repo::{Recommendation, RepoTrait};
use axum::{extract::State, http::HeaderMap, response::Html, response::IntoResponse, Json};
use nostr_sdk::PublicKey;
use std::sync::Arc;
use tracing::info;

pub async fn cached_get_recommendations<T>(
State(state): State<Arc<AppState<T>>>,
axum::extract::Path(pubkey): axum::extract::Path<String>,
) -> Result<Json<Vec<Recommendation>>, ApiError>
where
T: RepoTrait,
{
let public_key = PublicKey::from_hex(&pubkey).map_err(ApiError::InvalidPublicKey)?;
if let Some(cached_recommendation_result) = state.recommendation_cache.get(&pubkey).await {
return Ok(Json(cached_recommendation_result));
}

let recommendations = get_recommendations(&state.repo, public_key).await?;

state
.recommendation_cache
.insert(pubkey, recommendations.clone())
.await;

Ok(Json(recommendations))
}

async fn get_recommendations<T>(
repo: &Arc<T>,
public_key: PublicKey,
) -> Result<Vec<Recommendation>, ApiError>
where
T: RepoTrait,
{
repo.get_recommendations(&public_key)
.await
.map_err(ApiError::from)
}

pub async fn cached_maybe_spammer<T>(
State(state): State<Arc<AppState<T>>>, // Extract shared state with generic RepoTrait
axum::extract::Path(pubkey): axum::extract::Path<String>, // Extract pubkey from the path
) -> Result<Json<bool>, ApiError>
where
T: RepoTrait,
{
println!("cached_maybe_spammer");
let public_key = PublicKey::from_hex(&pubkey).map_err(ApiError::InvalidPublicKey)?;
println!("cached_maybe_spammer2");

if let Some(cached_spammer_result) = state.spammer_cache.get(&pubkey).await {
return Ok(Json(cached_spammer_result));
}

let is_spammer = maybe_spammer(&state.repo, public_key).await?;

state.spammer_cache.insert(pubkey, is_spammer).await;

Ok(Json(is_spammer))
}

async fn maybe_spammer<T>(repo: &Arc<T>, public_key: PublicKey) -> Result<bool, ApiError>
where
T: RepoTrait,
{
info!("Checking if {} is a spammer", public_key.to_hex());
let pagerank = repo
.get_pagerank(&public_key)
.await
.map_err(|_| ApiError::NotFound)?;

info!("Pagerank for {}: {}", public_key.to_hex(), pagerank);

Ok(pagerank < 0.2)
// TODO don't return if it's too low, instead use other manual
// checks, nos user, nip05, check network, more hints, and only then
// give up
}

pub async fn serve_root_page(_headers: HeaderMap) -> impl IntoResponse {
let body = r#"
<html>
<head>
<title>Nos Followers Server</title>
</head>
<body>
<h1>Healthy</h1>
</body>
</html>
"#;

Html(body)
}
Loading

0 comments on commit 4b7b203

Please sign in to comment.