i18n+filtering fork - fluent-templates v2

chore: cleanup and last minute bug fixes before prod push

+3 -1
.gitignore
··· 42 42 CLAUDE.local.md 43 43 Cargo.lock 44 44 /keys.json 45 - /.vscode/launch.json 45 + /.vscode/launch.json 46 + .*.env 47 + .env
+4 -4
Cargo.toml
··· 2 2 name = "smokesignal" 3 3 version = "0.1.0" 4 4 edition = "2021" 5 - rust-version = "1.86" 5 + rust-version = "1.83" 6 6 authors = ["Nick Gerakines <nick.gerakines@gmail.com>"] 7 7 description = "An event and RSVP management application." 8 8 readme = "README.md" ··· 61 61 cookie = "0.18" 62 62 ammonia = "4" 63 63 rust-embed = "8.5" 64 - sqlx = { version = "0.8", default-features = false, features = ["derive", "macros", "migrate", "json", "runtime-tokio", "postgres", "chrono", "tls-rustls"] } 64 + sqlx = { version = "0.8", default-features = false, features = ["derive", "macros", "migrate", "json", "runtime-tokio", "postgres", "chrono", "tls-rustls-ring-native-roots"] } 65 65 elliptic-curve = { version = "0.13.8", features = ["pem", "pkcs8", "sec1", "std", "alloc", "digest", "ecdh", "jwk", "bits"] } 66 66 p256 = { version = "0.13.2", features = ["ecdsa-core", "jwk", "serde", "ecdh"] } 67 67 ordermap = "0.5" ··· 73 73 fluent-bundle = "0.15" 74 74 fluent-syntax = "0.11" 75 75 sha2 = "0.10.8" 76 - redis = { version = "0.28", features = ["connection-manager", "r2d2", "tokio-comp"] } 76 + redis = { version = "0.28", features = ["tokio-comp", "tokio-rustls-comp"] } 77 77 itertools = "0.14.0" 78 78 deadpool = "0.12.2" 79 - deadpool-redis = "0.20.0" 79 + deadpool-redis = {version = "0.20.0", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp"] } 80 80 crockford = "1.2.1" 81 81 tokio-websockets = { version = "0.11.3", features = ["client", "rand", "ring", "rustls-native-roots"] } 82 82 zstd = "0.13.3"
+14 -14
Dockerfile
··· 1 1 # syntax=docker/dockerfile:1.4 2 - FROM rust:1-bookworm AS build 2 + FROM rust:latest AS build 3 3 4 4 RUN cargo install sqlx-cli@0.8.2 --no-default-features --features postgres 5 5 RUN cargo install sccache --version ^0.8 ··· 25 25 cargo build --locked --release --bin smokesignal --target-dir . --no-default-features -F embed 26 26 EOF 27 27 28 - FROM debian:bookworm-slim 28 + RUN groupadd -g 1500 -r smokesignal && useradd -u 1501 -r -g smokesignal -d /var/lib/smokesignal -m smokesignal 29 + RUN chown -R smokesignal:smokesignal /app/release/smokesignal 30 + 31 + FROM gcr.io/distroless/cc 29 32 30 33 LABEL org.opencontainers.image.title="Smoke Signal" 31 34 LABEL org.opencontainers.image.description="An event and RSVP management application." ··· 33 36 LABEL org.opencontainers.image.authors="Nick Gerakines <nick.gerakines@gmail.com>" 34 37 LABEL org.opencontainers.image.source="https://tangled.sh/@smokesignal.events/smokesignal" 35 38 36 - RUN set -x \ 37 - && apt-get update \ 38 - && apt-get install ca-certificates -y 39 - 40 - RUN groupadd -g 1500 -r smokesignal && useradd -u 1501 -r -g smokesignal -d /var/lib/smokesignal -m smokesignal 41 - 42 - ENV RUST_LOG=info 43 - ENV RUST_BACKTRACE=full 39 + WORKDIR /var/lib/smokesignal 40 + USER smokesignal:smokesignal 44 41 42 + COPY --from=build /etc/passwd /etc/passwd 43 + COPY --from=build /etc/group /etc/group 45 44 COPY --from=build /app/release/smokesignal /var/lib/smokesignal/ 45 + COPY static /var/lib/smokesignal/static 46 46 47 - RUN chown -R smokesignal:smokesignal /var/lib/smokesignal 47 + ENV HTTP_STATIC_PATH=/var/lib/smokesignal/static 48 48 49 - WORKDIR /var/lib/smokesignal 49 + ENV RUST_LOG=info 50 + ENV RUST_BACKTRACE=full 50 51 51 - USER smokesignal 52 - ENTRYPOINT ["sh", "-c", "/var/lib/smokesignal/smokesignal"] 52 + ENTRYPOINT ["/var/lib/smokesignal/smokesignal"]
+9 -22
src/atproto/uri.rs
··· 1 1 use anyhow::Result; 2 2 3 - use crate::atproto::errors::UriError; 3 + use crate::{atproto::errors::UriError, validation::is_valid_hostname}; 4 4 5 5 // Constants for maximum lengths 6 6 const MAX_REPOSITORY_LENGTH: usize = 253; // DNS name length limit ··· 21 21 // Check for invalid characters 22 22 if !repository 23 23 .chars() 24 - .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.') 24 + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == ':') 25 25 { 26 26 return false; 27 27 } 28 28 29 - // Check for consecutive periods 30 - if repository.contains("..") { 31 - return false; 29 + // TODO: If starts with "did:plc:" then validate encoded string and length 30 + if repository.starts_with("did:plc:") { 31 + return true; 32 32 } 33 33 34 - // Check for path traversal attempts 35 - if repository.contains("../") || repository == ".." { 36 - return false; 34 + // TODO: If starts with "did:web:" then validate hostname and parts 35 + if repository.starts_with("did:web:") { 36 + return true; 37 37 } 38 38 39 - // Check start/end characters 40 - let starts_with_valid = repository 41 - .chars() 42 - .next() 43 - .map(|c| c.is_ascii_alphanumeric()) 44 - .unwrap_or(false); 45 - 46 - let ends_with_valid = repository 47 - .chars() 48 - .last() 49 - .map(|c| c.is_ascii_alphanumeric()) 50 - .unwrap_or(false); 51 - 52 - starts_with_valid && ends_with_valid 39 + is_valid_hostname(repository) 53 40 } 54 41 55 42 /// Validates a collection name for AT Protocol URIs
+18 -7
src/config.rs
··· 6 6 use rand::seq::SliceRandom; 7 7 8 8 use crate::config_errors::ConfigError; 9 + use crate::encoding_errors::EncodingError; 9 10 use crate::jose::jwk::WrappedJsonWebKeySet; 10 11 11 12 #[derive(Clone)] ··· 34 35 pub version: String, 35 36 pub http_port: HttpPort, 36 37 pub http_cookie_key: HttpCookieKey, 38 + pub http_static_path: String, 37 39 pub external_base: String, 38 40 pub certificate_bundles: CertificateBundles, 39 41 pub user_agent: String, ··· 54 56 let http_cookie_key: HttpCookieKey = 55 57 require_env("HTTP_COOKIE_KEY").and_then(|value| value.try_into())?; 56 58 59 + let http_static_path = default_env("HTTP_STATIC_PATH", "static").try_into()?; 60 + 57 61 let external_base = require_env("EXTERNAL_BASE")?; 58 62 59 63 let certificate_bundles: CertificateBundles = ··· 91 95 Ok(Self { 92 96 version: version()?, 93 97 http_port, 98 + http_static_path, 94 99 external_base, 95 100 certificate_bundles, 96 101 user_agent, ··· 220 225 impl TryFrom<String> for SigningKeys { 221 226 type Error = anyhow::Error; 222 227 fn try_from(value: String) -> Result<Self, Self::Error> { 223 - // Verify file exists before reading 224 - if !std::path::Path::new(&value).exists() { 225 - return Err(ConfigError::SigningKeysFileNotFound(value).into()); 226 - } 227 - 228 - // Read file with proper error handling 229 - let content = std::fs::read(&value).map_err(ConfigError::ReadSigningKeysFailed)?; 228 + let content = { 229 + if value.starts_with("/") { 230 + // Verify file exists before reading 231 + if !std::path::Path::new(&value).exists() { 232 + return Err(ConfigError::SigningKeysFileNotFound(value).into()); 233 + } 234 + std::fs::read(&value).map_err(ConfigError::ReadSigningKeysFailed)? 235 + } else { 236 + general_purpose::STANDARD 237 + .decode(&value) 238 + .map_err(EncodingError::Base64DecodingFailed)? 239 + } 240 + }; 230 241 231 242 // Validate content is not empty 232 243 if content.is_empty() {
+10 -1
src/http/handle_index.rs
··· 87 87 .iter() 88 88 .filter_map(|event_view| { 89 89 let organizer_maybe = organizer_handlers.get(&event_view.event.did); 90 - EventView::try_from((auth.0.as_ref(), organizer_maybe, &event_view.event)).ok() 90 + let event_view = 91 + EventView::try_from((auth.0.as_ref(), organizer_maybe, &event_view.event)); 92 + 93 + match event_view { 94 + Ok(event_view) => Some(event_view), 95 + Err(err) => { 96 + tracing::warn!(err = ?err, "error converting event view"); 97 + None 98 + } 99 + } 91 100 }) 92 101 .collect::<Vec<EventView>>(); 93 102
+1 -1
src/http/server.rs
··· 56 56 }; 57 57 58 58 pub fn build_router(web_context: WebContext) -> Router { 59 - let serve_dir = ServeDir::new("static"); 59 + let serve_dir = ServeDir::new(web_context.config.http_static_path.clone()); 60 60 61 61 Router::new() 62 62 .route("/", get(handle_index))