added simple API + PAM authorization
This commit is contained in:
299
Cargo.lock
generated
299
Cargo.lock
generated
@@ -2,6 +2,15 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -35,10 +44,10 @@ dependencies = [
|
||||
"memchr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -117,6 +126,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.10"
|
||||
@@ -126,12 +141,41 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.69.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
"shlex",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
@@ -154,6 +198,15 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -173,24 +226,75 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashcore"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"axum",
|
||||
"base64",
|
||||
"chrono",
|
||||
"hmac",
|
||||
"pam",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -255,12 +359,37 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -376,6 +505,15 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -392,6 +530,18 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
@@ -431,6 +581,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
@@ -442,6 +598,16 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -457,6 +623,40 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pam"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"pam-macros",
|
||||
"pam-sys",
|
||||
"users",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pam-macros"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pam-sys"
|
||||
version = "1.0.0-alpha5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -525,6 +725,41 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
@@ -576,7 +811,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -628,6 +863,17 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -666,6 +912,23 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@@ -708,7 +971,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -759,6 +1022,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -771,6 +1040,22 @@ version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "users"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
@@ -809,7 +1094,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -843,7 +1128,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -854,7 +1139,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -6,7 +6,12 @@ edition = "2021"
|
||||
[dependencies]
|
||||
axum = "0.8.8"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9.34"
|
||||
tokio = { version = "1", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1", features = ["full", "rt-multi-thread", "process"] }
|
||||
askama = "0.14"
|
||||
chrono = "0.4.44"
|
||||
pam = "0.8.0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
base64 = "0.22.1"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
host: "127.0.0.1"
|
||||
port: 8080
|
||||
debug: true
|
||||
auth:
|
||||
secret: "change-me-please"
|
||||
token_ttl_seconds: 3600
|
||||
pam_service: "login"
|
||||
|
||||
85
src/api/auth.rs
Normal file
85
src/api/auth.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use pam::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::auth::{issue_token, AuthState};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct LoginRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct TokenResponse {
|
||||
token: String,
|
||||
token_type: String,
|
||||
expires_in: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
State(state): State<AuthState>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> impl IntoResponse {
|
||||
if payload.username.trim().is_empty() || payload.password.is_empty() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: "username/password required".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let service = state.pam_service.clone();
|
||||
let username = payload.username.clone();
|
||||
let password = payload.password.clone();
|
||||
|
||||
let pam_result = tokio::task::spawn_blocking(move || {
|
||||
let mut client = Client::with_password(&service)
|
||||
.map_err(|e| format!("pam init failed: {}", e))?;
|
||||
client
|
||||
.conversation_mut()
|
||||
.set_credentials(&username, &password);
|
||||
client
|
||||
.authenticate()
|
||||
.map_err(|e| format!("pam auth failed: {}", e))?;
|
||||
Ok::<(), String>(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("pam task failed: {}", e));
|
||||
|
||||
match pam_result {
|
||||
Ok(Ok(())) => {
|
||||
let (token, ttl) = match issue_token(&payload.username, &state) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
Json(TokenResponse {
|
||||
token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: ttl,
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
_ => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ErrorResponse {
|
||||
error: "invalid credentials".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
710
src/api/ip.rs
Normal file
710
src/api/ip.rs
Normal file
@@ -0,0 +1,710 @@
|
||||
use axum::{Json, response::IntoResponse, http::StatusCode, extract::Path};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IpAddrInfo {
|
||||
pub interfaces: Vec<InterfaceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InterfaceInfo {
|
||||
pub name: String,
|
||||
pub index: u32,
|
||||
pub state: String,
|
||||
pub mac_address: Option<String>,
|
||||
pub ipv4_addresses: Vec<Ipv4Address>,
|
||||
pub ipv6_addresses: Vec<Ipv6Address>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Ipv4Address {
|
||||
pub address: String,
|
||||
pub prefix_len: u8,
|
||||
pub scope: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Ipv6Address {
|
||||
pub address: String,
|
||||
pub prefix_len: u8,
|
||||
pub scope: String,
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IpRouteInfo {
|
||||
pub routes: Vec<RouteEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RouteEntry {
|
||||
pub destination: Option<String>,
|
||||
pub gateway: Option<String>,
|
||||
pub device: String,
|
||||
pub metric: Option<u32>,
|
||||
pub route_type: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IpLinkInfo {
|
||||
pub links: Vec<LinkInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LinkInfo {
|
||||
pub index: u32,
|
||||
pub name: String,
|
||||
pub state: String,
|
||||
pub mac_address: Option<String>,
|
||||
pub mtu: u32,
|
||||
pub qdisc: Option<String>,
|
||||
pub link_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct OkResponse {
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum AddrAction {
|
||||
Add,
|
||||
Del,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum RouteAction {
|
||||
Add,
|
||||
Del,
|
||||
Replace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum IpFamily {
|
||||
Inet,
|
||||
Inet6,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct LinkMtuRequest {
|
||||
pub mtu: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct AddrRequest {
|
||||
pub action: AddrAction,
|
||||
pub dev: String,
|
||||
pub address: String,
|
||||
pub prefix_len: u8,
|
||||
pub family: Option<IpFamily>,
|
||||
pub scope: Option<String>,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RouteRequest {
|
||||
pub action: RouteAction,
|
||||
pub destination: String,
|
||||
pub gateway: Option<String>,
|
||||
pub dev: Option<String>,
|
||||
pub metric: Option<u32>,
|
||||
pub table: Option<String>,
|
||||
pub proto: Option<String>,
|
||||
pub scope: Option<String>,
|
||||
}
|
||||
pub async fn get_ip_addr() -> impl IntoResponse {
|
||||
handle_ip("addr").await
|
||||
}
|
||||
|
||||
pub async fn get_ip_route() -> impl IntoResponse {
|
||||
handle_ip("route").await
|
||||
}
|
||||
|
||||
pub async fn get_ip_link() -> impl IntoResponse {
|
||||
handle_ip("link").await
|
||||
}
|
||||
|
||||
pub async fn set_link_up(Path(ifname): Path<String>) -> impl IntoResponse {
|
||||
handle_link_state(&ifname, "up").await
|
||||
}
|
||||
|
||||
pub async fn set_link_down(Path(ifname): Path<String>) -> impl IntoResponse {
|
||||
handle_link_state(&ifname, "down").await
|
||||
}
|
||||
|
||||
pub async fn set_link_mtu(
|
||||
Path(ifname): Path<String>,
|
||||
Json(payload): Json<LinkMtuRequest>,
|
||||
) -> impl IntoResponse {
|
||||
if let Err(e) = validate_ifname(&ifname) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if payload.mtu < 68 || payload.mtu > 65535 {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: "mtu must be between 68 and 65535".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let args = vec![
|
||||
"link".to_string(),
|
||||
"set".to_string(),
|
||||
"dev".to_string(),
|
||||
ifname,
|
||||
"mtu".to_string(),
|
||||
payload.mtu.to_string(),
|
||||
];
|
||||
|
||||
match run_ip_cmd(&args).await {
|
||||
Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn addr_modify(Json(payload): Json<AddrRequest>) -> impl IntoResponse {
|
||||
if let Err(e) = validate_ifname(&payload.dev) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let family = payload.family.unwrap_or(IpFamily::Inet);
|
||||
let max_prefix = match family {
|
||||
IpFamily::Inet => 32,
|
||||
IpFamily::Inet6 => 128,
|
||||
};
|
||||
if payload.prefix_len > max_prefix {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: format!("prefix_len must be <= {}", max_prefix),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
match family {
|
||||
IpFamily::Inet => args.push("-4".to_string()),
|
||||
IpFamily::Inet6 => args.push("-6".to_string()),
|
||||
}
|
||||
args.push("addr".to_string());
|
||||
args.push(match payload.action {
|
||||
AddrAction::Add => "add".to_string(),
|
||||
AddrAction::Del => "del".to_string(),
|
||||
});
|
||||
args.push(format!("{}/{}", payload.address, payload.prefix_len));
|
||||
args.push("dev".to_string());
|
||||
args.push(payload.dev);
|
||||
|
||||
if let Some(scope) = payload.scope {
|
||||
args.push("scope".to_string());
|
||||
args.push(scope);
|
||||
}
|
||||
if let Some(label) = payload.label {
|
||||
args.push("label".to_string());
|
||||
args.push(label);
|
||||
}
|
||||
|
||||
match run_ip_cmd(&args).await {
|
||||
Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn route_modify(Json(payload): Json<RouteRequest>) -> impl IntoResponse {
|
||||
if let Some(dev) = payload.dev.as_ref() {
|
||||
if let Err(e) = validate_ifname(dev) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
if payload.gateway.is_none() && payload.dev.is_none() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: "either gateway or dev must be provided".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
args.push("route".to_string());
|
||||
args.push(match payload.action {
|
||||
RouteAction::Add => "add".to_string(),
|
||||
RouteAction::Del => "del".to_string(),
|
||||
RouteAction::Replace => "replace".to_string(),
|
||||
});
|
||||
args.push(payload.destination);
|
||||
|
||||
if let Some(gw) = payload.gateway {
|
||||
args.push("via".to_string());
|
||||
args.push(gw);
|
||||
}
|
||||
if let Some(dev) = payload.dev {
|
||||
args.push("dev".to_string());
|
||||
args.push(dev);
|
||||
}
|
||||
if let Some(metric) = payload.metric {
|
||||
args.push("metric".to_string());
|
||||
args.push(metric.to_string());
|
||||
}
|
||||
if let Some(table) = payload.table {
|
||||
args.push("table".to_string());
|
||||
args.push(table);
|
||||
}
|
||||
if let Some(proto) = payload.proto {
|
||||
args.push("proto".to_string());
|
||||
args.push(proto);
|
||||
}
|
||||
if let Some(scope) = payload.scope {
|
||||
args.push("scope".to_string());
|
||||
args.push(scope);
|
||||
}
|
||||
|
||||
match run_ip_cmd(&args).await {
|
||||
Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ip(subcmd: &str) -> impl IntoResponse {
|
||||
match execute_ip_cmd(subcmd).await {
|
||||
Ok(data) => Json(data).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_link_state(ifname: &str, state: &str) -> impl IntoResponse {
|
||||
if let Err(e) = validate_ifname(ifname) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let args = vec![
|
||||
"link".to_string(),
|
||||
"set".to_string(),
|
||||
"dev".to_string(),
|
||||
ifname.to_string(),
|
||||
state.to_string(),
|
||||
];
|
||||
|
||||
match run_ip_cmd(&args).await {
|
||||
Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_ip_cmd(args: &[String]) -> Result<(), String> {
|
||||
let output = Command::new("ip")
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to execute ip {}: {}", args.join(" "), e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("ip {} failed: {}", args.join(" "), stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ifname(name: &str) -> Result<(), String> {
|
||||
if name.is_empty() {
|
||||
return Err("ifname must not be empty".to_string());
|
||||
}
|
||||
if name.len() > 64 {
|
||||
return Err("ifname is слишком длинное".to_string());
|
||||
}
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | ':' ))
|
||||
{
|
||||
return Err("ifname содержит недопустимые символы".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_ip_cmd(subcmd: &str) -> Result<serde_json::Value, String> {
|
||||
// Prefer native JSON output from iproute2 for a complete and robust payload.
|
||||
let mut json_errors: Vec<String> = Vec::new();
|
||||
|
||||
match execute_ip_cmd_json(subcmd, true).await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(e) => json_errors.push(e),
|
||||
}
|
||||
|
||||
// Some ip versions support JSON but not -details.
|
||||
match execute_ip_cmd_json(subcmd, false).await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(e) => json_errors.push(e),
|
||||
}
|
||||
|
||||
// Fallback to legacy text parsing for older ip versions without JSON support.
|
||||
match execute_ip_cmd_legacy(subcmd).await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(legacy_err) => {
|
||||
let mut msg = String::new();
|
||||
if !json_errors.is_empty() {
|
||||
msg.push_str(&format!("JSON attempts failed: {}; ", json_errors.join(" | ")));
|
||||
}
|
||||
msg.push_str(&format!("legacy parsing failed: {}", legacy_err));
|
||||
Err(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_ip_cmd_json(subcmd: &str, details: bool) -> Result<serde_json::Value, String> {
|
||||
let mut args: Vec<&str> = vec!["-j"];
|
||||
if details {
|
||||
args.push("-details");
|
||||
}
|
||||
args.push(subcmd);
|
||||
|
||||
let output = Command::new("ip")
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to execute ip -j {}: {}", subcmd, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("ip -j {} failed: {}", subcmd, stderr));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
serde_json::from_str::<serde_json::Value>(&stdout)
|
||||
.map_err(|e| format!("Failed to parse ip -j {} output: {}", subcmd, e))
|
||||
}
|
||||
|
||||
async fn execute_ip_cmd_legacy(subcmd: &str) -> Result<serde_json::Value, String> {
|
||||
let output = Command::new("ip")
|
||||
.arg(subcmd)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to execute ip {}: {}", subcmd, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("ip {} failed: {}", subcmd, stderr));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let result = match subcmd {
|
||||
"addr" => parse_ip_addr(&stdout)?,
|
||||
"route" => parse_ip_route(&stdout)?,
|
||||
"link" => parse_ip_link(&stdout)?,
|
||||
_ => return Err(format!("Unknown subcommand: {}", subcmd)),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn parse_ip_addr(output: &str) -> Result<serde_json::Value, String> {
|
||||
let mut interfaces = Vec::new();
|
||||
let mut current_interface: Option<InterfaceInfo> = None;
|
||||
|
||||
for line in output.lines() {
|
||||
let line = line.trim();
|
||||
|
||||
// Начало описания интерфейса: "1: lo: <LOOPBACK> ..."
|
||||
if let Some(idx) = line.split(':').next() {
|
||||
if let Ok(index) = idx.parse::<u32>() {
|
||||
if let Some(iface) = current_interface.take() {
|
||||
interfaces.push(iface);
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split(':').collect();
|
||||
if parts.len() >= 3 {
|
||||
let name = parts[1].trim().trim_start_matches('@').to_string();
|
||||
let state = if line.contains("<UP>") || line.contains("UP") {
|
||||
"UP"
|
||||
} else if line.contains("DOWN") {
|
||||
"DOWN"
|
||||
} else {
|
||||
"UNKNOWN"
|
||||
}.to_string();
|
||||
|
||||
current_interface = Some(InterfaceInfo {
|
||||
name,
|
||||
index,
|
||||
state,
|
||||
mac_address: None,
|
||||
ipv4_addresses: Vec::new(),
|
||||
ipv6_addresses: Vec::new(),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref mut iface) = current_interface {
|
||||
// MAC адрес: "link/ether aa:bb:cc:dd:ee:ff"
|
||||
if line.starts_with("link/") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 && parts[1] != "none" {
|
||||
iface.mac_address = Some(parts[1].to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// IPv4 адрес: "inet 192.168.1.1/24 brd ..."
|
||||
if line.starts_with("inet ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let addr_parts: Vec<&str> = parts[1].split('/').collect();
|
||||
if addr_parts.len() == 2 {
|
||||
if let Ok(prefix) = addr_parts[1].parse::<u8>() {
|
||||
let scope = if let Some(idx) = line.find("scope") {
|
||||
line[idx..].split_whitespace().nth(1).unwrap_or("global").to_string()
|
||||
} else {
|
||||
"global".to_string()
|
||||
};
|
||||
iface.ipv4_addresses.push(Ipv4Address {
|
||||
address: addr_parts[0].to_string(),
|
||||
prefix_len: prefix,
|
||||
scope,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// IPv6 адрес: "inet6 ::1/128 scope ..."
|
||||
if line.starts_with("inet6 ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let addr_parts: Vec<&str> = parts[1].split('/').collect();
|
||||
if addr_parts.len() == 2 {
|
||||
if let Ok(prefix) = addr_parts[1].parse::<u8>() {
|
||||
let scope = if let Some(idx) = line.find("scope") {
|
||||
line[idx..].split_whitespace().nth(1).unwrap_or("global").to_string()
|
||||
} else {
|
||||
"global".to_string()
|
||||
};
|
||||
iface.ipv6_addresses.push(Ipv6Address {
|
||||
address: addr_parts[0].to_string(),
|
||||
prefix_len: prefix,
|
||||
scope,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(iface) = current_interface {
|
||||
interfaces.push(iface);
|
||||
}
|
||||
|
||||
Ok(serde_json::to_value(IpAddrInfo { interfaces }).unwrap())
|
||||
}
|
||||
|
||||
fn parse_ip_route(output: &str) -> Result<serde_json::Value, String> {
|
||||
let mut routes = Vec::new();
|
||||
|
||||
for line in output.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
let mut route = RouteEntry {
|
||||
destination: None,
|
||||
gateway: None,
|
||||
device: String::new(),
|
||||
metric: None,
|
||||
route_type: "unicast".to_string(),
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
while i < parts.len() {
|
||||
match parts[i] {
|
||||
"default" => {
|
||||
route.destination = Some("default".to_string());
|
||||
}
|
||||
"via" => {
|
||||
if i + 1 < parts.len() {
|
||||
route.gateway = Some(parts[i + 1].to_string());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"dev" => {
|
||||
if i + 1 < parts.len() {
|
||||
route.device = parts[i + 1].to_string();
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"metric" => {
|
||||
if i + 1 < parts.len() {
|
||||
route.metric = parts[i + 1].parse::<u32>().ok();
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"src" | "proto" | "scope" | "onlink" => {
|
||||
// Пропускаем эти параметры
|
||||
}
|
||||
_ => {
|
||||
if route.destination.is_none() && !["via", "dev", "metric", "src", "proto", "scope", "onlink"].contains(&parts[i]) {
|
||||
route.destination = Some(parts[i].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if !route.device.is_empty() {
|
||||
routes.push(route);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::to_value(IpRouteInfo { routes }).unwrap())
|
||||
}
|
||||
|
||||
fn parse_ip_link(output: &str) -> Result<serde_json::Value, String> {
|
||||
let mut links = Vec::new();
|
||||
let mut current_link: Option<LinkInfo> = None;
|
||||
|
||||
for line in output.lines() {
|
||||
let line = line.trim();
|
||||
|
||||
|
||||
if let Some(idx) = line.split(':').next() {
|
||||
if let Ok(index) = idx.parse::<u32>() {
|
||||
if let Some(link) = current_link.take() {
|
||||
links.push(link);
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split(':').collect();
|
||||
let name = if parts.len() >= 3 {
|
||||
parts[1].trim().trim_start_matches('@').to_string()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
};
|
||||
|
||||
let state = if line.contains("<UP>") || line.contains("UP") {
|
||||
"UP"
|
||||
} else if line.contains("DOWN") {
|
||||
"DOWN"
|
||||
} else {
|
||||
"UNKNOWN"
|
||||
}.to_string();
|
||||
|
||||
current_link = Some(LinkInfo {
|
||||
index,
|
||||
name,
|
||||
state,
|
||||
mac_address: None,
|
||||
mtu: 1500,
|
||||
qdisc: None,
|
||||
link_type: "unknown".to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref mut link) = current_link {
|
||||
// link/ether или link/type
|
||||
if line.starts_with("link/") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
link.link_type = parts[0].trim_start_matches("link/").to_string();
|
||||
if parts[1] != "none" {
|
||||
link.mac_address = Some(parts[1].to_string());
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// mtu
|
||||
if line.starts_with("mtu ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
link.mtu = parts[1].parse::<u32>().unwrap_or(1500);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// qdisc
|
||||
if let Some(idx) = line.find("qdisc ") {
|
||||
let rest = &line[idx + 6..];
|
||||
if let Some(qdisc) = rest.split_whitespace().next() {
|
||||
link.qdisc = Some(qdisc.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link) = current_link {
|
||||
links.push(link);
|
||||
}
|
||||
|
||||
Ok(serde_json::to_value(IpLinkInfo { links }).unwrap())
|
||||
}
|
||||
3
src/api/mod.rs
Normal file
3
src/api/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod ip;
|
||||
pub mod nft;
|
||||
pub mod auth;
|
||||
463
src/api/nft.rs
Normal file
463
src/api/nft.rs
Normal file
@@ -0,0 +1,463 @@
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Stdio;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct OkResponse {
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum NftFamily {
|
||||
Ip,
|
||||
Ip6,
|
||||
Inet,
|
||||
Arp,
|
||||
Bridge,
|
||||
Netdev,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum TableAction {
|
||||
Add,
|
||||
Del,
|
||||
Flush,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum ChainAction {
|
||||
Add,
|
||||
Del,
|
||||
Flush,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum RuleAction {
|
||||
Add,
|
||||
Insert,
|
||||
Del,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct TableRequest {
|
||||
pub action: TableAction,
|
||||
pub family: NftFamily,
|
||||
pub table: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct ChainRequest {
|
||||
pub action: ChainAction,
|
||||
pub family: NftFamily,
|
||||
pub table: String,
|
||||
pub chain: String,
|
||||
#[serde(rename = "type")]
|
||||
pub chain_type: Option<String>,
|
||||
pub hook: Option<String>,
|
||||
pub priority: Option<i32>,
|
||||
pub policy: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RuleRequest {
|
||||
pub action: RuleAction,
|
||||
pub family: NftFamily,
|
||||
pub table: String,
|
||||
pub chain: String,
|
||||
pub expr: Option<String>,
|
||||
pub handle: Option<u64>,
|
||||
pub position: Option<u32>,
|
||||
}
|
||||
|
||||
pub async fn get_nft_ruleset() -> impl IntoResponse {
|
||||
handle_nft_list(vec!["list".to_string(), "ruleset".to_string()]).await
|
||||
}
|
||||
|
||||
pub async fn get_nft_tables() -> impl IntoResponse {
|
||||
handle_nft_list(vec!["list".to_string(), "tables".to_string()]).await
|
||||
}
|
||||
|
||||
pub async fn get_nft_chains() -> impl IntoResponse {
|
||||
handle_nft_list(vec!["list".to_string(), "chains".to_string()]).await
|
||||
}
|
||||
|
||||
pub async fn get_nft_rules(Path((family, table, chain)): Path<(String, String, String)>) -> impl IntoResponse {
|
||||
let family = match normalize_family(&family) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = validate_ident(&table, "table") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Err(e) = validate_ident(&chain, "chain") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
handle_nft_list(vec![
|
||||
"list".to_string(),
|
||||
"chain".to_string(),
|
||||
family,
|
||||
table,
|
||||
chain,
|
||||
])
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn table_modify(Json(payload): Json<TableRequest>) -> impl IntoResponse {
|
||||
if let Err(e) = validate_ident(&payload.table, "table") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let family = family_to_str(&payload.family);
|
||||
let cmd = match payload.action {
|
||||
TableAction::Add => format!("add table {} {}", family, payload.table),
|
||||
TableAction::Del => format!("delete table {} {}", family, payload.table),
|
||||
TableAction::Flush => format!("flush table {} {}", family, payload.table),
|
||||
};
|
||||
|
||||
match run_nft_script(&cmd).await {
|
||||
Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn chain_modify(Json(payload): Json<ChainRequest>) -> impl IntoResponse {
|
||||
if let Err(e) = validate_ident(&payload.table, "table") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Err(e) = validate_ident(&payload.chain, "chain") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if let Some(ref t) = payload.chain_type {
|
||||
if let Err(e) = validate_ident(t, "type") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
if let Some(ref h) = payload.hook {
|
||||
if let Err(e) = validate_ident(h, "hook") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
if let Some(ref p) = payload.policy {
|
||||
if let Err(e) = validate_ident(p, "policy") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let family = family_to_str(&payload.family);
|
||||
let base = match payload.action {
|
||||
ChainAction::Add => "add chain",
|
||||
ChainAction::Del => "delete chain",
|
||||
ChainAction::Flush => "flush chain",
|
||||
};
|
||||
|
||||
let mut cmd = format!("{} {} {} {}", base, family, payload.table, payload.chain);
|
||||
|
||||
if let ChainAction::Add = payload.action {
|
||||
if let Some(block) = build_chain_block(&payload) {
|
||||
cmd.push(' ');
|
||||
cmd.push_str(&block);
|
||||
}
|
||||
}
|
||||
|
||||
match run_nft_script(&cmd).await {
|
||||
Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rule_modify(Json(payload): Json<RuleRequest>) -> impl IntoResponse {
|
||||
if let Err(e) = validate_ident(&payload.table, "table") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Err(e) = validate_ident(&payload.chain, "chain") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if let Some(ref expr) = payload.expr {
|
||||
if let Err(e) = validate_expr(expr) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let family = family_to_str(&payload.family);
|
||||
let cmd = match payload.action {
|
||||
RuleAction::Add => {
|
||||
let expr = match payload.expr {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: "expr is required for add".to_string() }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
format!("add rule {} {} {} {}", family, payload.table, payload.chain, expr)
|
||||
}
|
||||
RuleAction::Insert => {
|
||||
let expr = match payload.expr {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: "expr is required for insert".to_string() }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if let Some(pos) = payload.position {
|
||||
format!(
|
||||
"insert rule {} {} {} position {} {}",
|
||||
family, payload.table, payload.chain, pos, expr
|
||||
)
|
||||
} else {
|
||||
format!("insert rule {} {} {} {}", family, payload.table, payload.chain, expr)
|
||||
}
|
||||
}
|
||||
RuleAction::Del => {
|
||||
let handle = match payload.handle {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse { error: "handle is required for del".to_string() }),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
format!(
|
||||
"delete rule {} {} {} handle {}",
|
||||
family, payload.table, payload.chain, handle
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
match run_nft_script(&cmd).await {
|
||||
Ok(()) => Json(OkResponse { status: "ok".to_string() }).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_nft_list(args: Vec<String>) -> Response {
|
||||
match run_nft_json(&args).await {
|
||||
Ok(data) => Json(data).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse { error: e }),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_nft_json(args: &[String]) -> Result<serde_json::Value, String> {
|
||||
let output = Command::new("nft")
|
||||
.arg("-j")
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to execute nft -j {}: {}", args.join(" "), e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("nft -j {} failed: {}", args.join(" "), stderr));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
serde_json::from_str::<serde_json::Value>(&stdout)
|
||||
.map_err(|e| format!("Failed to parse nft -j output: {}", e))
|
||||
}
|
||||
|
||||
async fn run_nft_script(cmd: &str) -> Result<(), String> {
|
||||
let mut child = Command::new("nft")
|
||||
.arg("-f")
|
||||
.arg("-")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to execute nft: {}", e))?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin
|
||||
.write_all(cmd.as_bytes())
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write nft stdin: {}", e))?;
|
||||
stdin
|
||||
.write_all(b"\n")
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write nft stdin: {}", e))?;
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to wait for nft: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("nft failed: {}", stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn family_to_str(family: &NftFamily) -> &'static str {
|
||||
match family {
|
||||
NftFamily::Ip => "ip",
|
||||
NftFamily::Ip6 => "ip6",
|
||||
NftFamily::Inet => "inet",
|
||||
NftFamily::Arp => "arp",
|
||||
NftFamily::Bridge => "bridge",
|
||||
NftFamily::Netdev => "netdev",
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_family(value: &str) -> Result<String, String> {
|
||||
let v = value.to_ascii_lowercase();
|
||||
match v.as_str() {
|
||||
"ip" | "ip6" | "inet" | "arp" | "bridge" | "netdev" => Ok(v),
|
||||
_ => Err("unknown family".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_ident(value: &str, field: &str) -> Result<(), String> {
|
||||
if value.is_empty() {
|
||||
return Err(format!("{} must not be empty", field));
|
||||
}
|
||||
if value.len() > 64 {
|
||||
return Err(format!("{} is слишком длинное", field));
|
||||
}
|
||||
if !value
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | ':' ))
|
||||
{
|
||||
return Err(format!("{} содержит недопустимые символы", field));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_expr(expr: &str) -> Result<(), String> {
|
||||
if expr.trim().is_empty() {
|
||||
return Err("expr must not be empty".to_string());
|
||||
}
|
||||
if expr.contains('\n') || expr.contains('\r') {
|
||||
return Err("expr must be single-line".to_string());
|
||||
}
|
||||
if expr.contains(';') {
|
||||
return Err("expr must not contain ';'".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_chain_block(req: &ChainRequest) -> Option<String> {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
|
||||
if req.chain_type.is_some() || req.hook.is_some() || req.priority.is_some() {
|
||||
let mut line = Vec::new();
|
||||
if let Some(ref t) = req.chain_type {
|
||||
line.push(format!("type {}", t));
|
||||
}
|
||||
if let Some(ref h) = req.hook {
|
||||
line.push(format!("hook {}", h));
|
||||
}
|
||||
if let Some(p) = req.priority {
|
||||
line.push(format!("priority {}", p));
|
||||
}
|
||||
if !line.is_empty() {
|
||||
parts.push(format!("{};", line.join(" ")));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref p) = req.policy {
|
||||
parts.push(format!("policy {};", p));
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!("{{ {} }}", parts.join(" ")))
|
||||
}
|
||||
}
|
||||
140
src/auth.rs
Normal file
140
src/auth.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
body::Body,
|
||||
http::{header::AUTHORIZATION, Request, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use chrono::Utc;
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthState {
|
||||
pub secret: String,
|
||||
pub token_ttl_seconds: i64,
|
||||
pub pam_service: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub sub: String,
|
||||
pub exp: usize,
|
||||
pub iat: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
pub fn issue_token(username: &str, state: &AuthState) -> Result<(String, i64), String> {
|
||||
let now = Utc::now().timestamp();
|
||||
let ttl = if state.token_ttl_seconds > 0 {
|
||||
state.token_ttl_seconds
|
||||
} else {
|
||||
3600
|
||||
};
|
||||
let exp = now + ttl;
|
||||
|
||||
let claims = Claims {
|
||||
sub: username.to_string(),
|
||||
iat: now as usize,
|
||||
exp: exp as usize,
|
||||
};
|
||||
|
||||
let payload = serde_json::to_vec(&claims)
|
||||
.map_err(|e| format!("token encode failed: {}", e))?;
|
||||
let signature = sign(&payload, state.secret.as_bytes())?;
|
||||
|
||||
let token = format!(
|
||||
"{}.{}",
|
||||
URL_SAFE_NO_PAD.encode(payload),
|
||||
URL_SAFE_NO_PAD.encode(signature)
|
||||
);
|
||||
|
||||
Ok((token, ttl))
|
||||
}
|
||||
|
||||
pub fn validate_token(token: &str, state: &AuthState) -> Result<Claims, String> {
|
||||
let parts: Vec<&str> = token.split('.').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("invalid token format".to_string());
|
||||
}
|
||||
|
||||
let payload = URL_SAFE_NO_PAD
|
||||
.decode(parts[0].as_bytes())
|
||||
.map_err(|_| "invalid token payload".to_string())?;
|
||||
let signature = URL_SAFE_NO_PAD
|
||||
.decode(parts[1].as_bytes())
|
||||
.map_err(|_| "invalid token signature".to_string())?;
|
||||
|
||||
verify(&payload, &signature, state.secret.as_bytes())?;
|
||||
|
||||
let claims: Claims = serde_json::from_slice(&payload)
|
||||
.map_err(|_| "invalid token payload".to_string())?;
|
||||
|
||||
let now = Utc::now().timestamp() as usize;
|
||||
if claims.exp <= now {
|
||||
return Err("token expired".to_string());
|
||||
}
|
||||
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
pub async fn auth_middleware(
|
||||
State(state): State<AuthState>,
|
||||
mut req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
let token = match auth_header {
|
||||
Some(value) if value.starts_with("Bearer ") => &value[7..],
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ErrorResponse {
|
||||
error: "missing or invalid Authorization header".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
match validate_token(token, &state) {
|
||||
Ok(claims) => {
|
||||
req.extensions_mut().insert(claims);
|
||||
next.run(req).await
|
||||
}
|
||||
Err(_) => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ErrorResponse {
|
||||
error: "invalid or expired token".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sign(payload: &[u8], secret: &[u8]) -> Result<Vec<u8>, String> {
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(secret)
|
||||
.map_err(|_| "invalid secret".to_string())?;
|
||||
mac.update(payload);
|
||||
Ok(mac.finalize().into_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn verify(payload: &[u8], signature: &[u8], secret: &[u8]) -> Result<(), String> {
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(secret)
|
||||
.map_err(|_| "invalid secret".to_string())?;
|
||||
mac.update(payload);
|
||||
mac.verify_slice(signature)
|
||||
.map_err(|_| "invalid token signature".to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,6 +6,14 @@ pub struct Config {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub debug: bool,
|
||||
pub auth: AuthConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AuthConfig {
|
||||
pub secret: String,
|
||||
pub token_ttl_seconds: i64,
|
||||
pub pam_service: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
46
src/main.rs
46
src/main.rs
@@ -1,5 +1,6 @@
|
||||
use axum::{
|
||||
routing::get,
|
||||
routing::{get, post},
|
||||
middleware,
|
||||
Router,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
@@ -11,6 +12,9 @@ use crate::config::Config;
|
||||
mod utils;
|
||||
use utils::logger;
|
||||
|
||||
mod api;
|
||||
mod auth;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct IndexTemplate {
|
||||
@@ -48,10 +52,48 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let addr = format!("{}:{}", cfg.host, cfg.port);
|
||||
logger::success(format!("Running on {}", addr), "Core");
|
||||
|
||||
let auth_state = auth::AuthState {
|
||||
secret: cfg.auth.secret.clone(),
|
||||
token_ttl_seconds: cfg.auth.token_ttl_seconds,
|
||||
pam_service: cfg.auth.pam_service.clone(),
|
||||
};
|
||||
|
||||
let protected_api = Router::new()
|
||||
// IP API
|
||||
.route("/ip/addr", get(api::ip::get_ip_addr))
|
||||
.route("/ip/addr", post(api::ip::addr_modify))
|
||||
.route("/ip/route", get(api::ip::get_ip_route))
|
||||
.route("/ip/route", post(api::ip::route_modify))
|
||||
.route("/ip/link", get(api::ip::get_ip_link))
|
||||
.route("/ip/link/:ifname/up", post(api::ip::set_link_up))
|
||||
.route("/ip/link/:ifname/down", post(api::ip::set_link_down))
|
||||
.route("/ip/link/:ifname/mtu", post(api::ip::set_link_mtu))
|
||||
// nftables API
|
||||
.route("/nft/ruleset", get(api::nft::get_nft_ruleset))
|
||||
.route("/nft/tables", get(api::nft::get_nft_tables))
|
||||
.route("/nft/chains", get(api::nft::get_nft_chains))
|
||||
.route(
|
||||
"/nft/rules/:family/:table/:chain",
|
||||
get(api::nft::get_nft_rules),
|
||||
)
|
||||
.route("/nft/table", post(api::nft::table_modify))
|
||||
.route("/nft/chain", post(api::nft::chain_modify))
|
||||
.route("/nft/rule", post(api::nft::rule_modify))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
auth_state.clone(),
|
||||
auth::auth_middleware,
|
||||
));
|
||||
|
||||
let api = Router::new()
|
||||
.route("/auth/login", post(api::auth::login))
|
||||
.merge(protected_api);
|
||||
|
||||
// Build our application with routes
|
||||
let app = Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/prefs", get(about));
|
||||
.route("/prefs", get(about))
|
||||
.nest("/api", api)
|
||||
.with_state(auth_state);
|
||||
|
||||
// Запускаем сервер
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
|
||||
Reference in New Issue
Block a user