diff --git a/lib/modules/programs/docker.nix b/lib/modules/programs/docker.nix index 1a139fe7..81cd8b81 100644 --- a/lib/modules/programs/docker.nix +++ b/lib/modules/programs/docker.nix @@ -27,6 +27,7 @@ in services = { anki-sync.enable = mkEnableOption "Anki Sync server"; drone.enable = mkEnableOption "drone server"; + firefox-sync.enable = mkEnableOption "Firefox Sync server"; forgejo.enable = mkEnableOption "Forgejo server"; gotify.enable = mkEnableOption "Gotify server"; jellyfin.enable = mkEnableOption "Jellyfin media server"; diff --git a/profiles/machines/manuel/server.nix b/profiles/machines/manuel/server.nix index b362ad6d..9a7d384e 100644 --- a/profiles/machines/manuel/server.nix +++ b/profiles/machines/manuel/server.nix @@ -108,6 +108,7 @@ in { services = { anki-sync.enable = true; drone.enable = true; + firefox-sync.enable = true; forgejo.enable = true; gotify.enable = true; jellyfin.enable = true; diff --git a/scripts/Common/Software/docker/services/firefox-sync/.dockerignore b/scripts/Common/Software/docker/services/firefox-sync/.dockerignore new file mode 100644 index 00000000..ac26fc24 --- /dev/null +++ b/scripts/Common/Software/docker/services/firefox-sync/.dockerignore @@ -0,0 +1,2 @@ +* +!*.patch diff --git a/scripts/Common/Software/docker/services/firefox-sync/Dockerfile b/scripts/Common/Software/docker/services/firefox-sync/Dockerfile new file mode 100644 index 00000000..83d8800c --- /dev/null +++ b/scripts/Common/Software/docker/services/firefox-sync/Dockerfile @@ -0,0 +1,28 @@ +FROM rust AS build +ARG SYNC_STORAGE_VERSION=0.18.2 +RUN git clone https://github.com/mozilla-services/syncstorage-rs -b ${SYNC_STORAGE_VERSION} /app +WORKDIR /app +COPY ./public-url.patch . + +RUN \ + apt-get update \ + && apt-get install -y libpython3-dev \ + && git apply public-url.patch \ + && cargo install --path ./syncserver --features mysql --locked \ + && cargo install diesel_cli --no-default-features --features mysql --locked \ + && cargo clean \ + && apt-get remove -y libpython3-dev \ + && rm -rf /var/lib/apt/lists \ + && bash -O extglob -c 'rm -rf /usr/local/cargo/!(bin)' \ + && bash -O extglob -c 'rm -rf /usr/local/cargo/bin/!(diesel|syncserver)' + +FROM python:3.11 AS sync +COPY --from=build /usr/local/cargo/bin/syncserver /usr/local/bin +COPY --from=build /app/requirements.txt . +RUN pip install -r requirements.txt +CMD [ "/usr/local/bin/syncserver" ] + +FROM mariadb AS db +RUN mkdir -p /app/tokenserver-db +COPY --from=build /app/tokenserver-db/migrations /app/tokenserver-db/migrations +COPY --from=build /usr/local/cargo/bin/diesel /usr/local/bin diff --git a/scripts/Common/Software/docker/services/firefox-sync/docker-compose.base.yml b/scripts/Common/Software/docker/services/firefox-sync/docker-compose.base.yml new file mode 100644 index 00000000..8169224f --- /dev/null +++ b/scripts/Common/Software/docker/services/firefox-sync/docker-compose.base.yml @@ -0,0 +1,37 @@ +services: + sync-server: + build: + context: . + target: sync + restart: unless-stopped + environment: + RUST_LOG: warn + SYNC_HUMAN_LOGS: 1 + SYNC_HOST: "0.0.0.0" + SYNC_PORT: 80 + SYNC_SYNCSTORAGE__ENABLED: "true" + SYNC_SYNCSTORAGE__ENABLE_QUOTA: 0 + SYNC_TOKENSERVER__ENABLED: "true" + SYNC_TOKENSERVER__RUN_MIGRATIONS: "true" + SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api.accounts.firefox.com + SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL: https://oauth.accounts.firefox.com + SYNC_TOKENSERVER__FXA_BROWSERID_AUDIENCE: https://token.services.mozilla.com + SYNC_TOKENSERVER__FXA_BROWSERID_ISSUER: https://api.accounts.firefox.com + SYNC_TOKENSERVER__FXA_BROWSERID_SERVER_URL: https://verifier.accounts.firefox.com/v2 + sync-db: + image: mariadb + restart: unless-stopped + environment: + MARIADB_RANDOM_ROOT_PASSWORD: "yes" + volumes: + - ./data/sync:/var/lib/mysql + token-db: + build: + context: . + target: db + restart: unless-stopped + environment: + MARIADB_RANDOM_ROOT_PASSWORD: "yes" + volumes: + - ./data/token:/var/lib/mysql + - ./init:/docker-entrypoint-initdb.d diff --git a/scripts/Common/Software/docker/services/firefox-sync/docker-compose.overrides.yml b/scripts/Common/Software/docker/services/firefox-sync/docker-compose.overrides.yml new file mode 100644 index 00000000..7f1ad454 --- /dev/null +++ b/scripts/Common/Software/docker/services/firefox-sync/docker-compose.overrides.yml @@ -0,0 +1,21 @@ +services: + sync-server: + environment: + SYNC_PUBLIC_URL: https://example.com + SYNC_MASTER_SECRET: secret + SYNC_SYNCSTORAGE__DATABASE_URL: mysql://sync:password@sync-db/SyncStorage + SYNC_TOKENSERVER__DATABASE_URL: mysql://token:password@token-db/TokenServer + SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret + ports: + - 127.0.0.1:1337:80 + sync-db: + environment: + MARIADB_USER: sync + MARIADB_PASSWORD: password + MARIADB_DATABASE: SyncStorage + token-db: + environment: + SYNC_PUBLIC_URL: https://example.com + MARIADB_USER: token + MARIADB_PASSWORD: password + MARIADB_DATABASE: TokenServer diff --git a/scripts/Common/Software/docker/services/firefox-sync/init/z1-migrate.sh b/scripts/Common/Software/docker/services/firefox-sync/init/z1-migrate.sh new file mode 100644 index 00000000..cc78a8fd --- /dev/null +++ b/scripts/Common/Software/docker/services/firefox-sync/init/z1-migrate.sh @@ -0,0 +1,2 @@ +#!/bin/bash +diesel --database-url "mysql://${MARIADB_USER}:${MARIADB_PASSWORD}@localhost/${MARIADB_DATABASE}" migration --migration-dir /app/tokenserver-db/migrations run diff --git a/scripts/Common/Software/docker/services/firefox-sync/init/z2-add-service.sh b/scripts/Common/Software/docker/services/firefox-sync/init/z2-add-service.sh new file mode 100644 index 00000000..ad829967 --- /dev/null +++ b/scripts/Common/Software/docker/services/firefox-sync/init/z2-add-service.sh @@ -0,0 +1,10 @@ +mariadb -u$MARIADB_USER -p$MARIADB_PASSWORD -D $MARIADB_DATABASE <, + /// Keep-alive header value (seconds) + pub actix_keep_alive: Option, + /// The master secret, from which are derived +@@ -182,6 +183,7 @@ impl Default for Settings { + Settings { + port: 8000, + host: "127.0.0.1".to_string(), ++ public_url: None, + actix_keep_alive: None, + master_secret: Secrets::default(), + statsd_host: Some("localhost".to_owned()), +diff --git a/syncserver/src/server/mod.rs b/syncserver/src/server/mod.rs +index f1e0a18..d686693 100644 +--- a/syncserver/src/server/mod.rs ++++ b/syncserver/src/server/mod.rs +@@ -80,7 +80,7 @@ pub struct Server; + + #[macro_export] + macro_rules! build_app { +- ($syncstorage_state: expr, $tokenserver_state: expr, $secrets: expr, $limits: expr, $cors: expr, $metrics: expr) => { ++ ($public_url: expr, $syncstorage_state: expr, $tokenserver_state: expr, $secrets: expr, $limits: expr, $cors: expr, $metrics: expr) => { + App::new() + .configure(|cfg| { + cfg.app_data(Data::new($syncstorage_state)); +@@ -90,6 +90,7 @@ macro_rules! build_app { + cfg.app_data(Data::new(state)); + } + }) ++ .app_data(Data::new($public_url)) + .app_data(Data::new($secrets)) + // Middleware is applied LIFO + // These will wrap all outbound responses with matching status codes. +@@ -336,6 +337,7 @@ impl Server { + }; + + build_app!( ++ settings.public_url.clone(), + syncstorage_state, + tokenserver_state.clone(), + Arc::clone(&secrets), +diff --git a/syncserver/src/server/test.rs b/syncserver/src/server/test.rs +index 8690526..7efbe98 100644 +--- a/syncserver/src/server/test.rs ++++ b/syncserver/src/server/test.rs +@@ -127,6 +127,7 @@ macro_rules! init_app { + let state = get_test_state(&$settings).await; + let metrics = state.metrics.clone(); + test::init_service(build_app!( ++ $settings.public_url.clone(), + state, + None::, + Arc::clone(&SECRETS), +@@ -248,6 +249,7 @@ where + let state = get_test_state(&settings).await; + let metrics = state.metrics.clone(); + let app = test::init_service(build_app!( ++ settings.public_url.clone(), + state, + None::, + Arc::clone(&SECRETS), +@@ -292,6 +294,7 @@ async fn test_endpoint_with_body( + let state = get_test_state(&settings).await; + let metrics = state.metrics.clone(); + let app = test::init_service(build_app!( ++ settings.public_url.clone(), + state, + None::, + Arc::clone(&SECRETS), +diff --git a/syncserver/src/web/auth.rs b/syncserver/src/web/auth.rs +index 9153d38..ca1d123 100644 +--- a/syncserver/src/web/auth.rs ++++ b/syncserver/src/web/auth.rs +@@ -170,6 +170,7 @@ impl HawkPayload { + pub fn extrude( + header: &str, + method: &str, ++ public_url: &Option, + secrets: &Secrets, + ci: &ConnectionInfo, + uri: &Uri, +@@ -190,6 +191,15 @@ impl HawkPayload { + } else { + 80 + }; ++ ++ let prefix = match &public_url { ++ None => "".to_owned(), ++ Some(url) => match url.parse::() { ++ Err(_) => "".to_owned(), ++ Ok(uri) => uri.path().to_owned() ++ } ++ }; ++ + let path = uri.path_and_query().ok_or(HawkErrorKind::MissingPath)?; + let expiry = if path.path().ends_with("/info/collections") { + 0 +@@ -197,7 +207,7 @@ impl HawkPayload { + Utc::now().timestamp() as u64 + }; + +- HawkPayload::new(header, method, path.as_str(), host, port, secrets, expiry) ++ HawkPayload::new(header, method, (prefix + path.as_str()).as_str(), host, port, secrets, expiry) + } + } + +diff --git a/syncserver/src/web/extractors.rs b/syncserver/src/web/extractors.rs +index 4bd617b..b9aee1c 100644 +--- a/syncserver/src/web/extractors.rs ++++ b/syncserver/src/web/extractors.rs +@@ -1056,6 +1056,7 @@ impl HawkIdentifier { + method: &str, + uri: &Uri, + ci: &ConnectionInfo, ++ public_url: &Option, + secrets: &Secrets, + ) -> Result + where +@@ -1072,6 +1073,7 @@ impl HawkIdentifier { + .to_str() + .map_err(|e| -> ApiError { HawkErrorKind::Header(e).into() })?; + let identifier = Self::generate( ++ public_url, + secrets, + method, + auth_header, +@@ -1084,6 +1086,7 @@ impl HawkIdentifier { + } + + pub fn generate( ++ public_url: &Option, + secrets: &Secrets, + method: &str, + header: &str, +@@ -1091,7 +1094,7 @@ impl HawkIdentifier { + uri: &Uri, + exts: &mut Extensions, + ) -> Result { +- let payload = HawkPayload::extrude(header, method, secrets, connection_info, uri)?; ++ let payload = HawkPayload::extrude(header, method, &public_url, secrets, connection_info, uri)?; + let puid = Self::uid_from_path(uri)?; + if payload.user_id != puid { + warn!("⚠️ Hawk UID not in URI: {:?} {:?}", payload.user_id, uri); +@@ -1145,6 +1148,12 @@ impl FromRequest for HawkIdentifier { + // NOTE: `connection_info()` will get a mutable reference lock on `extensions()` + let connection_info = req.connection_info().clone(); + let method = req.method().clone(); ++ ++ let public_url: &Option = match &req.app_data::>>() { ++ Some(data) => data, ++ None => &None ++ }; ++ + // Tried collapsing this to a `.or_else` and hit problems with the return resolving + // to an appropriate error state. Can't use `?` since the function does not return a result. + let secrets = match req.app_data::>>() { +@@ -1155,7 +1164,7 @@ impl FromRequest for HawkIdentifier { + } + }; + +- let result = Self::extrude(&req, method.as_str(), uri, &connection_info, secrets); ++ let result = Self::extrude(&req, method.as_str(), uri, &connection_info, &public_url, secrets); + + if let Ok(ref hawk_id) = result { + // Store the origin of the token as an extra to be included when emitting a Sentry error