Compare commits
10
Commits
8e2f622811
...
7ba8629e47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ba8629e47 | ||
|
|
2919037d3b | ||
|
|
44c8e6b6f8 | ||
|
|
1467b71037 | ||
|
|
08aace5428 | ||
|
|
1749d9c08e | ||
|
|
ab163ca5e6 | ||
|
|
6385be0890 | ||
|
|
1de0fa16db | ||
|
|
83d437c9ce |
@@ -0,0 +1,77 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
A minimal Rust reimplementation of the Cyrus `saslauthd` daemon. It listens on a Unix
|
||||
socket, speaks the saslauthd wire protocol, and checks credentials against per-user
|
||||
password files in a directory. In production it runs as PID 1 in an Alpine container
|
||||
(`pobox_saslauthd`), and Cyrus IMAP / Exim authenticate through its socket.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
cargo build --workspace
|
||||
cargo test --workspace # unit + integration tests
|
||||
cargo test -p server --test shutdown_test # one integration test file
|
||||
cargo test -p server it_checks_argon2 # one test by name
|
||||
cargo clippy --workspace --all-targets # plain `cargo clippy` skips test code
|
||||
cargo run -p server -- -f /tmp/saslauthd.sock -p ./test_passwords
|
||||
RUST_LOG=info cargo run -p server # env_logger; info shows auth results + shutdown
|
||||
cargo run -p client -- -f /tmp/saslauthd.sock bp some-rad-password # manual round-trip
|
||||
docker build -t saslauthd . # musl static build, targets x86_64 only
|
||||
```
|
||||
|
||||
Tests spawn the real `saslauthd` binary on a socket under the OS temp dir and signal it
|
||||
with `kill`, so they are Unix-only and rely on `test_passwords/bp` at the repo root.
|
||||
|
||||
## Architecture
|
||||
|
||||
Cargo workspace (edition 2024) with three crates:
|
||||
|
||||
- `common` — the wire protocol shared by server and client. `io.rs` reads/writes
|
||||
length-prefixed strings (u16 big-endian length + bytes). `request.rs` is the four-field
|
||||
request (userid, password, service, realm) in that order.
|
||||
- `server` — the `saslauthd` binary. `Server` owns the `UnixListener`, spawns one thread
|
||||
per connection running `Handler`, which parses a `Request`, asks `PasswordDirectory`
|
||||
whether it is valid, and writes back `OK` or `NO` as a length-prefixed string.
|
||||
- `client` — `saslauthd_test_client`, a tiny CLI for exercising a running server.
|
||||
|
||||
Top-level `tests/` holds the `common` crate's protocol tests; `server/tests/` holds the
|
||||
process-level shutdown tests.
|
||||
|
||||
### Authentication model (`server/src/repository.rs`)
|
||||
|
||||
The password directory contains one file per userid. Blank lines and `#` comments are
|
||||
ignored. Each remaining line is either:
|
||||
|
||||
- a plain-text password, matched exactly, or
|
||||
- `[key]<argon2 PHC hash>`, matched only when the client sends the password as
|
||||
`[key]<plaintext>` and the plaintext verifies against the hash for that key.
|
||||
|
||||
The client's password prefix decides which path is taken. Userids containing `/` are
|
||||
rejected before touching the filesystem. `service` and `realm` are read but ignored.
|
||||
|
||||
### Server lifecycle (`server/src/server.rs`)
|
||||
|
||||
Startup removes any stale file at the socket path, binds, and chmods the socket 0777.
|
||||
The accept loop is a plain blocking `accept()`. A `signal-hook` thread waits for
|
||||
SIGTERM/SIGINT; on the first one it sets a shutdown flag and makes a throwaway
|
||||
connection to the socket to wake the accept loop, which then closes the listener,
|
||||
unlinks the socket file, and waits up to 3 s for in-flight handler threads before
|
||||
exiting 0. A second signal exits immediately with status 1. Accepted streams get a 5 s
|
||||
read/write timeout so a silent client cannot pin a thread. This matters because as
|
||||
container PID 1 the process gets no default signal handling; without it `docker stop`
|
||||
hangs for 10 s and host reboots stall.
|
||||
|
||||
CLI options live in `options.rs` as a `LazyLock<Opt>` (clap derive) and are read from
|
||||
anywhere via `OPTIONS`.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not change the wire protocol or the `--socket-name` / `--password-dir` flags;
|
||||
Cyrus and Exim depend on both.
|
||||
- The Dockerfile hardcodes `x86_64-unknown-linux-musl`. On an arm64 Mac the cross-build
|
||||
segfaults under qemu; verify container behaviour by substituting
|
||||
`aarch64-unknown-linux-musl` in a copy of the Dockerfile instead.
|
||||
Generated
+352
-78
@@ -3,10 +3,19 @@
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -19,15 +28,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
@@ -54,9 +63,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
@@ -71,28 +80,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.57"
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -100,9 +121,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.57"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -112,78 +133,157 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.7"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "client"
|
||||
version = "0.3.1"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "common"
|
||||
version = "0.3.1"
|
||||
version = "0.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"defmt-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-parser"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
"ctutils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
name = "env_filter"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -192,6 +292,15 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -199,10 +308,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
name = "jiff"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"jiff-core",
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-core"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212"
|
||||
dependencies = [
|
||||
"jiff-core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
@@ -212,46 +370,143 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"phc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phc"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
"ctutils",
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "server"
|
||||
version = "0.3.1"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"clap",
|
||||
"common",
|
||||
"env_logger",
|
||||
"log",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -260,17 +515,11 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -278,16 +527,47 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
name = "syn"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
@@ -295,12 +575,6 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.1"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
################
|
||||
##### Builder
|
||||
FROM rust:1.88.0-slim AS builder
|
||||
FROM rust:1.98.1-slim AS builder
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN cargo build --package server --target x86_64-unknown-linux-musl --release
|
||||
|
||||
################
|
||||
##### Runtime
|
||||
FROM alpine:3.22.0 AS runtime
|
||||
FROM alpine:3.24.1 AS runtime
|
||||
|
||||
# Copy application binary from builder image
|
||||
COPY --from=builder /usr/src/target/x86_64-unknown-linux-musl/release/saslauthd /usr/local/bin
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(Parser, Clone, Debug)]
|
||||
#[command(name = "saslauthd_test_client", version)]
|
||||
/// saslauthd test client
|
||||
pub struct Opt {
|
||||
#[arg(
|
||||
|
||||
+5
-2
@@ -6,8 +6,11 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
common = { workspace = true }
|
||||
argon2 = "0.5.3"
|
||||
argon2 = "0.6.0"
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.29"
|
||||
signal-hook = "0.4"
|
||||
|
||||
[[bin]]
|
||||
name = "saslauthd"
|
||||
path = "src/main.rs"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::repository::PasswordDirectory;
|
||||
use common::request::Request;
|
||||
use log::{error, info};
|
||||
use std::io::{Error, Read, Write};
|
||||
|
||||
pub const RESPONSE_NO: [u8; 4] = [0x0, 0x2, b'N', b'O'];
|
||||
@@ -18,19 +19,21 @@ impl<S: Read + Write> Handler<S> {
|
||||
fn communicate(&mut self) -> Result<(), Error> {
|
||||
let request = Request::from_stream(&mut self.stream)?;
|
||||
|
||||
println!("userid: {}", request.userid);
|
||||
if self.repository.check_auth(&request) {
|
||||
self.stream.write_all(&RESPONSE_OK)?
|
||||
self.stream.write_all(&RESPONSE_OK)?;
|
||||
info!("userid: {}, service: {}, realm: {} - OK", request.userid, request.service, request.realm);
|
||||
} else {
|
||||
self.stream.write_all(&RESPONSE_NO)?
|
||||
self.stream.write_all(&RESPONSE_NO)?;
|
||||
info!("userid: {}, service: {}, realm: {} - FAIL", request.userid, request.service, request.realm);
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_client(&mut self) {
|
||||
if let Err(error) = self.communicate() {
|
||||
eprint!("{error}");
|
||||
error!("{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -8,7 +8,9 @@ use crate::repository::PasswordDirectory;
|
||||
use crate::server::Server;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let repository = PasswordDirectory::new(&OPTIONS.password_dir);
|
||||
let mut server = Server::new(&OPTIONS.socket_name, repository)?;
|
||||
let server = Server::new(&OPTIONS.socket_name, repository)?;
|
||||
server.run()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use clap::Parser;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(Parser, Clone, Debug)]
|
||||
#[command(name = "saslauthd", version)]
|
||||
/// saslauthd server
|
||||
pub struct Opt {
|
||||
#[arg(
|
||||
|
||||
+13
-12
@@ -1,13 +1,11 @@
|
||||
use common::request::Request;
|
||||
|
||||
use argon2::{
|
||||
Argon2,
|
||||
password_hash::{PasswordHash, PasswordVerifier},
|
||||
};
|
||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use log::info;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PasswordDirectory {
|
||||
@@ -49,6 +47,7 @@ impl PasswordDirectory {
|
||||
///
|
||||
fn check_hashed_password(&self, lines: &Vec<String>, password: &str) -> bool {
|
||||
let (key, split_password) = password.strip_prefix('[').unwrap().split_once(']').unwrap();
|
||||
info!(" looking for password-key: [{}]", key);
|
||||
|
||||
let mut result = false;
|
||||
|
||||
@@ -72,6 +71,8 @@ impl PasswordDirectory {
|
||||
}
|
||||
|
||||
fn check_plain_password(&self, lines: &Vec<String>, password: &str) -> bool {
|
||||
info!(" looking for plain password");
|
||||
|
||||
let mut result = false;
|
||||
|
||||
for line in lines {
|
||||
@@ -123,7 +124,7 @@ mod tests {
|
||||
// echo -n "hunter2" | argon2 "$(openssl rand -base64 16)" -id
|
||||
let password_hash = "$argon2id$v=19$m=4096,t=3,p=1$VTMrc2wyaW93L01ibVFPOGNQcHcxQT09$UxX86sGpknkc45CnXq+4CZ0coiTYDvSWIN7JgbeAZUs";
|
||||
|
||||
let parsed_hash = PasswordHash::new(&password_hash).unwrap();
|
||||
let parsed_hash = PasswordHash::new(password_hash).unwrap();
|
||||
assert!(
|
||||
Argon2::default()
|
||||
.verify_password(password, &parsed_hash)
|
||||
@@ -137,7 +138,7 @@ mod tests {
|
||||
|
||||
let request = Request::new("bp", "some-rad-password", "ignore-this", "also-ignore-this");
|
||||
|
||||
assert_eq!(repository.check_auth(&request), true);
|
||||
assert!(repository.check_auth(&request));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -146,7 +147,7 @@ mod tests {
|
||||
|
||||
let request = Request::new("bp", "[id-1]hunter2", "ignore-this", "also-ignore-this");
|
||||
|
||||
assert_eq!(repository.check_auth(&request), true);
|
||||
assert!(repository.check_auth(&request));
|
||||
}
|
||||
|
||||
/// The key [id-1] is present in the test file, but the hash shouldn't match
|
||||
@@ -157,7 +158,7 @@ mod tests {
|
||||
|
||||
let request = Request::new("bp", "[id-1]hunter-bad", "ignore-this", "also-ignore-this");
|
||||
|
||||
assert_eq!(repository.check_auth(&request), false);
|
||||
assert!(!repository.check_auth(&request));
|
||||
}
|
||||
|
||||
/// The key [id-2] is not present in the test file
|
||||
@@ -168,7 +169,7 @@ mod tests {
|
||||
|
||||
let request = Request::new("bp", "[id-2]hunter", "ignore-this", "also-ignore-this");
|
||||
|
||||
assert_eq!(repository.check_auth(&request), false);
|
||||
assert!(!repository.check_auth(&request));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -177,7 +178,7 @@ mod tests {
|
||||
|
||||
let request = Request::new("bp", "not-correct", "ignore-this", "also-ignore-this");
|
||||
|
||||
assert_eq!(repository.check_auth(&request), false);
|
||||
assert!(!repository.check_auth(&request));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -191,7 +192,7 @@ mod tests {
|
||||
"also-ignore-this",
|
||||
);
|
||||
|
||||
assert_eq!(repository.check_auth(&request), false);
|
||||
assert!(!repository.check_auth(&request));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -200,6 +201,6 @@ mod tests {
|
||||
|
||||
let request = Request::new("bp-xxx", "commented-out", "ignore-this", "also-ignore-this");
|
||||
|
||||
assert_eq!(repository.check_auth(&request), false);
|
||||
assert!(!repository.check_auth(&request));
|
||||
}
|
||||
}
|
||||
|
||||
+168
-19
@@ -1,16 +1,42 @@
|
||||
use crate::handler::Handler;
|
||||
use crate::repository::PasswordDirectory;
|
||||
use log::{error, info, warn};
|
||||
use signal_hook::consts::{SIGINT, SIGTERM};
|
||||
use signal_hook::flag;
|
||||
use signal_hook::iterator::Signals;
|
||||
use signal_hook::low_level::signal_name;
|
||||
use std::fs::{metadata, set_permissions};
|
||||
use std::io::Error;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Signals that request a graceful shutdown.
|
||||
const SHUTDOWN_SIGNALS: [i32; 2] = [SIGTERM, SIGINT];
|
||||
|
||||
/// Exit status used when a second shutdown signal forces an immediate exit.
|
||||
const FORCED_EXIT_STATUS: i32 = 1;
|
||||
|
||||
/// Upper bound on how long a single client may take to send its request
|
||||
/// or receive its response.
|
||||
const CLIENT_IO_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// How long to wait for in-flight client handlers to finish on shutdown.
|
||||
const SHUTDOWN_GRACE_PERIOD: Duration = Duration::from_secs(3);
|
||||
|
||||
/// How long to pause after a transient accept() failure (e.g. EMFILE)
|
||||
/// before trying again.
|
||||
const ACCEPT_RETRY_DELAY: Duration = Duration::from_millis(100);
|
||||
|
||||
pub struct Server {
|
||||
listener: UnixListener,
|
||||
socket_file: SocketFile,
|
||||
repository: PasswordDirectory,
|
||||
socket_name: PathBuf,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
@@ -22,40 +48,163 @@ impl Server {
|
||||
}
|
||||
|
||||
let listener = UnixListener::bind(&socket_name)?;
|
||||
let socket_file = SocketFile(socket_name.clone());
|
||||
|
||||
let mut perms = metadata(&socket_name)?.permissions();
|
||||
perms.set_mode(0o0777);
|
||||
set_permissions(&socket_name, perms)?;
|
||||
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// A second SIGTERM/SIGINT while already shutting down exits immediately.
|
||||
// Registered first so it runs before the handler that sets the flag.
|
||||
for signal in SHUTDOWN_SIGNALS {
|
||||
flag::register_conditional_shutdown(signal, FORCED_EXIT_STATUS, Arc::clone(&shutdown))?;
|
||||
}
|
||||
|
||||
// The first SIGTERM/SIGINT sets the flag, then wakes the blocking
|
||||
// accept() with a throwaway connection to the socket.
|
||||
let mut signals = Signals::new(SHUTDOWN_SIGNALS)?;
|
||||
let wake = (Arc::clone(&shutdown), socket_name);
|
||||
thread::spawn(move || {
|
||||
if let Some(signal) = signals.forever().next() {
|
||||
let name = signal_name(signal).unwrap_or("signal");
|
||||
info!("received {name}, shutting down");
|
||||
wake.0.store(true, Ordering::SeqCst);
|
||||
if let Err(err) = UnixStream::connect(&wake.1) {
|
||||
warn!("could not wake the accept loop: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
info!("listening on {}", socket_file.0.display());
|
||||
|
||||
Ok(Server {
|
||||
repository,
|
||||
listener,
|
||||
socket_name,
|
||||
socket_file,
|
||||
repository,
|
||||
shutdown,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<(), Error> {
|
||||
// accept connections and process them, spawning a new thread for each one
|
||||
for stream in self.listener.incoming() {
|
||||
match stream {
|
||||
Ok(stream) => {
|
||||
/* connection succeeded */
|
||||
let repository = self.repository.clone();
|
||||
thread::spawn(|| Handler::new(stream, repository).handle_client());
|
||||
pub fn run(self) -> Result<(), Error> {
|
||||
let Server {
|
||||
listener,
|
||||
socket_file,
|
||||
repository,
|
||||
shutdown,
|
||||
} = self;
|
||||
|
||||
let in_flight = Arc::new(InFlight::default());
|
||||
|
||||
// accept connections and process them, spawning a new thread for each one,
|
||||
// until a shutdown signal arrives
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _addr)) => {
|
||||
if shutdown.load(Ordering::SeqCst) {
|
||||
// Either the wake-up connection from the signal thread or a
|
||||
// client that raced the shutdown; either way, stop here.
|
||||
break;
|
||||
}
|
||||
if let Err(err) = configure_stream(&stream) {
|
||||
error!("could not configure client connection: {err}");
|
||||
continue;
|
||||
}
|
||||
let repository = repository.clone();
|
||||
let guard = in_flight.start();
|
||||
thread::spawn(move || {
|
||||
let _guard = guard;
|
||||
Handler::new(stream, repository).handle_client()
|
||||
});
|
||||
}
|
||||
Err(_err) => {
|
||||
/* connection failed */
|
||||
break;
|
||||
Err(err) if err.kind() == ErrorKind::Interrupted => {}
|
||||
Err(err) => {
|
||||
// Transient failures (EMFILE, ECONNABORTED, ...) should not
|
||||
// take the daemon down; pause briefly and keep serving.
|
||||
error!("accept failed: {err}");
|
||||
if shutdown.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
thread::sleep(ACCEPT_RETRY_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop accepting and remove the socket path right away, so clients that
|
||||
// connect from here on fail fast instead of queueing behind the shutdown.
|
||||
drop(listener);
|
||||
drop(socket_file);
|
||||
|
||||
in_flight.wait_until_idle(SHUTDOWN_GRACE_PERIOD);
|
||||
info!("shutdown complete");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
/// Put an accepted stream into blocking mode with bounded I/O, so a client that
|
||||
/// stalls cannot pin a handler thread forever.
|
||||
fn configure_stream(stream: &UnixStream) -> Result<(), Error> {
|
||||
// On some platforms (e.g. macOS) an accepted stream can inherit the
|
||||
// listener's flags; the handler expects blocking I/O.
|
||||
stream.set_nonblocking(false)?;
|
||||
stream.set_read_timeout(Some(CLIENT_IO_TIMEOUT))?;
|
||||
stream.set_write_timeout(Some(CLIENT_IO_TIMEOUT))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Owns the socket path on disk; removes it when dropped.
|
||||
struct SocketFile(PathBuf);
|
||||
|
||||
impl Drop for SocketFile {
|
||||
fn drop(&mut self) {
|
||||
let _result = std::fs::remove_file(&self.socket_name);
|
||||
println!("Shutting down");
|
||||
let _result = std::fs::remove_file(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts client handlers that are still running.
|
||||
#[derive(Default)]
|
||||
struct InFlight {
|
||||
count: Mutex<usize>,
|
||||
idle: Condvar,
|
||||
}
|
||||
|
||||
impl InFlight {
|
||||
fn start(self: &Arc<Self>) -> InFlightGuard {
|
||||
*self.count.lock().unwrap() += 1;
|
||||
InFlightGuard(Arc::clone(self))
|
||||
}
|
||||
|
||||
fn wait_until_idle(&self, timeout: Duration) {
|
||||
let mut count = self.count.lock().unwrap();
|
||||
if *count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
info!("waiting for {} in-flight request(s) to finish", *count);
|
||||
let deadline = Instant::now() + timeout;
|
||||
|
||||
while *count > 0 {
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
warn!("abandoning {} request(s) still in flight after {timeout:?}", *count);
|
||||
break;
|
||||
}
|
||||
count = self.idle.wait_timeout(count, remaining).unwrap().0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrements the in-flight count when the handler thread finishes, even on panic.
|
||||
struct InFlightGuard(Arc<InFlight>);
|
||||
|
||||
impl Drop for InFlightGuard {
|
||||
fn drop(&mut self) {
|
||||
let mut count = self.0.count.lock().unwrap();
|
||||
*count -= 1;
|
||||
if *count == 0 {
|
||||
self.0.idle.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
use common::io::read_string;
|
||||
use common::request::Request;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Idle shutdown takes milliseconds; anything approaching the daemon's 3s
|
||||
/// grace period means it is waiting on something it should not be.
|
||||
const EXIT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
const PASSWORD_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../test_passwords");
|
||||
|
||||
fn temp_socket_path(tag: &str) -> PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(format!("saslauthd-{}-{}.sock", tag, std::process::id()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
path
|
||||
}
|
||||
|
||||
/// A spawned daemon that is killed if the test panics before stopping it.
|
||||
struct Daemon(Child);
|
||||
|
||||
impl Drop for Daemon {
|
||||
fn drop(&mut self) {
|
||||
if self.0.try_wait().ok().flatten().is_none() {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_socket(path: &PathBuf) -> bool {
|
||||
std::fs::metadata(path)
|
||||
.map(|m| m.file_type().is_socket())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn spawn_daemon(socket: &PathBuf) -> Daemon {
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_saslauthd"))
|
||||
.arg("--socket-name")
|
||||
.arg(socket)
|
||||
.arg("--password-dir")
|
||||
.arg(PASSWORD_DIR)
|
||||
.spawn()
|
||||
.expect("failed to spawn saslauthd");
|
||||
// Wrap immediately so a failed startup below still kills the process.
|
||||
let daemon = Daemon(child);
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !is_socket(socket) {
|
||||
assert!(Instant::now() < deadline, "daemon never created {socket:?}");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
daemon
|
||||
}
|
||||
|
||||
fn send_signal(daemon: &Daemon, signal: &str) {
|
||||
let child = &daemon.0;
|
||||
let status = Command::new("kill")
|
||||
.arg(format!("-{signal}"))
|
||||
.arg(child.id().to_string())
|
||||
.status()
|
||||
.expect("failed to run kill");
|
||||
assert!(status.success(), "kill -{signal} failed");
|
||||
}
|
||||
|
||||
fn wait_for_exit(daemon: &mut Daemon, timeout: Duration) -> std::process::ExitStatus {
|
||||
let child = &mut daemon.0;
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
if let Some(status) = child.try_wait().expect("try_wait failed") {
|
||||
return status;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
panic!("daemon did not exit within {timeout:?}");
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
fn authenticate(socket: &PathBuf, password: &str) -> String {
|
||||
let mut stream = UnixStream::connect(socket).expect("connect to daemon");
|
||||
Request::new("bp", password, "imap", "")
|
||||
.write_to_stream(&mut stream)
|
||||
.expect("write request");
|
||||
read_string(&mut stream).expect("read response")
|
||||
}
|
||||
|
||||
fn assert_clean_shutdown(signal: &str) {
|
||||
let socket = temp_socket_path(signal);
|
||||
let mut daemon = spawn_daemon(&socket);
|
||||
|
||||
// Make sure the daemon actually works before we shut it down.
|
||||
assert_eq!(authenticate(&socket, "some-rad-password"), "OK");
|
||||
assert_eq!(authenticate(&socket, "wrong-password"), "NO");
|
||||
|
||||
send_signal(&daemon, signal);
|
||||
let status = wait_for_exit(&mut daemon, EXIT_TIMEOUT);
|
||||
|
||||
assert!(status.success(), "expected exit status 0 on {signal}, got {status}");
|
||||
assert!(!socket.exists(), "socket file {socket:?} was left behind");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_shuts_down_cleanly_on_sigterm() {
|
||||
assert_clean_shutdown("TERM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_shuts_down_cleanly_on_sigint() {
|
||||
assert_clean_shutdown("INT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_recreates_a_stale_socket_on_startup() {
|
||||
let socket = temp_socket_path("stale");
|
||||
std::fs::write(&socket, b"stale").expect("create stale socket file");
|
||||
|
||||
let mut daemon = spawn_daemon(&socket);
|
||||
assert_eq!(authenticate(&socket, "some-rad-password"), "OK");
|
||||
|
||||
send_signal(&daemon, "TERM");
|
||||
let status = wait_for_exit(&mut daemon, EXIT_TIMEOUT);
|
||||
assert!(status.success());
|
||||
assert!(!socket.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_wait_forever_for_a_silent_client() {
|
||||
let socket = temp_socket_path("silent");
|
||||
let mut daemon = spawn_daemon(&socket);
|
||||
|
||||
// Connect but never send a request, leaving a handler blocked on read.
|
||||
let _silent = UnixStream::connect(&socket).expect("connect to daemon");
|
||||
|
||||
send_signal(&daemon, "TERM");
|
||||
// Must finish once the grace period (3s) expires, whatever the client does.
|
||||
let status = wait_for_exit(&mut daemon, Duration::from_secs(5));
|
||||
assert!(status.success());
|
||||
assert!(!socket.exists());
|
||||
}
|
||||
Reference in New Issue
Block a user