+1
-1
Cargo.lock
+1
-1
Cargo.lock
+3
-3
Cargo.toml
+3
-3
Cargo.toml
···
1
1
[package]
2
2
name = "pds_gatekeeper"
3
-
version = "0.1.0"
3
+
version = "0.1.2"
4
4
edition = "2024"
5
5
license = "MIT"
6
6
···
15
15
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
16
16
hyper-util = { version = "0.1.16", features = ["client", "client-legacy"] }
17
17
tower-http = { version = "0.6", features = ["cors", "compression-zstd"] }
18
-
tower_governor = "0.8.0"
18
+
tower_governor = { version = "0.8.0", features = ["axum", "tracing"] }
19
19
hex = "0.4"
20
20
jwt-compact = { version = "0.8.0", features = ["es256k"] }
21
21
scrypt = "0.11"
22
-
#Leaveing these two cause I think it is needed by the
22
+
#Leaveing these two cause I think it is needed by the email crate for ssl
23
23
aws-lc-rs = "1.13.0"
24
24
rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] }
25
25
lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] }
+58
-2
README.md
+58
-2
README.md
···
37
37
```yml
38
38
gatekeeper:
39
39
container_name: gatekeeper
40
-
image: fatfingers23/pds_gatekeeper:arm-latest
40
+
image: fatfingers23/pds_gatekeeper:latest
41
41
network_mode: host
42
42
restart: unless-stopped
43
43
#This gives the container to the access to the PDS folder. Source is the location on your server of that directory
···
49
49
- pds
50
50
```
51
51
52
+
For Coolify, if you're using Traefik as your proxy you'll need to make sure the labels for the container are set up correctly. A full example can be found at [./examples/coolify-compose.yml](./examples/coolify-compose.yml).
53
+
54
+
```yml
55
+
gatekeeper:
56
+
container_name: gatekeeper
57
+
image: 'fatfingers23/pds_gatekeeper:latest'
58
+
restart: unless-stopped
59
+
volumes:
60
+
- '/pds:/pds'
61
+
environment:
62
+
- 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}'
63
+
- 'PDS_BASE_URL=http://pds:3000'
64
+
- GATEKEEPER_HOST=0.0.0.0
65
+
depends_on:
66
+
- pds
67
+
healthcheck:
68
+
test:
69
+
- CMD
70
+
- timeout
71
+
- '1'
72
+
- bash
73
+
- '-c'
74
+
- 'cat < /dev/null > /dev/tcp/0.0.0.0/8080'
75
+
interval: 10s
76
+
timeout: 5s
77
+
retries: 3
78
+
start_period: 10s
79
+
labels:
80
+
- traefik.enable=true
81
+
- 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`))'
82
+
- traefik.http.routers.pds-gatekeeper.entrypoints=https
83
+
- traefik.http.routers.pds-gatekeeper.tls=true
84
+
- traefik.http.routers.pds-gatekeeper.priority=100
85
+
- traefik.http.routers.pds-gatekeeper.middlewares=gatekeeper-cors
86
+
- traefik.http.services.pds-gatekeeper.loadbalancer.server.port=8080
87
+
- traefik.http.services.pds-gatekeeper.loadbalancer.server.scheme=http
88
+
- 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS,PATCH'
89
+
- 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowheaders=*'
90
+
- 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolalloworiginlist=*'
91
+
- traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolmaxage=100
92
+
- traefik.http.middlewares.gatekeeper-cors.headers.addvaryheader=true
93
+
- traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowcredentials=true
94
+
```
95
+
52
96
## Caddy setup
53
97
54
98
For the reverse proxy I use caddy. This part is what overwrites the endpoints and proxies them to PDS gatekeeper to add
···
60
104
path /xrpc/com.atproto.server.getSession
61
105
path /xrpc/com.atproto.server.updateEmail
62
106
path /xrpc/com.atproto.server.createSession
107
+
path /xrpc/com.atproto.server.createAccount
63
108
path /@atproto/oauth-provider/~api/sign-in
64
109
}
65
110
···
79
124
path /xrpc/com.atproto.server.getSession
80
125
path /xrpc/com.atproto.server.updateEmail
81
126
path /xrpc/com.atproto.server.createSession
127
+
path /xrpc/com.atproto.server.createAccount
82
128
path /@atproto/oauth-provider/~api/sign-in
83
129
}
84
130
85
131
handle @gatekeeper {
86
-
reverse_proxy http://localhost:8080
132
+
reverse_proxy http://localhost:8080 {
133
+
#Makes sure the cloudflare ip is proxied and able to be picked up by pds gatekeeper
134
+
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
135
+
}
87
136
}
88
137
89
138
reverse_proxy http://localhost:3000
···
113
162
`GATEKEEPER_HOST` - Host for pds gatekeeper. Defaults to `127.0.0.1`
114
163
115
164
`GATEKEEPER_PORT` - Port for pds gatekeeper. Defaults to `8080`
165
+
166
+
`GATEKEEPER_CREATE_ACCOUNT_PER_SECOND` - Sets how often it takes a count off the limiter. example if you hit the rate
167
+
limit of 5 and set to 60, then in 60 seconds you will be able to make one more. Or in 5 minutes be able to make 5 more.
168
+
169
+
`GATEKEEPER_CREATE_ACCOUNT_BURST` - Sets how many requests can be made in a burst. In the prior example this is where
170
+
the 5 comes from. Example can set this to 10 to allow for 10 requests in a burst, and after 60 seconds it will drop one
171
+
off.
+1
-1
examples/compose.yml
+1
-1
examples/compose.yml
···
39
39
WATCHTOWER_SCHEDULE: "@midnight"
40
40
gatekeeper:
41
41
container_name: gatekeeper
42
-
image: fatfingers23/pds_gatekeeper:arm-latest
42
+
image: fatfingers23/pds_gatekeeper:latest
43
43
network_mode: host
44
44
restart: unless-stopped
45
45
#This gives the container to the access to the PDS folder. Source is the location on your server of that directory
+73
examples/coolify-compose.yml
+73
examples/coolify-compose.yml
···
1
+
services:
2
+
pds:
3
+
image: 'ghcr.io/bluesky-social/pds:0.4.182'
4
+
volumes:
5
+
- '/pds:/pds'
6
+
environment:
7
+
- SERVICE_URL_PDS_3000
8
+
- 'PDS_HOSTNAME=${SERVICE_FQDN_PDS_3000}'
9
+
- 'PDS_JWT_SECRET=${SERVICE_HEX_32_JWTSECRET}'
10
+
- 'PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}'
11
+
- 'PDS_ADMIN_EMAIL=${PDS_ADMIN_EMAIL}'
12
+
- 'PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${SERVICE_HEX_32_ROTATIONKEY}'
13
+
- 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}'
14
+
- 'PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATA_DIRECTORY:-/pds}/blocks'
15
+
- 'PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-104857600}'
16
+
- 'PDS_DID_PLC_URL=${PDS_DID_PLC_URL:-https://plc.directory}'
17
+
- 'PDS_EMAIL_FROM_ADDRESS=${PDS_EMAIL_FROM_ADDRESS}'
18
+
- 'PDS_EMAIL_SMTP_URL=${PDS_EMAIL_SMTP_URL}'
19
+
- 'PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL:-https://api.bsky.app}'
20
+
- 'PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID:-did:web:api.bsky.app}'
21
+
- 'PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL:-https://mod.bsky.app/xrpc/com.atproto.moderation.createReport}'
22
+
- 'PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID:-did:plc:ar7c4by46qjdydhdevvrndac}'
23
+
- 'PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network}'
24
+
- 'LOG_ENABLED=${LOG_ENABLED:-true}'
25
+
command: "sh -c '\n set -euo pipefail\n echo \"Installing required packages and pdsadmin...\"\n apk add --no-cache openssl curl bash jq coreutils gnupg util-linux-misc >/dev/null\n curl -o /usr/local/bin/pdsadmin.sh https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh\n chmod 700 /usr/local/bin/pdsadmin.sh\n ln -sf /usr/local/bin/pdsadmin.sh /usr/local/bin/pdsadmin\n echo \"Creating an empty pds.env file so pdsadmin works...\"\n touch ${PDS_DATA_DIRECTORY}/pds.env\n echo \"Launching PDS, enjoy!...\"\n exec node --enable-source-maps index.js\n'\n"
26
+
healthcheck:
27
+
test:
28
+
- CMD
29
+
- wget
30
+
- '--spider'
31
+
- 'http://127.0.0.1:3000/xrpc/_health'
32
+
interval: 5s
33
+
timeout: 10s
34
+
retries: 10
35
+
gatekeeper:
36
+
container_name: gatekeeper
37
+
image: 'fatfingers23/pds_gatekeeper:latest'
38
+
restart: unless-stopped
39
+
volumes:
40
+
- '/pds:/pds'
41
+
environment:
42
+
- 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}'
43
+
- 'PDS_BASE_URL=http://pds:3000'
44
+
- GATEKEEPER_HOST=0.0.0.0
45
+
depends_on:
46
+
- pds
47
+
healthcheck:
48
+
test:
49
+
- CMD
50
+
- timeout
51
+
- '1'
52
+
- bash
53
+
- '-c'
54
+
- 'cat < /dev/null > /dev/tcp/0.0.0.0/8080'
55
+
interval: 10s
56
+
timeout: 5s
57
+
retries: 3
58
+
start_period: 10s
59
+
labels:
60
+
- traefik.enable=true
61
+
- 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`))'
62
+
- traefik.http.routers.pds-gatekeeper.entrypoints=https
63
+
- traefik.http.routers.pds-gatekeeper.tls=true
64
+
- traefik.http.routers.pds-gatekeeper.priority=100
65
+
- traefik.http.routers.pds-gatekeeper.middlewares=gatekeeper-cors
66
+
- traefik.http.services.pds-gatekeeper.loadbalancer.server.port=8080
67
+
- traefik.http.services.pds-gatekeeper.loadbalancer.server.scheme=http
68
+
- 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS,PATCH'
69
+
- 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowheaders=*'
70
+
- 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolalloworiginlist=*'
71
+
- traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolmaxage=100
72
+
- traefik.http.middlewares.gatekeeper-cors.headers.addvaryheader=true
73
+
- traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowcredentials=true
+1
-1
justfile
+1
-1
justfile
+10
-3
src/main.rs
+10
-3
src/main.rs
···
19
19
use std::time::Duration;
20
20
use std::{env, net::SocketAddr};
21
21
use tower_governor::GovernorLayer;
22
-
use tower_governor::governor::{GovernorConfig, GovernorConfigBuilder};
22
+
use tower_governor::governor::GovernorConfigBuilder;
23
+
use tower_governor::key_extractor::SmartIpKeyExtractor;
23
24
use tower_http::compression::CompressionLayer;
24
25
use tower_http::cors::{Any, CorsLayer};
25
26
use tracing::log;
···
97
98
"Error loading pds.env file (ignore if you loaded your variables in the environment somehow else): {e}"
98
99
);
99
100
}
100
-
let pds_root = env::var("PDS_DATA_DIRECTORY")?;
101
+
102
+
let pds_root =
103
+
env::var("PDS_DATA_DIRECTORY").expect("PDS_DATA_DIRECTORY is not set in your pds.env file");
101
104
let account_db_url = format!("{pds_root}/account.sqlite");
102
105
103
106
let account_options = SqliteConnectOptions::new()
···
170
173
let create_session_governor_conf = GovernorConfigBuilder::default()
171
174
.per_second(60)
172
175
.burst_size(5)
176
+
.key_extractor(SmartIpKeyExtractor)
173
177
.finish()
174
178
.expect("failed to create governor config for create session. this should not happen and is a bug");
175
179
···
177
181
let sign_in_governor_conf = GovernorConfigBuilder::default()
178
182
.per_second(60)
179
183
.burst_size(5)
184
+
.key_extractor(SmartIpKeyExtractor)
180
185
.finish()
181
186
.expect(
182
187
"failed to create governor config for sign in. this should not happen and is a bug",
···
205
210
create_account_governor_conf.burst_size(burst);
206
211
}
207
212
208
-
let create_account_governor_conf = create_account_governor_conf.finish().expect(
213
+
let create_account_governor_conf = create_account_governor_conf
214
+
.key_extractor(SmartIpKeyExtractor)
215
+
.finish().expect(
209
216
"failed to create governor config for create account. this should not happen and is a bug",
210
217
);
211
218
-2
src/middleware.rs
-2
src/middleware.rs
···
1
1
use crate::helpers::json_error_response;
2
2
use axum::extract::Request;
3
-
use axum::http::header::AUTHORIZATION;
4
3
use axum::http::{HeaderMap, StatusCode};
5
4
use axum::middleware::Next;
6
5
use axum::response::IntoResponse;
···
73
72
.expect("Error creating an error response");
74
73
}
75
74
let token = token.expect("Already checked for error,");
76
-
// Not going to worry about expiration since it still goes to the PDS
77
75
req.extensions_mut()
78
76
.insert(Did(Some(token.claims().custom.sub.clone())));
79
77
}
+2
-1
src/oauth_provider.rs
+2
-1
src/oauth_provider.rs
···
13
13
pub struct SignInRequest {
14
14
pub username: String,
15
15
pub password: String,
16
-
pub remember: bool,
16
+
#[serde(skip_serializing_if = "Option::is_none")]
17
+
pub remember: Option<bool>,
17
18
pub locale: String,
18
19
#[serde(skip_serializing_if = "Option::is_none", rename = "emailOtp")]
19
20
pub email_otp: Option<String>,
+29
-11
src/xrpc/com_atproto_server.rs
+29
-11
src/xrpc/com_atproto_server.rs
···
155
155
// Email update asked for
156
156
if email_auth_update {
157
157
let email = payload.email.clone();
158
-
let email_confirmed = sqlx::query_as::<_, (String,)>(
158
+
let email_confirmed = match sqlx::query_as::<_, (String,)>(
159
159
"SELECT did FROM account WHERE emailConfirmedAt IS NOT NULL AND email = ?",
160
160
)
161
161
.bind(&email)
162
162
.fetch_optional(&state.account_pool)
163
163
.await
164
-
.map_err(|_| StatusCode::BAD_REQUEST)?;
164
+
{
165
+
Ok(row) => row,
166
+
Err(err) => {
167
+
log::error!("Error checking if email is confirmed: {err}");
168
+
return Err(StatusCode::BAD_REQUEST);
169
+
}
170
+
};
165
171
166
172
//Since the email is already confirmed we can enable 2fa
167
173
return match email_confirmed {
···
184
190
if !email_auth_update && !email_auth_not_set {
185
191
//User wants auth turned off and has a token
186
192
if let Some(token) = &payload.token {
187
-
let token_found = sqlx::query_as::<_, (String,)>(
193
+
let token_found = match sqlx::query_as::<_, (String,)>(
188
194
"SELECT token FROM email_token WHERE token = ? AND did = ? AND purpose = 'update_email'",
189
195
)
190
196
.bind(token)
191
197
.bind(&did.0)
192
198
.fetch_optional(&state.account_pool)
193
-
.await
194
-
.map_err(|_| StatusCode::BAD_REQUEST)?;
199
+
.await{
200
+
Ok(token) => token,
201
+
Err(err) => {
202
+
log::error!("Error checking if token is valid: {err}");
203
+
return Err(StatusCode::BAD_REQUEST);
204
+
}
205
+
};
195
206
196
207
return if token_found.is_some() {
197
-
let _ = sqlx::query(
208
+
//TODO I think there may be a bug here and need to do some retry logic
209
+
// First try was erroring, seconds was allowing
210
+
match sqlx::query(
198
211
"INSERT INTO two_factor_accounts (did, required) VALUES (?, 0) ON CONFLICT(did) DO UPDATE SET required = 0",
199
212
)
200
213
.bind(&did.0)
201
214
.execute(&state.pds_gatekeeper_pool)
202
-
.await
203
-
.map_err(|_| StatusCode::BAD_REQUEST)?;
215
+
.await {
216
+
Ok(_) => {}
217
+
Err(err) => {
218
+
log::error!("Error updating email auth: {err}");
219
+
return Err(StatusCode::BAD_REQUEST);
220
+
}
221
+
}
204
222
205
223
Ok(StatusCode::OK.into_response())
206
224
} else {
···
269
287
State(state): State<AppState>,
270
288
mut req: Request,
271
289
) -> Result<Response<Body>, StatusCode> {
290
+
//TODO if I add the block of only accounts authenticated just take the body as json here and grab the lxm token. No middle ware is needed
291
+
272
292
let uri = format!(
273
293
"{}{}",
274
294
state.pds_base_url, "/xrpc/com.atproto.server.createAccount"
275
295
);
276
296
277
297
// Rewrite the URI to point at the upstream PDS; keep headers, method, and body intact
278
-
*req.uri_mut() = uri
279
-
.parse()
280
-
.map_err(|_| StatusCode::BAD_REQUEST)?;
298
+
*req.uri_mut() = uri.parse().map_err(|_| StatusCode::BAD_REQUEST)?;
281
299
282
300
let proxied = state
283
301
.reverse_proxy_client