Microservice to bring 2FA to self hosted PDSes

Compare changes

Choose any two refs to compare.

+4
.dockerignore
··· 1 + target 2 + /target 3 + **/.idea 4 + .idea
+184 -152
Cargo.lock
··· 112 112 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 113 113 114 114 [[package]] 115 + name = "aws-lc-rs" 116 + version = "1.13.3" 117 + source = "registry+https://github.com/rust-lang/crates.io-index" 118 + checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" 119 + dependencies = [ 120 + "aws-lc-sys", 121 + "untrusted 0.7.1", 122 + "zeroize", 123 + ] 124 + 125 + [[package]] 126 + name = "aws-lc-sys" 127 + version = "0.30.0" 128 + source = "registry+https://github.com/rust-lang/crates.io-index" 129 + checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" 130 + dependencies = [ 131 + "bindgen", 132 + "cc", 133 + "cmake", 134 + "dunce", 135 + "fs_extra", 136 + ] 137 + 138 + [[package]] 115 139 name = "axum" 116 140 version = "0.8.4" 117 141 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 217 241 checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 218 242 219 243 [[package]] 244 + name = "bindgen" 245 + version = "0.69.5" 246 + source = "registry+https://github.com/rust-lang/crates.io-index" 247 + checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" 248 + dependencies = [ 249 + "bitflags", 250 + "cexpr", 251 + "clang-sys", 252 + "itertools", 253 + "lazy_static", 254 + "lazycell", 255 + "log", 256 + "prettyplease", 257 + "proc-macro2", 258 + "quote", 259 + "regex", 260 + "rustc-hash", 261 + "shlex", 262 + "syn", 263 + "which", 264 + ] 265 + 266 + [[package]] 220 267 name = "bitflags" 221 268 version = "2.9.1" 222 269 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 274 321 ] 275 322 276 323 [[package]] 324 + name = "cexpr" 325 + version = "0.6.0" 326 + source = "registry+https://github.com/rust-lang/crates.io-index" 327 + checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 328 + dependencies = [ 329 + "nom 7.1.3", 330 + ] 331 + 332 + [[package]] 277 333 name = "cfg-if" 278 334 version = "1.0.1" 279 335 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 341 397 ] 342 398 343 399 [[package]] 400 + name = "clang-sys" 401 + version = "1.8.1" 402 + source = "registry+https://github.com/rust-lang/crates.io-index" 403 + checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 404 + dependencies = [ 405 + "glob", 406 + "libc", 407 + "libloading", 408 + ] 409 + 410 + [[package]] 411 + name = "cmake" 412 + version = "0.1.54" 413 + source = "registry+https://github.com/rust-lang/crates.io-index" 414 + checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 415 + dependencies = [ 416 + "cc", 417 + ] 418 + 419 + [[package]] 344 420 name = "concurrent-queue" 345 421 version = "2.5.0" 346 422 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 356 432 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 357 433 358 434 [[package]] 359 - name = "core-foundation" 360 - version = "0.9.4" 361 - source = "registry+https://github.com/rust-lang/crates.io-index" 362 - checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 363 - dependencies = [ 364 - "core-foundation-sys", 365 - "libc", 366 - ] 367 - 368 - [[package]] 369 435 name = "core-foundation-sys" 370 436 version = "0.8.7" 371 437 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 547 613 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 548 614 549 615 [[package]] 616 + name = "dunce" 617 + version = "1.0.5" 618 + source = "registry+https://github.com/rust-lang/crates.io-index" 619 + checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 620 + 621 + [[package]] 550 622 name = "either" 551 623 version = "1.15.0" 552 624 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 639 711 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 640 712 641 713 [[package]] 642 - name = "foreign-types" 643 - version = "0.3.2" 644 - source = "registry+https://github.com/rust-lang/crates.io-index" 645 - checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 646 - dependencies = [ 647 - "foreign-types-shared", 648 - ] 649 - 650 - [[package]] 651 - name = "foreign-types-shared" 652 - version = "0.1.1" 653 - source = "registry+https://github.com/rust-lang/crates.io-index" 654 - checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 655 - 656 - [[package]] 657 714 name = "form_urlencoded" 658 715 version = "1.2.1" 659 716 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 671 728 "nonempty", 672 729 "thiserror 1.0.69", 673 730 ] 731 + 732 + [[package]] 733 + name = "fs_extra" 734 + version = "1.3.0" 735 + source = "registry+https://github.com/rust-lang/crates.io-index" 736 + checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 674 737 675 738 [[package]] 676 739 name = "futures-channel" ··· 790 853 version = "0.31.1" 791 854 source = "registry+https://github.com/rust-lang/crates.io-index" 792 855 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 856 + 857 + [[package]] 858 + name = "glob" 859 + version = "0.3.3" 860 + source = "registry+https://github.com/rust-lang/crates.io-index" 861 + checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 793 862 794 863 [[package]] 795 864 name = "globset" ··· 943 1012 ] 944 1013 945 1014 [[package]] 946 - name = "hostname" 947 - version = "0.4.1" 948 - source = "registry+https://github.com/rust-lang/crates.io-index" 949 - checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" 950 - dependencies = [ 951 - "cfg-if", 952 - "libc", 953 - "windows-link", 954 - ] 955 - 956 - [[package]] 957 1015 name = "http" 958 1016 version = "1.3.1" 959 1017 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1222 1280 ] 1223 1281 1224 1282 [[package]] 1283 + name = "itertools" 1284 + version = "0.12.1" 1285 + source = "registry+https://github.com/rust-lang/crates.io-index" 1286 + checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 1287 + dependencies = [ 1288 + "either", 1289 + ] 1290 + 1291 + [[package]] 1225 1292 name = "itoa" 1226 1293 version = "1.0.15" 1227 1294 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1279 1346 ] 1280 1347 1281 1348 [[package]] 1349 + name = "lazycell" 1350 + version = "1.3.0" 1351 + source = "registry+https://github.com/rust-lang/crates.io-index" 1352 + checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 1353 + 1354 + [[package]] 1282 1355 name = "lettre" 1283 1356 version = "0.11.18" 1284 1357 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1292 1365 "fastrand", 1293 1366 "futures-io", 1294 1367 "futures-util", 1295 - "hostname", 1296 1368 "httpdate", 1297 1369 "idna", 1298 1370 "mime", 1299 - "native-tls", 1300 - "nom", 1371 + "nom 8.0.0", 1301 1372 "percent-encoding", 1302 1373 "quoted_printable", 1374 + "rustls", 1303 1375 "socket2", 1304 1376 "tokio", 1305 - "tokio-native-tls", 1377 + "tokio-rustls", 1306 1378 "url", 1379 + "webpki-roots 1.0.2", 1307 1380 ] 1308 1381 1309 1382 [[package]] ··· 1311 1384 version = "0.2.175" 1312 1385 source = "registry+https://github.com/rust-lang/crates.io-index" 1313 1386 checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 1387 + 1388 + [[package]] 1389 + name = "libloading" 1390 + version = "0.8.8" 1391 + source = "registry+https://github.com/rust-lang/crates.io-index" 1392 + checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" 1393 + dependencies = [ 1394 + "cfg-if", 1395 + "windows-targets 0.52.6", 1396 + ] 1314 1397 1315 1398 [[package]] 1316 1399 name = "libm" ··· 1342 1425 1343 1426 [[package]] 1344 1427 name = "linux-raw-sys" 1345 - version = "0.9.4" 1428 + version = "0.4.15" 1346 1429 source = "registry+https://github.com/rust-lang/crates.io-index" 1347 - checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1430 + checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 1348 1431 1349 1432 [[package]] 1350 1433 name = "litemap" ··· 1406 1489 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1407 1490 1408 1491 [[package]] 1492 + name = "minimal-lexical" 1493 + version = "0.2.1" 1494 + source = "registry+https://github.com/rust-lang/crates.io-index" 1495 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1496 + 1497 + [[package]] 1409 1498 name = "miniz_oxide" 1410 1499 version = "0.8.9" 1411 1500 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1426 1515 ] 1427 1516 1428 1517 [[package]] 1429 - name = "native-tls" 1430 - version = "0.2.14" 1518 + name = "nom" 1519 + version = "7.1.3" 1431 1520 source = "registry+https://github.com/rust-lang/crates.io-index" 1432 - checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1521 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1433 1522 dependencies = [ 1434 - "libc", 1435 - "log", 1436 - "openssl", 1437 - "openssl-probe", 1438 - "openssl-sys", 1439 - "schannel", 1440 - "security-framework", 1441 - "security-framework-sys", 1442 - "tempfile", 1523 + "memchr", 1524 + "minimal-lexical", 1443 1525 ] 1444 1526 1445 1527 [[package]] ··· 1551 1633 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1552 1634 1553 1635 [[package]] 1554 - name = "openssl" 1555 - version = "0.10.73" 1556 - source = "registry+https://github.com/rust-lang/crates.io-index" 1557 - checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 1558 - dependencies = [ 1559 - "bitflags", 1560 - "cfg-if", 1561 - "foreign-types", 1562 - "libc", 1563 - "once_cell", 1564 - "openssl-macros", 1565 - "openssl-sys", 1566 - ] 1567 - 1568 - [[package]] 1569 - name = "openssl-macros" 1570 - version = "0.1.1" 1571 - source = "registry+https://github.com/rust-lang/crates.io-index" 1572 - checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1573 - dependencies = [ 1574 - "proc-macro2", 1575 - "quote", 1576 - "syn", 1577 - ] 1578 - 1579 - [[package]] 1580 - name = "openssl-probe" 1581 - version = "0.1.6" 1582 - source = "registry+https://github.com/rust-lang/crates.io-index" 1583 - checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1584 - 1585 - [[package]] 1586 - name = "openssl-sys" 1587 - version = "0.9.109" 1588 - source = "registry+https://github.com/rust-lang/crates.io-index" 1589 - checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 1590 - dependencies = [ 1591 - "cc", 1592 - "libc", 1593 - "pkg-config", 1594 - "vcpkg", 1595 - ] 1596 - 1597 - [[package]] 1598 1636 name = "overload" 1599 1637 version = "0.1.1" 1600 1638 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1652 1690 1653 1691 [[package]] 1654 1692 name = "pds_gatekeeper" 1655 - version = "0.1.0" 1693 + version = "0.1.2" 1656 1694 dependencies = [ 1657 1695 "anyhow", 1696 + "aws-lc-rs", 1658 1697 "axum", 1659 1698 "axum-template", 1660 1699 "chrono", ··· 1666 1705 "lettre", 1667 1706 "rand 0.9.2", 1668 1707 "rust-embed", 1708 + "rustls", 1669 1709 "scrypt", 1670 1710 "serde", 1671 1711 "serde_json", ··· 1821 1861 ] 1822 1862 1823 1863 [[package]] 1864 + name = "prettyplease" 1865 + version = "0.2.35" 1866 + source = "registry+https://github.com/rust-lang/crates.io-index" 1867 + checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" 1868 + dependencies = [ 1869 + "proc-macro2", 1870 + "syn", 1871 + ] 1872 + 1873 + [[package]] 1824 1874 name = "proc-macro2" 1825 1875 version = "1.0.97" 1826 1876 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2005 2055 "cfg-if", 2006 2056 "getrandom 0.2.16", 2007 2057 "libc", 2008 - "untrusted", 2058 + "untrusted 0.9.0", 2009 2059 "windows-sys 0.52.0", 2010 2060 ] 2011 2061 ··· 2071 2121 checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 2072 2122 2073 2123 [[package]] 2124 + name = "rustc-hash" 2125 + version = "1.1.0" 2126 + source = "registry+https://github.com/rust-lang/crates.io-index" 2127 + checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 2128 + 2129 + [[package]] 2074 2130 name = "rustix" 2075 - version = "1.0.8" 2131 + version = "0.38.44" 2076 2132 source = "registry+https://github.com/rust-lang/crates.io-index" 2077 - checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 2133 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 2078 2134 dependencies = [ 2079 2135 "bitflags", 2080 2136 "errno", ··· 2089 2145 source = "registry+https://github.com/rust-lang/crates.io-index" 2090 2146 checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" 2091 2147 dependencies = [ 2148 + "aws-lc-rs", 2149 + "log", 2092 2150 "once_cell", 2093 2151 "ring", 2094 2152 "rustls-pki-types", ··· 2112 2170 source = "registry+https://github.com/rust-lang/crates.io-index" 2113 2171 checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 2114 2172 dependencies = [ 2173 + "aws-lc-rs", 2115 2174 "ring", 2116 2175 "rustls-pki-types", 2117 - "untrusted", 2176 + "untrusted 0.9.0", 2118 2177 ] 2119 2178 2120 2179 [[package]] ··· 2148 2207 ] 2149 2208 2150 2209 [[package]] 2151 - name = "schannel" 2152 - version = "0.1.27" 2153 - source = "registry+https://github.com/rust-lang/crates.io-index" 2154 - checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 2155 - dependencies = [ 2156 - "windows-sys 0.59.0", 2157 - ] 2158 - 2159 - [[package]] 2160 2210 name = "scopeguard" 2161 2211 version = "1.2.0" 2162 2212 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2193 2243 ] 2194 2244 2195 2245 [[package]] 2196 - name = "security-framework" 2197 - version = "2.11.1" 2198 - source = "registry+https://github.com/rust-lang/crates.io-index" 2199 - checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2200 - dependencies = [ 2201 - "bitflags", 2202 - "core-foundation", 2203 - "core-foundation-sys", 2204 - "libc", 2205 - "security-framework-sys", 2206 - ] 2207 - 2208 - [[package]] 2209 - name = "security-framework-sys" 2210 - version = "2.14.0" 2211 - source = "registry+https://github.com/rust-lang/crates.io-index" 2212 - checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 2213 - dependencies = [ 2214 - "core-foundation-sys", 2215 - "libc", 2216 - ] 2217 - 2218 - [[package]] 2219 2246 name = "serde" 2220 2247 version = "1.0.219" 2221 2248 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2643 2670 ] 2644 2671 2645 2672 [[package]] 2646 - name = "tempfile" 2647 - version = "3.21.0" 2648 - source = "registry+https://github.com/rust-lang/crates.io-index" 2649 - checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" 2650 - dependencies = [ 2651 - "fastrand", 2652 - "getrandom 0.3.3", 2653 - "once_cell", 2654 - "rustix", 2655 - "windows-sys 0.59.0", 2656 - ] 2657 - 2658 - [[package]] 2659 2673 name = "thiserror" 2660 2674 version = "1.0.69" 2661 2675 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2760 2774 ] 2761 2775 2762 2776 [[package]] 2763 - name = "tokio-native-tls" 2764 - version = "0.3.1" 2777 + name = "tokio-rustls" 2778 + version = "0.26.2" 2765 2779 source = "registry+https://github.com/rust-lang/crates.io-index" 2766 - checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2780 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 2767 2781 dependencies = [ 2768 - "native-tls", 2782 + "rustls", 2769 2783 "tokio", 2770 2784 ] 2771 2785 ··· 2998 3012 2999 3013 [[package]] 3000 3014 name = "untrusted" 3015 + version = "0.7.1" 3016 + source = "registry+https://github.com/rust-lang/crates.io-index" 3017 + checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 3018 + 3019 + [[package]] 3020 + name = "untrusted" 3001 3021 version = "0.9.0" 3002 3022 source = "registry+https://github.com/rust-lang/crates.io-index" 3003 3023 checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" ··· 3171 3191 checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" 3172 3192 dependencies = [ 3173 3193 "rustls-pki-types", 3194 + ] 3195 + 3196 + [[package]] 3197 + name = "which" 3198 + version = "4.4.2" 3199 + source = "registry+https://github.com/rust-lang/crates.io-index" 3200 + checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 3201 + dependencies = [ 3202 + "either", 3203 + "home", 3204 + "once_cell", 3205 + "rustix", 3174 3206 ] 3175 3207 3176 3208 [[package]]
+7 -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 + license = "MIT" 5 6 6 7 [dependencies] 7 8 axum = { version = "0.8.4", features = ["macros", "json"] } ··· 14 15 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 15 16 hyper-util = { version = "0.1.16", features = ["client", "client-legacy"] } 16 17 tower-http = { version = "0.6", features = ["cors", "compression-zstd"] } 17 - tower_governor = "0.8.0" 18 + tower_governor = { version = "0.8.0", features = ["axum", "tracing"] } 18 19 hex = "0.4" 19 20 jwt-compact = { version = "0.8.0", features = ["es256k"] } 20 21 scrypt = "0.11" 21 - lettre = { version = "0.11.18", features = ["tokio1", "pool", "tokio1-native-tls"] } 22 + #Leaveing these two cause I think it is needed by the email crate for ssl 23 + aws-lc-rs = "1.13.0" 24 + rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 25 + lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 22 26 handlebars = { version = "6.3.2", features = ["rust-embed"] } 23 27 rust-embed = "8.7.2" 24 28 axum-template = { version = "3.0.0", features = ["handlebars"] }
+10
Dockerfile
··· 1 + FROM rust:1.89.0-bookworm AS builder 2 + WORKDIR /app 3 + COPY ../ /app 4 + RUN cargo build --release 5 + # 6 + FROM rust:1.89-slim-bookworm AS api 7 + RUN apt-get update 8 + RUN apt-get install -y ca-certificates 9 + COPY --from=builder /app/target/release/pds_gatekeeper /usr/local/bin/pds_gatekeeper 10 + CMD ["pds_gatekeeper"]
+21
LICENSE.md
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Bailey Townsend 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+136 -13
README.md
··· 21 21 22 22 # Setup 23 23 24 - We are getting close! Testing now 24 + PDS Gatekeeper has 2 parts to its setup, docker compose file and a reverse proxy (Caddy in this case). I will be 25 + assuming you setup the PDS following the directions 26 + found [here](https://atproto.com/guides/self-hosting), but if yours is different, or you have questions, feel free to 27 + let 28 + me know, and we can figure it out. 29 + 30 + ## Docker compose 31 + 32 + The pds gatekeeper container can be found on docker hub under the name `fatfingers23/pds_gatekeeper`. The container does 33 + need access to the `/pds` root folder to access the same db's as your PDS. The part you need to add would look a bit 34 + like below. You can find a full example of what I use for my pds at [./examples/compose.yml](./examples/compose.yml). 35 + This is usually found at `/pds/compose.yaml`on your PDS> 36 + 37 + ```yml 38 + gatekeeper: 39 + container_name: gatekeeper 40 + image: fatfingers23/pds_gatekeeper:latest 41 + network_mode: host 42 + restart: unless-stopped 43 + #This gives the container to the access to the PDS folder. Source is the location on your server of that directory 44 + volumes: 45 + - type: bind 46 + source: /pds 47 + target: /pds 48 + depends_on: 49 + - pds 50 + ``` 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 + ``` 25 95 26 - Nothing here yet! If you are brave enough to try before full release, let me know and I'll help you set it up. 27 - But I want to run it locally on my own PDS first to test run it a bit. 96 + ## Caddy setup 28 97 29 - Example Caddyfile (mostly so I don't lose it for now. Will have a better one in the future) 98 + For the reverse proxy I use caddy. This part is what overwrites the endpoints and proxies them to PDS gatekeeper to add 99 + in extra functionality. The main part is below, for a full example see [./examples/Caddyfile](./examples/Caddyfile). 100 + This is usually found at `/pds/caddy/etc/caddy/Caddyfile` on your PDS. 30 101 31 102 ```caddyfile 32 - http://localhost { 33 - 34 103 @gatekeeper { 35 - path /xrpc/com.atproto.server.getSession 36 - path /xrpc/com.atproto.server.updateEmail 37 - path /xrpc/com.atproto.server.createSession 38 - path /@atproto/oauth-provider/~api/sign-in 104 + path /xrpc/com.atproto.server.getSession 105 + path /xrpc/com.atproto.server.updateEmail 106 + path /xrpc/com.atproto.server.createSession 107 + path /xrpc/com.atproto.server.createAccount 108 + path /@atproto/oauth-provider/~api/sign-in 39 109 } 40 110 41 111 handle @gatekeeper { 42 - reverse_proxy http://localhost:8080 112 + reverse_proxy http://localhost:8080 43 113 } 44 114 45 - reverse_proxy /* http://localhost:3000 115 + reverse_proxy http://localhost:3000 116 + ``` 117 + 118 + If you use a cloudflare tunnel then your caddyfile would look a bit more like below with your tunnel proxying to 119 + `localhost:8081` (or w/e port you want). 120 + 121 + ```caddyfile 122 + http://*.localhost:8082, http://localhost:8082 { 123 + @gatekeeper { 124 + path /xrpc/com.atproto.server.getSession 125 + path /xrpc/com.atproto.server.updateEmail 126 + path /xrpc/com.atproto.server.createSession 127 + path /xrpc/com.atproto.server.createAccount 128 + path /@atproto/oauth-provider/~api/sign-in 129 + } 130 + 131 + handle @gatekeeper { 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 + } 136 + } 137 + 138 + reverse_proxy http://localhost:3000 46 139 } 47 140 48 - ``` 141 + ``` 142 + 143 + # Environment variables and bonuses 144 + 145 + Every environment variable can be set in the `pds.env` and shared between PDS and gatekeeper and the PDS, with the 146 + exception of `PDS_ENV_LOCATION`. This can be set to load the pds.env, by default it checks `/pds/pds.env` and is 147 + recommended to mount the `/pds` folder on the server to `/pds` in the pds gatekeeper container. 148 + 149 + `PDS_DATA_DIRECTORY` - Root directory of the PDS. Same as the one found in `pds.env` this is how pds gatekeeper knows 150 + knows the rest of the environment variables. 151 + 152 + `GATEKEEPER_EMAIL_TEMPLATES_DIRECTORY` - The folder for templates of the emails PDS gatekeeper sends. You can find them 153 + in [./email_templates](./email_templates). You are free to edit them as you please and set this variable to a location 154 + in the pds gateekeper container and it will use them in place of the default ones. Just make sure ot keep the names the 155 + same. 156 + 157 + `GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT` - Subject of the email sent to the user when they turn on 2FA. Defaults to 158 + `Sign in to Bluesky` 159 + 160 + `PDS_BASE_URL` - Base url of the PDS. You most likely want `https://localhost:3000` which is also the default 161 + 162 + `GATEKEEPER_HOST` - Host for pds gatekeeper. Defaults to `127.0.0.1` 163 + 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.
+30
examples/Caddyfile
··· 1 + { 2 + email youremail@myemail.com 3 + on_demand_tls { 4 + ask http://localhost:3000/tls-check 5 + } 6 + } 7 + 8 + *.yourpds.com, yourpds.com { 9 + tls { 10 + on_demand 11 + } 12 + # You'll most likely just want from here to.... 13 + @gatekeeper { 14 + path /xrpc/com.atproto.server.getSession 15 + path /xrpc/com.atproto.server.updateEmail 16 + path /xrpc/com.atproto.server.createSession 17 + path /xrpc/com.atproto.server.createAccount 18 + path /@atproto/oauth-provider/~api/sign-in 19 + } 20 + 21 + handle @gatekeeper { 22 + #This is the address for PDS gatekeeper, default is 8080 23 + reverse_proxy http://localhost:8080 24 + } 25 + 26 + reverse_proxy http://localhost:3000 27 + #..here. Copy and paste this replacing the reverse_proxy http://localhost:3000 line 28 + } 29 + 30 +
+51
examples/compose.yml
··· 1 + version: '3.9' 2 + services: 3 + caddy: 4 + container_name: caddy 5 + image: caddy:2 6 + network_mode: host 7 + depends_on: 8 + - pds 9 + restart: unless-stopped 10 + volumes: 11 + - type: bind 12 + source: /pds/caddy/data 13 + target: /data 14 + - type: bind 15 + source: /pds/caddy/etc/caddy 16 + target: /etc/caddy 17 + pds: 18 + container_name: pds 19 + image: ghcr.io/bluesky-social/pds:0.4 20 + network_mode: host 21 + restart: unless-stopped 22 + volumes: 23 + - type: bind 24 + source: /pds 25 + target: /pds 26 + env_file: 27 + - /pds/pds.env 28 + watchtower: 29 + container_name: watchtower 30 + image: containrrr/watchtower:latest 31 + network_mode: host 32 + volumes: 33 + - type: bind 34 + source: /var/run/docker.sock 35 + target: /var/run/docker.sock 36 + restart: unless-stopped 37 + environment: 38 + WATCHTOWER_CLEANUP: true 39 + WATCHTOWER_SCHEDULE: "@midnight" 40 + gatekeeper: 41 + container_name: gatekeeper 42 + image: fatfingers23/pds_gatekeeper:latest 43 + network_mode: host 44 + restart: unless-stopped 45 + #This gives the container to the access to the PDS folder. Source is the location on your server of that directory 46 + volumes: 47 + - type: bind 48 + source: /pds 49 + target: /pds 50 + depends_on: 51 + - pds
+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
+6
justfile
··· 1 + release: 2 + docker buildx build \ 3 + --platform linux/arm64,linux/amd64 \ 4 + --tag fatfingers23/pds_gatekeeper:latest \ 5 + --tag fatfingers23/pds_gatekeeper:0.1.0.3 \ 6 + --push .
+6 -5
src/helpers.rs
··· 15 15 use serde_json::{Map, Value}; 16 16 use sha2::{Digest, Sha256}; 17 17 use sqlx::SqlitePool; 18 + use std::env; 18 19 use tracing::{error, log}; 19 20 20 21 ///Used to generate the email 2fa code ··· 134 135 full_code.push(UPPERCASE_BASE32_CHARS[idx] as char); 135 136 } 136 137 137 - //The PDS implementation creates in lowercase, then converts to uppercase. 138 - //Just going a head and doing uppercase here. 139 - let slice_one = &full_code[0..5].to_ascii_uppercase(); 140 - let slice_two = &full_code[5..10].to_ascii_uppercase(); 138 + let slice_one = &full_code[0..5]; 139 + let slice_two = &full_code[5..10]; 141 140 format!("{slice_one}-{slice_two}") 142 141 } 143 142 ··· 334 333 let email_body = state 335 334 .template_engine 336 335 .render("two_factor_code.hbs", email_data)?; 336 + let email_subject = env::var("GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT") 337 + .unwrap_or("Sign in to Bluesky".to_string()); 337 338 338 339 let email_message = Message::builder() 339 340 //TODO prob get the proper type in the state 340 341 .from(state.mailer_from.parse()?) 341 342 .to(email.parse()?) 342 - .subject("Sign in to Bluesky") 343 + .subject(email_subject) 343 344 .multipart( 344 345 MultiPart::alternative() // This is composed of two parts. 345 346 .singlepart(
+58 -10
src/main.rs
··· 1 1 #![warn(clippy::unwrap_used)] 2 2 use crate::oauth_provider::sign_in; 3 - use crate::xrpc::com_atproto_server::{create_session, get_session, update_email}; 3 + use crate::xrpc::com_atproto_server::{create_account, create_session, get_session, update_email}; 4 4 use axum::body::Body; 5 5 use axum::handler::Handler; 6 6 use axum::http::{Method, header}; ··· 20 20 use std::{env, net::SocketAddr}; 21 21 use tower_governor::GovernorLayer; 22 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; ··· 88 89 #[tokio::main] 89 90 async fn main() -> Result<(), Box<dyn std::error::Error>> { 90 91 setup_tracing(); 91 - //TODO may need to change where this reads from? Like an env variable for it's location? Or arg? 92 - dotenvy::from_path(Path::new("./pds.env"))?; 93 - let pds_root = env::var("PDS_DATA_DIRECTORY")?; 92 + let pds_env_location = 93 + env::var("PDS_ENV_LOCATION").unwrap_or_else(|_| "/pds/pds.env".to_string()); 94 + 95 + let result_of_finding_pds_env = dotenvy::from_path(Path::new(&pds_env_location)); 96 + if let Err(e) = result_of_finding_pds_env { 97 + log::error!( 98 + "Error loading pds.env file (ignore if you loaded your variables in the environment somehow else): {e}" 99 + ); 100 + } 101 + 102 + let pds_root = 103 + env::var("PDS_DATA_DIRECTORY").expect("PDS_DATA_DIRECTORY is not set in your pds.env file"); 94 104 let account_db_url = format!("{pds_root}/account.sqlite"); 95 105 96 106 let account_options = SqliteConnectOptions::new() ··· 129 139 env::var("PDS_EMAIL_SMTP_URL").expect("PDS_EMAIL_SMTP_URL is not set in your pds.env file"); 130 140 let sent_from = env::var("PDS_EMAIL_FROM_ADDRESS") 131 141 .expect("PDS_EMAIL_FROM_ADDRESS is not set in your pds.env file"); 142 + 132 143 let mailer: AsyncSmtpTransport<Tokio1Executor> = 133 144 AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build(); 134 145 //Email templates setup ··· 162 173 let create_session_governor_conf = GovernorConfigBuilder::default() 163 174 .per_second(60) 164 175 .burst_size(5) 176 + .key_extractor(SmartIpKeyExtractor) 165 177 .finish() 166 - .expect("failed to create governor config. this should not happen and is a bug"); 178 + .expect("failed to create governor config for create session. this should not happen and is a bug"); 167 179 168 180 // Create a second config with the same settings for the other endpoint 169 181 let sign_in_governor_conf = GovernorConfigBuilder::default() 170 182 .per_second(60) 171 183 .burst_size(5) 184 + .key_extractor(SmartIpKeyExtractor) 172 185 .finish() 173 - .expect("failed to create governor config. this should not happen and is a bug"); 186 + .expect( 187 + "failed to create governor config for sign in. this should not happen and is a bug", 188 + ); 189 + 190 + let create_account_limiter_time: Option<String> = 191 + env::var("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND").ok(); 192 + let create_account_limiter_burst: Option<String> = 193 + env::var("GATEKEEPER_CREATE_ACCOUNT_BURST").ok(); 194 + 195 + //Default should be 608 requests per 5 minutes, PDS is 300 per 500 so will never hit it ideally 196 + let mut create_account_governor_conf = GovernorConfigBuilder::default(); 197 + if create_account_limiter_time.is_some() { 198 + let time = create_account_limiter_time 199 + .expect("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND not set") 200 + .parse::<u64>() 201 + .expect("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND must be a valid integer"); 202 + create_account_governor_conf.per_second(time); 203 + } 204 + 205 + if create_account_limiter_burst.is_some() { 206 + let burst = create_account_limiter_burst 207 + .expect("GATEKEEPER_CREATE_ACCOUNT_BURST not set") 208 + .parse::<u32>() 209 + .expect("GATEKEEPER_CREATE_ACCOUNT_BURST must be a valid integer"); 210 + create_account_governor_conf.burst_size(burst); 211 + } 212 + 213 + let create_account_governor_conf = create_account_governor_conf 214 + .key_extractor(SmartIpKeyExtractor) 215 + .finish().expect( 216 + "failed to create governor config for create account. this should not happen and is a bug", 217 + ); 174 218 175 219 let create_session_governor_limiter = create_session_governor_conf.limiter().clone(); 176 220 let sign_in_governor_limiter = sign_in_governor_conf.limiter().clone(); 221 + let create_account_governor_limiter = create_account_governor_conf.limiter().clone(); 222 + 177 223 let interval = Duration::from_secs(60); 178 224 // a separate background task to clean up 179 225 std::thread::spawn(move || { ··· 181 227 std::thread::sleep(interval); 182 228 create_session_governor_limiter.retain_recent(); 183 229 sign_in_governor_limiter.retain_recent(); 230 + create_account_governor_limiter.retain_recent(); 184 231 } 185 232 }); 186 233 ··· 191 238 192 239 let app = Router::new() 193 240 .route("/", get(root_handler)) 194 - .route( 195 - "/xrpc/com.atproto.server.getSession", 196 - get(get_session).layer(ax_middleware::from_fn(middleware::extract_did)), 197 - ) 241 + .route("/xrpc/com.atproto.server.getSession", get(get_session)) 198 242 .route( 199 243 "/xrpc/com.atproto.server.updateEmail", 200 244 post(update_email).layer(ax_middleware::from_fn(middleware::extract_did)), ··· 206 250 .route( 207 251 "/xrpc/com.atproto.server.createSession", 208 252 post(create_session.layer(GovernorLayer::new(create_session_governor_conf))), 253 + ) 254 + .route( 255 + "/xrpc/com.atproto.server.createAccount", 256 + post(create_account).layer(GovernorLayer::new(create_account_governor_conf)), 209 257 ) 210 258 .layer(CompressionLayer::new()) 211 259 .layer(cors)
+73 -39
src/middleware.rs
··· 12 12 #[derive(Clone, Debug)] 13 13 pub struct Did(pub Option<String>); 14 14 15 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 + pub enum AuthScheme { 17 + Bearer, 18 + DPoP, 19 + } 20 + 15 21 #[derive(Serialize, Deserialize)] 16 22 pub struct TokenClaims { 17 23 pub sub: String, 18 24 } 19 25 20 26 pub async fn extract_did(mut req: Request, next: Next) -> impl IntoResponse { 21 - let token = extract_bearer(req.headers()); 27 + let auth = extract_auth(req.headers()); 22 28 23 - match token { 24 - Ok(token) => { 25 - match token { 29 + match auth { 30 + Ok(auth_opt) => { 31 + match auth_opt { 26 32 None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 27 33 .expect("Error creating an error response"), 28 - Some(token) => { 29 - let token = UntrustedToken::new(&token); 30 - if token.is_err() { 31 - return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 32 - .expect("Error creating an error response"); 33 - } 34 - let parsed_token = token.expect("Already checked for error"); 35 - let claims: Result<Claims<TokenClaims>, ValidationError> = 36 - parsed_token.deserialize_claims_unchecked(); 37 - if claims.is_err() { 38 - return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 39 - .expect("Error creating an error response"); 40 - } 34 + Some((scheme, token_str)) => { 35 + // For Bearer, validate JWT and extract DID from `sub`. 36 + // For DPoP, we currently only pass through and do not validate here; insert None DID. 37 + match scheme { 38 + AuthScheme::Bearer => { 39 + let token = UntrustedToken::new(&token_str); 40 + if token.is_err() { 41 + return json_error_response( 42 + StatusCode::BAD_REQUEST, 43 + "TokenRequired", 44 + "", 45 + ) 46 + .expect("Error creating an error response"); 47 + } 48 + let parsed_token = token.expect("Already checked for error"); 49 + let claims: Result<Claims<TokenClaims>, ValidationError> = 50 + parsed_token.deserialize_claims_unchecked(); 51 + if claims.is_err() { 52 + return json_error_response( 53 + StatusCode::BAD_REQUEST, 54 + "TokenRequired", 55 + "", 56 + ) 57 + .expect("Error creating an error response"); 58 + } 41 59 42 - let key = Hs256Key::new( 43 - env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env"), 44 - ); 45 - let token: Result<Token<TokenClaims>, ValidationError> = 46 - Hs256.validator(&key).validate(&parsed_token); 47 - if token.is_err() { 48 - return json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "") 49 - .expect("Error creating an error response"); 60 + let key = Hs256Key::new( 61 + env::var("PDS_JWT_SECRET") 62 + .expect("PDS_JWT_SECRET not set in the pds.env"), 63 + ); 64 + let token: Result<Token<TokenClaims>, ValidationError> = 65 + Hs256.validator(&key).validate(&parsed_token); 66 + if token.is_err() { 67 + return json_error_response( 68 + StatusCode::BAD_REQUEST, 69 + "InvalidToken", 70 + "", 71 + ) 72 + .expect("Error creating an error response"); 73 + } 74 + let token = token.expect("Already checked for error,"); 75 + req.extensions_mut() 76 + .insert(Did(Some(token.claims().custom.sub.clone()))); 77 + } 78 + AuthScheme::DPoP => { 79 + //Not going to worry about oauth email update for now, just always forward to the PDS 80 + req.extensions_mut().insert(Did(None)); 81 + } 50 82 } 51 - let token = token.expect("Already checked for error,"); 52 - //Not going to worry about expiration since it still goes to the PDS 53 - req.extensions_mut() 54 - .insert(Did(Some(token.claims().custom.sub.clone()))); 83 + 55 84 next.run(req).await 56 85 } 57 86 } ··· 64 93 } 65 94 } 66 95 67 - fn extract_bearer(headers: &HeaderMap) -> Result<Option<String>, String> { 96 + fn extract_auth(headers: &HeaderMap) -> Result<Option<(AuthScheme, String)>, String> { 68 97 match headers.get(axum::http::header::AUTHORIZATION) { 69 98 None => Ok(None), 70 - Some(hv) => match hv.to_str() { 71 - Err(_) => Err("Authorization header is not valid".into()), 72 - Ok(s) => { 73 - // Accept forms like: "Bearer <token>" (case-sensitive for the scheme here) 74 - let mut parts = s.splitn(2, ' '); 75 - match (parts.next(), parts.next()) { 76 - (Some("Bearer"), Some(tok)) if !tok.is_empty() => Ok(Some(tok.to_string())), 77 - _ => Err("Authorization header must be in format 'Bearer <token>'".into()), 99 + Some(hv) => { 100 + match hv.to_str() { 101 + Err(_) => Err("Authorization header is not valid".into()), 102 + Ok(s) => { 103 + // Accept forms like: "Bearer <token>" or "DPoP <token>" (case-sensitive for the scheme here) 104 + let mut parts = s.splitn(2, ' '); 105 + match (parts.next(), parts.next()) { 106 + (Some("Bearer"), Some(tok)) if !tok.is_empty() => 107 + Ok(Some((AuthScheme::Bearer, tok.to_string()))), 108 + (Some("DPoP"), Some(tok)) if !tok.is_empty() => 109 + Ok(Some((AuthScheme::DPoP, tok.to_string()))), 110 + _ => Err("Authorization header must be in format 'Bearer <token>' or 'DPoP <token>'".into()), 111 + } 78 112 } 79 113 } 80 - }, 114 + } 81 115 } 82 116 }
+3 -5
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>, ··· 36 37 "Invalid identifier or password", 37 38 ), 38 39 AuthResult::TwoFactorRequired(masked_email) => { 39 - // Email sending step can be handled here if needed in the future. 40 - 41 - // {"error":"second_authentication_factor_required","error_description":"emailOtp authentication factor required (hint: 2***0@p***m)","type":"emailOtp","hint":"2***0@p***m"} 42 40 let body_str = match serde_json::to_string(&serde_json::json!({ 43 41 "error": "second_authentication_factor_required", 44 42 "error_description": format!("emailOtp authentication factor required (hint: {})", masked_email), ··· 97 95 }, 98 96 Err(err) => { 99 97 log::error!( 100 - "Error during pre-auth check. This happens on the create_session endpoint when trying to decide if the user has access:\n {err}" 98 + "Error during pre-auth check. This happens on the oauth signin endpoint when trying to decide if the user has access:\n {err}" 101 99 ); 102 100 oauth_json_error_response( 103 101 StatusCode::BAD_REQUEST,
+94 -48
src/xrpc/com_atproto_server.rs
··· 87 87 ) 88 88 } 89 89 AuthResult::ProxyThrough => { 90 - log::info!("Proxying through"); 91 90 //No 2FA or already passed 92 91 let uri = format!( 93 92 "{}{}", ··· 148 147 //If email auth is set it is to either turn on or off 2fa 149 148 let email_auth_update = payload.email_auth_factor.unwrap_or(false); 150 149 151 - // Email update asked for 152 - if email_auth_update { 153 - let email = payload.email.clone(); 154 - let email_confirmed = sqlx::query_as::<_, (String,)>( 155 - "SELECT did FROM account WHERE emailConfirmedAt IS NOT NULL AND email = ?", 156 - ) 157 - .bind(&email) 158 - .fetch_optional(&state.account_pool) 159 - .await 160 - .map_err(|_| StatusCode::BAD_REQUEST)?; 150 + //This means the middleware successfully extracted a did from the request, if not it just needs to be forward to the PDS 151 + //This is also empty if it is an oauth request, which is not supported by gatekeeper turning on 2fa since the dpop stuff needs to be implemented 152 + let did_is_not_empty = did.0.is_some(); 161 153 162 - //Since the email is already confirmed we can enable 2fa 163 - return match email_confirmed { 164 - None => Err(StatusCode::BAD_REQUEST), 165 - Some(did_row) => { 166 - let _ = sqlx::query( 167 - "INSERT INTO two_factor_accounts (did, required) VALUES (?, 1) ON CONFLICT(did) DO UPDATE SET required = 1", 168 - ) 169 - .bind(&did_row.0) 170 - .execute(&state.pds_gatekeeper_pool) 171 - .await 172 - .map_err(|_| StatusCode::BAD_REQUEST)?; 173 - 174 - Ok(StatusCode::OK.into_response()) 175 - } 176 - }; 177 - } 178 - 179 - // User wants auth turned off 180 - if !email_auth_update && !email_auth_not_set { 181 - //User wants auth turned off and has a token 182 - if let Some(token) = &payload.token { 183 - let token_found = sqlx::query_as::<_, (String,)>( 184 - "SELECT token FROM email_token WHERE token = ? AND did = ? AND purpose = 'update_email'", 154 + if did_is_not_empty { 155 + // Email update asked for 156 + if email_auth_update { 157 + let email = payload.email.clone(); 158 + let email_confirmed = match sqlx::query_as::<_, (String,)>( 159 + "SELECT did FROM account WHERE emailConfirmedAt IS NOT NULL AND email = ?", 185 160 ) 186 - .bind(token) 187 - .bind(&did.0) 161 + .bind(&email) 188 162 .fetch_optional(&state.account_pool) 189 163 .await 190 - .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 + }; 171 + 172 + //Since the email is already confirmed we can enable 2fa 173 + return match email_confirmed { 174 + None => Err(StatusCode::BAD_REQUEST), 175 + Some(did_row) => { 176 + let _ = sqlx::query( 177 + "INSERT INTO two_factor_accounts (did, required) VALUES (?, 1) ON CONFLICT(did) DO UPDATE SET required = 1", 178 + ) 179 + .bind(&did_row.0) 180 + .execute(&state.pds_gatekeeper_pool) 181 + .await 182 + .map_err(|_| StatusCode::BAD_REQUEST)?; 191 183 192 - if token_found.is_some() { 193 - let _ = sqlx::query( 194 - "INSERT INTO two_factor_accounts (did, required) VALUES (?, 0) ON CONFLICT(did) DO UPDATE SET required = 0", 184 + Ok(StatusCode::OK.into_response()) 185 + } 186 + }; 187 + } 188 + 189 + // User wants auth turned off 190 + if !email_auth_update && !email_auth_not_set { 191 + //User wants auth turned off and has a token 192 + if let Some(token) = &payload.token { 193 + let token_found = match sqlx::query_as::<_, (String,)>( 194 + "SELECT token FROM email_token WHERE token = ? AND did = ? AND purpose = 'update_email'", 195 195 ) 196 - .bind(&did.0) 197 - .execute(&state.pds_gatekeeper_pool) 198 - .await 199 - .map_err(|_| StatusCode::BAD_REQUEST)?; 196 + .bind(token) 197 + .bind(&did.0) 198 + .fetch_optional(&state.account_pool) 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 + }; 206 + 207 + return if token_found.is_some() { 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( 211 + "INSERT INTO two_factor_accounts (did, required) VALUES (?, 0) ON CONFLICT(did) DO UPDATE SET required = 0", 212 + ) 213 + .bind(&did.0) 214 + .execute(&state.pds_gatekeeper_pool) 215 + .await { 216 + Ok(_) => {} 217 + Err(err) => { 218 + log::error!("Error updating email auth: {err}"); 219 + return Err(StatusCode::BAD_REQUEST); 220 + } 221 + } 200 222 201 - return Ok(StatusCode::OK.into_response()); 202 - } else { 203 - return Err(StatusCode::BAD_REQUEST); 223 + Ok(StatusCode::OK.into_response()) 224 + } else { 225 + Err(StatusCode::BAD_REQUEST) 226 + }; 204 227 } 205 228 } 206 229 } 207 - 208 230 // Updating the actual email address by sending it on to the PDS 209 231 let uri = format!( 210 232 "{}{}", ··· 260 282 ProxiedResult::Passthrough(resp) => Ok(resp), 261 283 } 262 284 } 285 + 286 + pub async fn create_account( 287 + State(state): State<AppState>, 288 + mut req: Request, 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 + 292 + let uri = format!( 293 + "{}{}", 294 + state.pds_base_url, "/xrpc/com.atproto.server.createAccount" 295 + ); 296 + 297 + // Rewrite the URI to point at the upstream PDS; keep headers, method, and body intact 298 + *req.uri_mut() = uri.parse().map_err(|_| StatusCode::BAD_REQUEST)?; 299 + 300 + let proxied = state 301 + .reverse_proxy_client 302 + .request(req) 303 + .await 304 + .map_err(|_| StatusCode::BAD_REQUEST)? 305 + .into_response(); 306 + 307 + Ok(proxied) 308 + }