+3
-1
.gitignore
+3
-1
.gitignore
+4
-4
Cargo.toml
+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
+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
+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
+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
+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