mirror of
https://github.com/mrvasil/ctf-ad-mirror-tool-rust.git
synced 2026-10-24 21:49:27 +00:00
Bind panel on all IPv4 interfaces and support TOML password
This commit is contained in:
parent
e7e33c1a32
commit
3674534985
2
.github/workflows/linux-release.yml
vendored
2
.github/workflows/linux-release.yml
vendored
@ -53,4 +53,4 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$RELEASE_TAG" release/* --verify-tag --title "Mirror Tool $RELEASE_TAG — Linux x64" --notes 'Ready-to-run static Linux x64 package. Extract and run ./run.sh. First launch creates a private random panel password and an empty configuration. No Rust, Python, Node, glibc or external SQLite required. See included README.md. Flag substitution is disabled by default.'
|
||||
gh release create "$RELEASE_TAG" release/* --verify-tag --title "Mirror Tool $RELEASE_TAG — Linux x64" --notes 'Ready-to-run static Linux x64 package. Extract and run ./run.sh. First launch creates mirror.toml with listen=0.0.0.0:6700 and password=mirror-tool-change-me. Change the password in the config before shared-network use. Existing configs are preserved. No Rust, Python, Node, glibc or external SQLite required. See included README.md. Flag substitution is disabled by default.'
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -921,7 +921,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mirror-tool"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mirror-tool"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
description = "Async TCP traffic mirror and ForcAD-aware upstream router"
|
||||
|
||||
25
README.md
25
README.md
@ -18,11 +18,19 @@ cd mirror-tool-linux-x64
|
||||
```
|
||||
|
||||
Статический musl-бинарник не требует установленной glibc, Rust, Python, Node,
|
||||
Docker или отдельной SQLite. Скрипт создаст пустой `mirror.toml` и случайный пароль
|
||||
в `data/panel-password` (0600); повторный запуск сохраняет настройки и пароль.
|
||||
Панель — `http://127.0.0.1:6700`, пароль прочитайте в другом терминале командой
|
||||
`cat data/panel-password`. Команды/сервисы добавляются через панель; подмена флагов
|
||||
выключена. [Полная инструкция архива](deploy/QUICKSTART.md).
|
||||
Docker или отдельной SQLite. Скрипт создаст `mirror.toml` (0600) с настройками:
|
||||
|
||||
```toml
|
||||
[web]
|
||||
listen = "0.0.0.0:6700"
|
||||
password = "mirror-tool-change-me"
|
||||
state_file = "data/state.json"
|
||||
```
|
||||
|
||||
Панель — `http://IP-СЕРВЕРА:6700`. Пароль меняется в TOML, затем процесс нужно
|
||||
перезапустить. Перед использованием в общей сети замените стандартный пароль.
|
||||
Повторный запуск сохраняет конфиг и состояние. Команды/сервисы добавляются через
|
||||
панель; подмена флагов выключена. [Инструкция архива](deploy/QUICKSTART.md).
|
||||
|
||||
GitHub Actions проверяет musl-тесты, статическую линковку и запуск архива с TCP,
|
||||
API и перезапуском; push тега `v*` публикует архив и SHA256SUMS в Releases.
|
||||
@ -36,13 +44,12 @@ API и перезапуском; push тега `v*` публикует архи
|
||||
cargo build --release --locked
|
||||
cp mirror.example.toml mirror.toml
|
||||
# Отредактируйте команды, адреса и порты. Пример использует тестовую сеть RFC 5737.
|
||||
read -s MIRROR_PASSWORD
|
||||
export MIRROR_PASSWORD
|
||||
# Укажите свой пароль в [web] password в mirror.toml.
|
||||
./target/release/mirror-tool --config mirror.toml check
|
||||
./target/release/mirror-tool --config mirror.toml
|
||||
```
|
||||
|
||||
Пароль должен содержать минимум 12 символов. Панель по умолчанию доступна на `http://127.0.0.1:6700`. Для удалённого доступа используйте VPN/SSH-туннель либо HTTPS reverse proxy; при HTTPS установите `secure_cookie = true`. TLS для управляющей панели внутри бинарника не реализован. Переменные окружения с паролем и конфигурацию игры не коммитьте.
|
||||
Пароль должен содержать минимум 12 символов. Панель по умолчанию слушает `0.0.0.0:6700`, адрес в браузере — `http://IP-СЕРВЕРА:6700`. При HTTPS установите `secure_cookie = true`. TLS для управляющей панели внутри бинарника не реализован. Переменные окружения с паролем и конфигурацию игры не коммитьте.
|
||||
|
||||
Панель сохраняет оформление старого `mirror_tool`: горизонтальные колонки сервисов, список команд, кружок primary, выбор реплики нажатием на строку, робот авто режима, открытие сервиса и переключатель запуска. При выборе нового primary прежний становится репликой. Кнопка «+» открывает прежнюю форму: название, порт и необязательный IPv4/IPv6. Предварительно создавать команду для произвольного IP не нужно. У custom-прокси остаются поле IP и удаление в самой колонке.
|
||||
|
||||
@ -115,7 +122,7 @@ Stop отключает приём новых соединений, сохран
|
||||
|
||||
Запись состояния: временный файл на той же ФС → fsync → atomic rename; права `0600`. Сбой сохранения вызывает попытку отката runtime. Публикация topology сериализована с ручными и автоматическими изменениями. Runtime и ФС не образуют распределённую транзакцию: клиент, принятый в коротком окне до неудачного сохранения, может сохранить временно применённый маршрут. Необходимость отката выводится как ошибка. После допуска к транзакции она завершается независимо от HTTP-соединения: закрытие вкладки не оставляет сохранение/очистку наполовину. Если ответ потерян, обновите snapshot перед повтором — изменение могло сохраниться.
|
||||
|
||||
Старый `config.py` не выполняется и не импортируется. Перенесите `TEAMS` в `[[teams]]`, `TASKS` в `[[services]]`, ForcAD URL в `[scoreboard]`, `MAIN_TEAM_IP` в верхнеуровневый `main_team_ip`. `PASSWORD` заменён переменной окружения, `SECRET_KEY` не нужен: сервер хранит случайные сессии в памяти. Старые cookie после перезапуска недействительны. API описан в [docs/api.md](docs/api.md).
|
||||
Старый `config.py` не выполняется и не импортируется. Перенесите `TEAMS` в `[[teams]]`, `TASKS` в `[[services]]`, ForcAD URL в `[scoreboard]`, `MAIN_TEAM_IP` в верхнеуровневый `main_team_ip`. `PASSWORD` переносится в `[web] password` (если поле отсутствует, используется `MIRROR_PASSWORD`), `SECRET_KEY` не нужен: сервер хранит случайные сессии в памяти. Старые cookie после перезапуска недействительны. API описан в [docs/api.md](docs/api.md).
|
||||
|
||||
Для Linux есть [systemd unit](deploy/mirror-tool.service): создайте отдельного пользователя `mirror-tool`, разместите бинарник в `/usr/local/bin/`, конфиг и закрытый `mirror.env` в `/etc/mirror-tool/`; в TOML задайте `state_file = "/var/lib/mirror-tool/state.json"`. Unit даёт только CAP_NET_BIND_SERVICE для портов ниже 1024; при ненужности удалите capability. При увеличении drain timeout увеличьте `TimeoutStopSec`. Unit поставляется как шаблон, автоматически не устанавливается.
|
||||
|
||||
|
||||
3
agent.md
3
agent.md
@ -50,3 +50,6 @@ User explicitly requires optional substitution, default off. Implemented per-ser
|
||||
User wants a ready-to-run Linux x64 build on GitHub, exact distribution unknown. Package static musl binary with run.sh, empty first-run config and random persistent private password; do not package runtime data or enable substitution. Workflow verifies native Clippy/fmt and musl tests plus archive login/API/TCP/restart smoke, then tag releases publish privately. Main push artifacts do not replace releases. Actual CI/package verification pending; docs in deploy/QUICKSTART.md.
|
||||
|
||||
Portable release completed: v0.1.0 at source f264f8a, private GitHub Release includes static musl Linux x64 tar.gz + SHA256SUMS. CI40011983417 succeeded: fmt/Clippy,50 musl tests, static linkage check, release build and packaged first-run/login/API/restart/256KiB TCP smoke. Downloaded release independently passed SHA256, full package smoke in Orb Ubuntu amd64 and launcher/HTTP/graceful-stop in isolated Alpine3.22 without glibc. Native49 tests/fmt/Clippy also passed. No runtime/compiler/database dependencies beyond normal shell utilities for run.sh. First-run config is empty; real targets still require operator setup. Live demo unchanged. See docs/linux-release-verification.md.
|
||||
|
||||
## v0.1.1 startup correction — in progress
|
||||
User explicitly requires panel bind0.0.0.0 and default password in TOML. Native default, example and first-run launcher now bind0.0.0.0:6700; launcher/example explicitly set web.password to a public placeholder. Explicit config password wins; absent field uses legacy environment, and run.sh preserves v0.1.0 password-file fallback. Existing configs are never overwritten. WebConfig password is excluded from serialization and redacted Debug. Native50 tests/fmt/Clippy passed; tag release/package verification pending.
|
||||
|
||||
@ -6,24 +6,26 @@ cd mirror-tool-linux-x64
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Панель: http://127.0.0.1:6700. В другом терминале из этой папки:
|
||||
Панель слушает все IPv4-интерфейсы: **http://IP-СЕРВЕРА:6700**.
|
||||
Пароль по умолчанию: **mirror-tool-change-me**. Настройки — в `mirror.toml`:
|
||||
|
||||
```sh
|
||||
cat data/panel-password
|
||||
```toml
|
||||
[web]
|
||||
listen = "0.0.0.0:6700"
|
||||
password = "mirror-tool-change-me"
|
||||
state_file = "data/state.json"
|
||||
```
|
||||
|
||||
Пароль генерируется случайно при первом запуске и сохраняется с правами0600.
|
||||
Можно передать собственный пароль через MIRROR_PASSWORD (не менее12 символов).
|
||||
Пароль не выводится в логи. Запускать от root не требуется для портов выше1023.
|
||||
Измените `password` и перезапустите процесс. Минимум12 символов; перед
|
||||
использованием в общей сети замените стандартное значение. Созданный конфиг
|
||||
имеет права0600. Пароль не выводится в логи/API. Если `password` не указан,
|
||||
используется переменная MIRROR_PASSWORD; старый run.sh-пароль из
|
||||
`data/panel-password` также подхватывается. Явный `web.password` имеет приоритет.
|
||||
Запускать от root не требуется для портов выше1023.
|
||||
|
||||
Для удалённого сервера откройте туннель со своего компьютера:
|
||||
|
||||
```sh
|
||||
ssh -N -L 6700:127.0.0.1:6700 user@server
|
||||
```
|
||||
|
||||
Затем откройте http://127.0.0.1:6700 на своём компьютере. Панель по умолчанию
|
||||
доступна только локально; не публикуйте её HTTP-порт в игровой сети.
|
||||
Обновление с v0.1.0 не перезаписывает ваш конфиг: для доступа по IP измените
|
||||
существующее `[web] listen` на `"0.0.0.0:6700"` и задайте `password` в той же секции.
|
||||
Для локального доступа можно явно оставить `"127.0.0.1:6700"`.
|
||||
|
||||
Первый запуск создаёт пустой mirror.toml. Добавьте команды и прокси в панели;
|
||||
нажмите название сервиса для настройки слушателя и upstream-порта. Выберите
|
||||
@ -46,7 +48,7 @@ mirror.example.toml содержит полный пример. Для ForcAD д
|
||||
|
||||
Бинарник статический (musl), базовая архитектура x86-64, без требования AVX.
|
||||
Не нужны glibc, Rust, Python, Node, Docker или отдельная SQLite. Для run.sh нужны
|
||||
обычные /bin/sh, uname, dirname, mkdir, od, tr, cat и /dev/urandom. Для распаковки
|
||||
обычные /bin/sh, uname, dirname, mkdir и cat. Для распаковки
|
||||
нужен tar с gzip. Linux kernel3.2+ — базовый минимум target Rust; конкретная старая
|
||||
система не считается проверенной только по номеру ядра. Ограничения контейнера,
|
||||
seccomp, права, доступная память, файловые дескрипторы и сеть всё равно действуют.
|
||||
|
||||
@ -13,22 +13,16 @@ if [ ! -e mirror.toml ]; then
|
||||
# Add teams/services in the panel or use mirror.example.toml as a reference.
|
||||
# Existing data/state.json takes precedence over teams/services in this file.
|
||||
[web]
|
||||
listen = "127.0.0.1:6700"
|
||||
listen = "0.0.0.0:6700"
|
||||
password = "mirror-tool-change-me"
|
||||
state_file = "data/state.json"
|
||||
CONFIG
|
||||
)
|
||||
fi
|
||||
if [ -z "${MIRROR_PASSWORD:-}" ]; then
|
||||
if [ ! -e data/panel-password ]; then
|
||||
(set -C; od -An -N24 -tx1 /dev/urandom | tr -d ' \n' > data/panel-password)
|
||||
fi
|
||||
# Compatibility with v0.1.0: explicit web.password takes priority over this.
|
||||
if [ -z "${MIRROR_PASSWORD:-}" ] && [ -f data/panel-password ]; then
|
||||
MIRROR_PASSWORD=$(cat data/panel-password)
|
||||
if [ "${#MIRROR_PASSWORD}" -lt 12 ]; then
|
||||
echo 'data/panel-password is invalid; restore it or supply MIRROR_PASSWORD.' >&2
|
||||
exit 1
|
||||
fi
|
||||
export MIRROR_PASSWORD
|
||||
echo 'Panel password: read data/panel-password (kept across restarts).'
|
||||
fi
|
||||
echo 'Config: mirror.toml. Default panel: http://127.0.0.1:6700 (see web.listen).'
|
||||
echo 'Config/password: mirror.toml [web]. Default listener: 0.0.0.0:6700; open http://SERVER-IP:6700.'
|
||||
exec ./mirror-tool --config mirror.toml "$@"
|
||||
|
||||
@ -3,6 +3,10 @@
|
||||
JSON API, UTF-8. Authenticated sessions use an HttpOnly SameSite=Strict cookie. Mutating authenticated requests require `X-CSRF-Token` from login or session response. Browser requests must be same-origin. No secrets returned in snapshots. Application-handled failures return `{ "error": "human readable explanation" }` with non-2xx status. Framework routing errors (for example 405) may have a plain-text body; clients must tolerate this. A snapshot is the source of truth; a 2xx response confirms completion. After admission under the serialized operation lock, topology mutations run independently of the HTTP connection through commit/rollback and cleanup. A lost response does not imply an uncommitted change: refresh the snapshot before retrying. Requests cancelled while waiting for admission make no change. Shutdown rejects new admissions and waits for admitted work within the documented process deadline.
|
||||
|
||||
## Routes
|
||||
Panel credentials come from TOML `web.password` when present; otherwise the
|
||||
configured `web.password_env` is used. They are never exposed in snapshots or
|
||||
serialized WebConfig diagnostics. Changing the password requires process restart.
|
||||
|
||||
- `GET /` — embedded UI shell (login shown until session validated).
|
||||
- `GET /licenses.txt` — licenses and attribution for the bundled Bootstrap/Font Awesome assets.
|
||||
- `GET /healthz` — process readiness `{status:"ok"}` (not upstream checker health).
|
||||
|
||||
@ -57,8 +57,10 @@ state-файл в резервную копию и запустите проце
|
||||
сервиса относится к своей команде (`main_team_ip`), а не к выбранному upstream.
|
||||
Серый статус означает отсутствие свежих данных, а не успех checker.
|
||||
|
||||
Пароль обычного процесса задаётся в `MIRROR_PASSWORD` (минимум 12 символов),
|
||||
не в TOML. Тестовый fixture для настоящей игры не используйте; запускайте бинарник
|
||||
Пароль задаётся в `[web] password` в TOML (минимум12 символов). Стартовый
|
||||
шаблон использует `mirror-tool-change-me`, измените его перед игрой. Если поле
|
||||
отсутствует, используется `MIRROR_PASSWORD`. По умолчанию панель слушает
|
||||
`0.0.0.0:6700`, в браузере открывайте `http://IP-СЕРВЕРА:6700`. Тестовый fixture для настоящей игры не используйте; запускайте бинарник
|
||||
напрямую с собственным конфигом и паролем. Реальная игра этой проверкой не затрагивалась.
|
||||
|
||||
## Повторяемое локальное демо
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
# Copy to mirror.toml. Set MIRROR_PASSWORD in your environment (12+ characters).
|
||||
# Copy to mirror.toml. Change web.password before using on a shared network.
|
||||
# Bind the panel to a VPN interface or put it behind HTTPS when used remotely.
|
||||
# Optional: own team's checker status in the top strip, as in the legacy UI.
|
||||
# main_team_ip = "192.0.2.10"
|
||||
|
||||
[web]
|
||||
listen = "127.0.0.1:6700"
|
||||
listen = "0.0.0.0:6700"
|
||||
password = "mirror-tool-change-me"
|
||||
password_env = "MIRROR_PASSWORD"
|
||||
state_file = "data/state.json"
|
||||
session_ttl_seconds = 43200
|
||||
|
||||
@ -29,13 +29,17 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary:
|
||||
env = os.environ.copy()
|
||||
env.pop("MIRROR_PASSWORD", None)
|
||||
subprocess.run([str(package / "run.sh"), "check"], cwd=root, env=env, check=True)
|
||||
password_path = package / "data/panel-password"
|
||||
password = password_path.read_text()
|
||||
assert len(password) == 48
|
||||
assert stat.S_IMODE(password_path.stat().st_mode) == 0o600
|
||||
config = package / "mirror.toml"
|
||||
password = "mirror-tool-change-me"
|
||||
assert f'password = "{password}"' in config.read_text()
|
||||
assert 'listen = "0.0.0.0:6700"' in config.read_text()
|
||||
assert stat.S_IMODE(config.stat().st_mode) == 0o600
|
||||
assert not (package / "data/panel-password").exists()
|
||||
# Explicit config must win over a legacy password file during upgrades.
|
||||
legacy_password = "legacy-synthetic-passphrase"
|
||||
(package / "data/panel-password").write_text(legacy_password)
|
||||
web_port = free_port()
|
||||
config.write_text(config.read_text().replace("127.0.0.1:6700", f"127.0.0.1:{web_port}"))
|
||||
config.write_text(config.read_text().replace("0.0.0.0:6700", f"0.0.0.0:{web_port}"))
|
||||
original_config = config.read_bytes()
|
||||
base = f"http://127.0.0.1:{web_port}"
|
||||
upstream = socket.socket()
|
||||
@ -55,7 +59,8 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary:
|
||||
thread = threading.Thread(target=echo, daemon=True)
|
||||
thread.start()
|
||||
proxy_port = free_port()
|
||||
for restart in (False, True):
|
||||
for phase in range(3):
|
||||
restart = phase > 0
|
||||
with (root / "server.log").open("wb") as log:
|
||||
process = subprocess.Popen([str(package / "run.sh")], cwd=root, env=env, stdout=log, stderr=log)
|
||||
try:
|
||||
@ -71,6 +76,7 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise RuntimeError("packaged server did not become ready")
|
||||
assert any(line.split()[1] == f"00000000:{web_port:04X}" and line.split()[3] == "0A" for line in Path("/proc/net/tcp").read_text().splitlines()[1:])
|
||||
|
||||
def request(path, value=None, method=None, csrf=None):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
@ -99,9 +105,10 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary:
|
||||
assert not thread.is_alive()
|
||||
assert reply == received == payload
|
||||
snapshot = request("/api/snapshot")
|
||||
assert password not in json.dumps(snapshot)
|
||||
assert snapshot["services"][0]["flag_protection"] is False
|
||||
assert snapshot["services"][0]["enabled"] is True
|
||||
assert password_path.read_text() == password
|
||||
assert (package / "data/panel-password").read_text() == legacy_password
|
||||
assert config.read_bytes() == original_config
|
||||
finally:
|
||||
process.terminate()
|
||||
@ -112,5 +119,14 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary:
|
||||
process.wait()
|
||||
raise
|
||||
assert process.returncode == 0
|
||||
if not restart:
|
||||
password = "updated-config-passphrase"
|
||||
config.write_text(config.read_text().replace("mirror-tool-change-me", password))
|
||||
original_config = config.read_bytes()
|
||||
elif phase == 1:
|
||||
# Old v0.1.0 config without web.password still uses the saved password.
|
||||
config.write_text(config.read_text().replace(f'password = "{password}"\n', ""))
|
||||
password = legacy_password
|
||||
original_config = config.read_bytes()
|
||||
upstream.close()
|
||||
print("Package OK: first start, private password, empty topology, login, API persistence/restart, exact 256KiB TCP half-close, flag substitution off.")
|
||||
print("Package OK: wildcard IPv4 listener, config password/change/restart, legacy-file precedence, empty topology, API persistence, exact 256KiB TCP half-close, flag substitution off.")
|
||||
|
||||
44
src/auth.rs
44
src/auth.rs
@ -46,16 +46,20 @@ fn random_token() -> String {
|
||||
}
|
||||
impl Auth {
|
||||
pub fn from_env(settings: &WebConfig) -> Result<Self> {
|
||||
let password = std::env::var(&settings.password_env).map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"set {} to a password with at least 12 characters",
|
||||
settings.password_env
|
||||
)
|
||||
})?;
|
||||
let password = settings
|
||||
.password
|
||||
.clone()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| std::env::var(&settings.password_env))
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"set web.password in TOML or {} to a password with at least 12 characters",
|
||||
settings.password_env
|
||||
)
|
||||
})?;
|
||||
ensure!(
|
||||
password.len() >= 12,
|
||||
"{} must contain at least 12 characters",
|
||||
settings.password_env
|
||||
"panel password must contain at least 12 characters"
|
||||
);
|
||||
Ok(Self::new(settings.clone(), &password))
|
||||
}
|
||||
@ -181,6 +185,30 @@ pub fn same_origin(headers: &HeaderMap) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn configured_password_authenticates_without_environment_and_is_redacted() {
|
||||
let config: crate::config::Config = toml::from_str(
|
||||
"[web]\npassword = 'configuration-test-passphrase'\npassword_env = 'MIRROR_UNUSED_TEST_ENV'",
|
||||
).unwrap();
|
||||
config.validate().unwrap();
|
||||
let a = Auth::from_env(&config.web).unwrap();
|
||||
let peer = "127.0.0.1".parse().unwrap();
|
||||
assert!(a.login("configuration-test-passphrase", peer).is_ok());
|
||||
assert!(matches!(
|
||||
a.login("incorrect-password", peer),
|
||||
Err(LoginError::Invalid)
|
||||
));
|
||||
assert!(!format!("{:?}", config.web).contains("configuration-test-passphrase"));
|
||||
assert!(
|
||||
!serde_json::to_string(&config.web)
|
||||
.unwrap()
|
||||
.contains("configuration-test-passphrase")
|
||||
);
|
||||
let mut invalid = config;
|
||||
invalid.web.password = Some("short".into());
|
||||
assert!(invalid.validate().is_err());
|
||||
assert!(Auth::from_env(&invalid.web).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn session_csrf_and_revocation() {
|
||||
let a = Auth::new(WebConfig::default(), "test-passphrase");
|
||||
let (cookie, info) = a
|
||||
|
||||
@ -19,16 +19,31 @@ pub struct Config {
|
||||
pub services: Vec<ServiceConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct WebConfig {
|
||||
pub listen: SocketAddr,
|
||||
#[serde(skip_serializing)]
|
||||
pub password: Option<String>,
|
||||
pub password_env: String,
|
||||
pub state_file: PathBuf,
|
||||
pub session_ttl_seconds: u64,
|
||||
pub secure_cookie: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WebConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("WebConfig")
|
||||
.field("listen", &self.listen)
|
||||
.field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
|
||||
.field("password_env", &self.password_env)
|
||||
.field("state_file", &self.state_file)
|
||||
.field("session_ttl_seconds", &self.session_ttl_seconds)
|
||||
.field("secure_cookie", &self.secure_cookie)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct NetworkConfig {
|
||||
@ -117,7 +132,8 @@ pub enum ReplicaPolicy {
|
||||
impl Default for WebConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
listen: "127.0.0.1:6700".parse().unwrap(),
|
||||
listen: "0.0.0.0:6700".parse().unwrap(),
|
||||
password: None,
|
||||
password_env: "MIRROR_PASSWORD".into(),
|
||||
state_file: "data/state.json".into(),
|
||||
session_ttl_seconds: 43200,
|
||||
@ -192,6 +208,10 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure!(
|
||||
self.web.password.as_ref().is_none_or(|p| p.len() >= 12),
|
||||
"web.password must contain at least 12 characters"
|
||||
);
|
||||
ensure!(
|
||||
!self.web.password_env.is_empty(),
|
||||
"password_env must name an environment variable"
|
||||
|
||||
@ -49,3 +49,7 @@ Flag implementation issues recorded: cargo-add combined feature syntax rejected
|
||||
- [complete] Add static x86_64-unknown-linux-musl packaging, first-run launcher and operator quickstart.
|
||||
- [complete] Add pinned GitHub Actions build/test/artifact and tag-triggered release workflow.
|
||||
- [complete] Validate actual archive on Linux, publish private GitHub release and verify downloaded asset.
|
||||
|
||||
## v0.1.1 startup correction
|
||||
- [complete] Wildcard IPv4 panel default and explicit TOML password, preserving existing configs/legacy auth.
|
||||
- [in progress] Publish and validate static Linux release; package tests assert wildcard socket, config password/change/restart and legacy-file fallback.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user