Something went wrong. Try again.
Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations. pdsmoover.com
pds atproto migrations moo cow
Something went wrong. Try again.
7.7 kB · 230 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231mod handlers;mod plc;
use crate::handlers::well_known::{ ServiceDID, ServiceDocument, ServiceKey, build_service_document, handle_wellknown_did_web,};use crate::handlers::xrpc::com_atproto_sync::atproto_routes;use crate::handlers::xrpc::com_pdsmoover_admin_handlers::admin_routes;use crate::handlers::xrpc::com_pdsmoover_backup_handlers::backup_routes;use crate::handlers::xrpc::com_pdsmoover_pds_handlers::{pds_admin_routes, pds_routes};use crate::handlers::xrpc::with_ip_rate_limit;use atproto_identity::key::to_public;use axum::http::header;use axum::{Router, routing::get};use chrono::{DateTime, Utc};use dotenvy::dotenv;use jacquard_identity::PublicResolver;use s3::creds::Credentials;use s3::{Bucket, Region};use shared::db::Db;use std::collections::HashSet;use std::env;use std::net::SocketAddr;use std::sync::Arc;use std::time::Duration;use tokio::net::TcpListener;use tokio::sync::RwLock;use tower_http::cors::{Any, CorsLayer};use tracing::{info, warn};use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Clone)]struct AppState { db: Db, public_resolver: Arc<PublicResolver>, service_document: ServiceDocument, did_web: ServiceDID, admin_token: Option<String>, /// DIDs allowed to call the `com.pdsmoover.pds.*` admin endpoints. admin_dids: Arc<HashSet<String>>, s3_bucket: Arc<Box<s3::Bucket>>, describe_server_cache: Arc<RwLock<Option<(shared::db::DescribeServerRow, DateTime<Utc>)>>>, plc_client: Arc<plc::PlcClient>,}
#[tokio::main]async fn main() -> anyhow::Result<()> { // Load environment variables from .env if present let _ = dotenv();
let _prod = std::env::var("PROD") .unwrap_or_else(|_| "false".to_string()) .parse::<bool>() .unwrap_or(false);
// Initialize tracing subscriber with env filter (RUST_LOG) and pretty formatter let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::registry() .with(env_filter) .with(fmt::layer().compact()) .init();
// Initialize DB let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set, e.g. postgres://user:pass@localhost:5432/dbname"); let db = Db::connect(&database_url).await?; db.apply_migrations().await?;
//did:web/atproto service setup let external_domain = env::var("EXTERNAL_DOMAIN")?; let service_key_string = env::var("SERVICE_KEY")?; let service_did = ServiceDID(format!("did:web:{}", external_domain)); let service_key: ServiceKey = service_key_string.try_into()?;
let public_service_key = to_public(&service_key.0) .map(|public_key_data| public_key_data.to_string()) .expect("public service key"); let service_document = build_service_document(&*external_domain, &public_service_key);
let resolver = PublicResolver::default();
// Admin token used for temporary admin Basic auth // If not set, admin endpoints will not be added to the router let admin_password = env::var("ADMIN_PASSWORD").ok();
// DIDs allowed to curate the PDS directory via com.pdsmoover.pds.* admin // endpoints. If empty, those endpoints are not added to the router. // A typo here silently locks the real admin out, so anything that is not // shaped like a DID is called out (but still honored — the allowlist is // matched literally, and refusing to load it would be worse). let admin_dids: HashSet<String> = env::var("ADMIN_DIDS") .unwrap_or_default() .split(',') .map(str::trim) .filter(|did| !did.is_empty()) .inspect(|did| { if !did.starts_with("did:") { warn!(entry = %did, "ADMIN_DIDS entry does not look like a DID"); } }) .map(str::to_string) .collect(); // DIDs are public identifiers, not secrets, so logging them is fine and // makes a misconfigured allowlist obvious from the startup logs. info!( count = admin_dids.len(), dids = ?admin_dids, "loaded admin DID allowlist" );
// S3 setup for serving backups let region_name = env::var("S3_REGION")?; let endpoint = env::var("S3_ENDPOINT")?; let region = Region::Custom { region: region_name, endpoint, }; let bucket = Bucket::new( env::var("S3_BUCKET_NAME")?.as_str(), region, Credentials::new( Some(env::var("S3_ACCESS_KEY")?.as_str()), Some(env::var("S3_SECRET_KEY")?.as_str()), None, None, None, )?, )?;
// PLC directory used to validate reported migrations let plc_directory_url = env::var("PLC_DIRECTORY_URL").unwrap_or_else(|_| "https://plc.directory".to_string()); let plc_client = plc::PlcClient::new(plc_directory_url)?;
let state = AppState { db, service_document, public_resolver: Arc::new(resolver), did_web: service_did, admin_token: admin_password.clone(), admin_dids: Arc::new(admin_dids), s3_bucket: Arc::new(bucket), describe_server_cache: Arc::new(RwLock::new(None)), plc_client: Arc::new(plc_client), };
// Build Axum router let mut app = Router::new() .route("/", get(root_handler)) //XRPC Routes .merge(backup_routes(state.clone())) .merge(atproto_routes(state.clone())) .merge(pds_routes(state.clone())) //Other routes .route("/.well-known/did.json", get(handle_wellknown_did_web));
// Conditionally add admin endpoints only if ADMIN_PASSWORD is set if state.admin_token.is_some() { // Basic rate limiting for admin endpoints. // Adjust the replenish period/burst as needed. app = app.merge(with_ip_rate_limit( admin_routes(state.clone()), Duration::from_secs(60), 5, )); }
// Conditionally add PDS directory admin endpoints only if ADMIN_DIDS is set if !state.admin_dids.is_empty() { app = app.merge(pds_admin_routes(state.clone())); }
//CORS let cors = CorsLayer::new() .allow_methods(Any) .allow_origin(Any) .allow_headers(Any);
// Finalize with state let app = app.layer(cors).with_state(state);
// Read PORT from env or default to 3000 let port: u16 = std::env::var("PORT") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(3000); let addr: SocketAddr = ([0, 0, 0, 0], port).into();
info!(%addr, "starting web server");
let listener = TcpListener::bind(addr).await?; // Rate limited routes key off the client IP, which SmartIpKeyExtractor can only fall back to // when the peer address is available. Without it, any request lacking a forwarded-for header // is rejected outright. axum::serve( listener, app.into_make_service_with_connect_info::<SocketAddr>(), ) .await?;
Ok(())}
async fn root_handler() -> impl axum::response::IntoResponse { let body = r".---. .---. .--.: .; :: . :: .--': _.': :: :`. `.: : : :; : _`, ::_; :___.'`.__.'
.-..-. .--. .--.: `' :: ,. :: ,. :: .. :: :: :: :: :.-..-. .--. .--.: :; :: :; :: :; :: `; :' '_.': ..':_;:_;`.__.'`.__.'`.__.'`.__.':_;
";
let intro = "\n\nThis is a PDS MOOver xrpc service\n\nCode: https://tangled.org/@pds.dad/pds-moover\n";
let banner = format!(" {body}\n{intro}");
( [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], banner, )}