Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
75cddc708f
|
113
Cargo.lock
generated
113
Cargo.lock
generated
@@ -142,20 +142,11 @@ version = "1.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "caps"
|
|
||||||
version = "0.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.52"
|
version = "1.2.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
|
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -175,9 +166,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.43"
|
version = "0.4.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -296,12 +287,6 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -347,9 +332,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.7"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
|
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fluent"
|
name = "fluent"
|
||||||
@@ -508,23 +493,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indoc"
|
|
||||||
version = "2.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "intl-memoizer"
|
name = "intl-memoizer"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -550,15 +526,6 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
@@ -624,9 +591,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.180"
|
version = "0.2.179"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
@@ -680,18 +647,6 @@ name = "nix"
|
|||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cfg-if",
|
|
||||||
"cfg_aliases",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nix"
|
|
||||||
version = "0.31.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -828,18 +783,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.105"
|
version = "1.0.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.43"
|
version = "1.0.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -867,19 +822,15 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"caps",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"indoc",
|
"nix",
|
||||||
"itertools",
|
|
||||||
"nix 0.31.1",
|
|
||||||
"sd-notify",
|
"sd-notify",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -965,9 +916,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.149"
|
version = "1.0.148"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1046,9 +997,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.114"
|
version = "2.0.113"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1193,9 +1144,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.9.11+spec-1.1.0"
|
version = "0.9.10+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
|
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -1349,7 +1300,7 @@ dependencies = [
|
|||||||
"fluent-syntax",
|
"fluent-syntax",
|
||||||
"jiff",
|
"jiff",
|
||||||
"libc",
|
"libc",
|
||||||
"nix 0.30.1",
|
"nix",
|
||||||
"os_display",
|
"os_display",
|
||||||
"phf",
|
"phf",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -1601,9 +1552,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlink"
|
name = "zlink"
|
||||||
version = "0.3.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab752257fcb51fc2d32c90752f02f31eaf6d393448a9d13f236b2c44f3d18b57"
|
checksum = "04baab6c44f6c5f33dd26dffabe2c6473d9a93a080c3424865df068d4e76f58a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zlink-smol",
|
"zlink-smol",
|
||||||
"zlink-tokio",
|
"zlink-tokio",
|
||||||
@@ -1611,9 +1562,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlink-core"
|
name = "zlink-core"
|
||||||
version = "0.3.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a9ae6db49ada3eeeb87f664aeec2786fd1f7a69a3a7325f0999343d0c7216ac"
|
checksum = "487e09febc08bcbac32cee2c26e85779351f711146db0176a3f61a3fea1c955f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -1629,9 +1580,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlink-macros"
|
name = "zlink-macros"
|
||||||
version = "0.3.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c3a659bdba80c9012aba36ad05b509b1d29fdb520eb851689f787a7a28fe21c"
|
checksum = "e6a10a1ed09222634dde2db7055226eafae571389ce57dae3707bd2a76f6dc7b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1640,9 +1591,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlink-smol"
|
name = "zlink-smol"
|
||||||
version = "0.3.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ee3e9df9f2a3725e78a8a080b3873dd523238ce63e4154ac9e4a98be0fc1952"
|
checksum = "72ec157812fcde1a3f45fea27db5a6fa1868ecbf5e11644166ab4caf5e3546dd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
@@ -1655,9 +1606,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlink-tokio"
|
name = "zlink-tokio"
|
||||||
version = "0.3.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aeda977355d1cd582cb3fdce7cf70f3cc89ed9c3ce278a0cda46dc3c09128578"
|
checksum = "6ab6f490a817bcbbc67d82b22427f1fafd9764da04a6b9499cf73e7ef68f4482"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
26
Cargo.toml
26
Cargo.toml
@@ -18,24 +18,21 @@ autolib = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
bytes = "1.11.0"
|
bytes = "1.11.0"
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.42", features = ["serde"] }
|
||||||
clap = { version = "4.5.54", features = ["derive"] }
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
nix = { version = "0.31.1", features = ["hostname", "net", "fs", "user"] }
|
nix = { version = "0.30.1", features = ["hostname", "net"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
tokio = { version = "1.49.0", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
tokio = { version = "1.49.0", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||||
toml = "0.9.11"
|
toml = "0.9.10"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
# onc-rpc = "0.3.2"
|
# onc-rpc = "0.3.2"
|
||||||
sd-notify = { version = "0.4.5", optional = true }
|
sd-notify = { version = "0.4.5", optional = true }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.148"
|
||||||
uucore = { version = "0.5.0", features = ["utmpx"] }
|
uucore = { version = "0.5.0", features = ["utmpx"] }
|
||||||
zlink = { version = "0.3.0", features = ["introspection"] }
|
zlink = { version = "0.2.0", features = ["introspection"] }
|
||||||
clap_complete = "4.5.65"
|
clap_complete = "4.5.65"
|
||||||
itertools = "0.14.0"
|
|
||||||
tokio-util = "0.7.18"
|
|
||||||
caps = "0.5.6"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["systemd"]
|
default = ["systemd"]
|
||||||
@@ -50,10 +47,10 @@ name = "roowhod"
|
|||||||
bench = false
|
bench = false
|
||||||
path = "src/bin/roowhod.rs"
|
path = "src/bin/roowhod.rs"
|
||||||
|
|
||||||
[[bin]]
|
# [[bin]]
|
||||||
name = "finger"
|
# name = "finger"
|
||||||
bench = false
|
# bench = false
|
||||||
path = "src/bin/finger.rs"
|
# path = "src/bin/finger.rs"
|
||||||
|
|
||||||
# [[bin]]
|
# [[bin]]
|
||||||
# name = "rup"
|
# name = "rup"
|
||||||
@@ -85,6 +82,3 @@ inherits = "release"
|
|||||||
strip = true
|
strip = true
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
indoc = "2.0.7"
|
|
||||||
|
|||||||
28
flake.lock
generated
28
flake.lock
generated
@@ -1,27 +1,12 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"crane": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1770419512,
|
|
||||||
"narHash": "sha256-o8Vcdz6B6bkiGUYkZqFwH3Pv1JwZyXht3dMtS7RchIo=",
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"rev": "2510f2cbc3ccd237f700bb213756a8f35c32d8d7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "ipetkov",
|
|
||||||
"repo": "crane",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769170682,
|
"lastModified": 1767116409,
|
||||||
"narHash": "sha256-oMmN1lVQU0F0W2k6OI3bgdzp2YOHWYUAw79qzDSjenU=",
|
"narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c5296fdd05cfa2c187990dd909864da9658df755",
|
"rev": "cad22e7d996aea55ecab064e84834289143e44a0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -33,7 +18,6 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"crane": "crane",
|
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"rust-overlay": "rust-overlay"
|
"rust-overlay": "rust-overlay"
|
||||||
}
|
}
|
||||||
@@ -45,11 +29,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769309768,
|
"lastModified": 1767322002,
|
||||||
"narHash": "sha256-AbOIlNO+JoqRJkK1VrnDXhxuX6CrdtIu2hSuy4pxi3g=",
|
"narHash": "sha256-yHKXXw2OWfIFsyTjduB4EyFwR0SYYF0hK8xI9z4NIn0=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "140c9dc582cb73ada2d63a2180524fcaa744fad5",
|
"rev": "03c6e38661c02a27ca006a284813afdc461e9f7e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
30
flake.nix
30
flake.nix
@@ -4,11 +4,9 @@
|
|||||||
|
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
crane.url = "github:ipetkov/crane";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, rust-overlay, crane }:
|
outputs = { self, nixpkgs, rust-overlay}:
|
||||||
let
|
let
|
||||||
inherit (nixpkgs) lib;
|
inherit (nixpkgs) lib;
|
||||||
|
|
||||||
@@ -73,9 +71,6 @@
|
|||||||
roowho2 = final: prev: {
|
roowho2 = final: prev: {
|
||||||
inherit (self.packages.${prev.stdenv.hostPlatform.system}) roowho2;
|
inherit (self.packages.${prev.stdenv.hostPlatform.system}) roowho2;
|
||||||
};
|
};
|
||||||
roowho2-crane = final: prev: {
|
|
||||||
roowho2 = self.packages.${prev.stdenv.hostPlatform.system}.roowho2-crane;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nixosModules.default = ./nix/module.nix;
|
nixosModules.default = ./nix/module.nix;
|
||||||
@@ -84,13 +79,12 @@
|
|||||||
let
|
let
|
||||||
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||||
cargoLock = ./Cargo.lock;
|
cargoLock = ./Cargo.lock;
|
||||||
craneLib = (crane.mkLib pkgs).overrideToolchain(p: p.rust-bin.nightly.latest.default);
|
|
||||||
|
|
||||||
src = lib.fileset.toSource {
|
src = lib.fileset.toSource {
|
||||||
root = ./.;
|
root = ./.;
|
||||||
fileset = lib.fileset.unions [
|
fileset = lib.fileset.unions [
|
||||||
(craneLib.fileset.commonCargoSources ./.)
|
./src
|
||||||
# ./assets
|
./Cargo.toml
|
||||||
|
./Cargo.lock
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,25 +93,13 @@
|
|||||||
cargo = pkgs.rust-bin.nightly.latest.cargo;
|
cargo = pkgs.rust-bin.nightly.latest.cargo;
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
default = self.packages.${system}.roowho2-crane;
|
default = self.packages.${system}.roowho2;
|
||||||
|
|
||||||
roowho2 = pkgs.callPackage ./nix/package.nix {
|
roowho2 = pkgs.callPackage ./nix/package.nix { inherit cargoToml cargoLock src rustPlatform; };
|
||||||
inherit cargoToml cargoLock src rustPlatform;
|
|
||||||
};
|
|
||||||
|
|
||||||
roowho2-crane = pkgs.callPackage ./nix/package.nix {
|
|
||||||
useCrane = true;
|
|
||||||
inherit cargoToml cargoLock src craneLib;
|
|
||||||
};
|
|
||||||
|
|
||||||
filteredSource = pkgs.runCommandLocal "filtered-source" { } ''
|
filteredSource = pkgs.runCommandLocal "filtered-source" { } ''
|
||||||
ln -s ${src} $out
|
ln -s ${src} $out
|
||||||
'';
|
'';
|
||||||
});
|
});
|
||||||
|
|
||||||
checks = forAllSystems (system: pkgs: _: {
|
|
||||||
# NOTE: the non-crane build runs tests during checkPhase
|
|
||||||
inherit (self.packages.${system}) roowho2;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ in {
|
|||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
# TODO: allow configuring socket config
|
|
||||||
};
|
|
||||||
fingerd = {
|
|
||||||
enable = lib.mkEnableOption "the fingerd service" // {
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
# TODO: allow configuring socket config
|
# TODO: allow configuring socket config
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -56,66 +49,11 @@ in {
|
|||||||
|
|
||||||
systemd.services.roowho2 = {
|
systemd.services.roowho2 = {
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "notify";
|
|
||||||
ExecStart = "${lib.getExe' cfg.package "roowhod"} --config ${format.generate "roowho2-config.toml" cfg.settings}";
|
ExecStart = "${lib.getExe' cfg.package "roowhod"} --config ${format.generate "roowho2-config.toml" cfg.settings}";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
DynamicUser = true;
|
DynamicUser = true;
|
||||||
|
|
||||||
# NOTE: roowho2 might at some point need to read from home directories
|
# TODO: hardening
|
||||||
# to get user settings, so let's keep these disabled for now.
|
|
||||||
# PrivateUsers = true;
|
|
||||||
# ProtectHome = true;
|
|
||||||
|
|
||||||
AmbientCapabilities = "";
|
|
||||||
CapabilityBoundingSet = "";
|
|
||||||
DeviceAllow = "";
|
|
||||||
DevicePolicy = "closed";
|
|
||||||
LockPersonality = true;
|
|
||||||
MemoryDenyWriteExecute = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
PrivateDevices = true;
|
|
||||||
# NOTE: all ipc traffic is served through the socket activation fds or provided by systemd
|
|
||||||
PrivateIPC = true;
|
|
||||||
PrivateMounts = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
ProcSubset = "pid";
|
|
||||||
ProtectClock = true;
|
|
||||||
ProtectControlGroups = "strict";
|
|
||||||
ProtectHostname = true;
|
|
||||||
ProtectKernelLogs = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectProc = "invisible";
|
|
||||||
RemoveIPC = true;
|
|
||||||
RestrictAddressFamilies = [
|
|
||||||
"AF_INET"
|
|
||||||
"AF_INET6"
|
|
||||||
"AF_UNIX"
|
|
||||||
"AF_NETLINK"
|
|
||||||
];
|
|
||||||
RestrictNamespaces = true;
|
|
||||||
RestrictRealtime = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
SocketBindDeny = "any";
|
|
||||||
SystemCallArchitectures = "native";
|
|
||||||
SystemCallFilter = [
|
|
||||||
"@system-service"
|
|
||||||
"~@privileged"
|
|
||||||
"~@resources"
|
|
||||||
];
|
|
||||||
|
|
||||||
RuntimeDirectory = "roowho2/root-mnt";
|
|
||||||
RuntimeDirectoryMode = "0700";
|
|
||||||
RootDirectory = "/run/roowho2/root-mnt";
|
|
||||||
BindReadOnlyPaths = [
|
|
||||||
builtins.storeDir
|
|
||||||
"/etc"
|
|
||||||
# NOTE: need logind socket for utmp entries
|
|
||||||
"/run/systemd"
|
|
||||||
"/home"
|
|
||||||
];
|
|
||||||
|
|
||||||
UMask = "0077";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,62 +10,22 @@
|
|||||||
, cargoToml
|
, cargoToml
|
||||||
, cargoLock
|
, cargoLock
|
||||||
, src
|
, src
|
||||||
|
|
||||||
, useCrane ? false
|
|
||||||
, craneLib ? null
|
|
||||||
}:
|
}:
|
||||||
let
|
rustPlatform.buildRustPackage {
|
||||||
mainProgram = "roowhod";
|
pname = "roowho2";
|
||||||
buildFunction = if useCrane then craneLib.buildPackage else rustPlatform.buildRustPackage;
|
|
||||||
|
|
||||||
pnameCraneSuffix = lib.optionalString useCrane "-crane";
|
|
||||||
pname = "${cargoToml.package.name}${pnameCraneSuffix}";
|
|
||||||
|
|
||||||
rustPlatformArgs = {
|
|
||||||
buildType = "releaselto";
|
|
||||||
buildFeatures = lib.optionals stdenv.hostPlatform.isLinux [
|
|
||||||
"systemd"
|
|
||||||
];
|
|
||||||
cargoLock.lockFile = cargoLock;
|
|
||||||
|
|
||||||
doCheck = true;
|
|
||||||
useNextest = true;
|
|
||||||
nativeCheckInputs = [
|
|
||||||
versionCheckHook
|
|
||||||
];
|
|
||||||
cargoCheckFeatures = lib.optionals stdenv.hostPlatform.isLinux [
|
|
||||||
"systemd"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
craneArgs = {
|
|
||||||
cargoLock = cargoLock;
|
|
||||||
cargoExtraArgs = lib.escapeShellArgs [ "--features" (lib.concatStringsSep "," (
|
|
||||||
lib.optionals stdenv.hostPlatform.isLinux [
|
|
||||||
"systemd"
|
|
||||||
]
|
|
||||||
)) ];
|
|
||||||
cargoArtifacts = craneLib.buildDepsOnly {
|
|
||||||
inherit pname;
|
|
||||||
inherit (cargoToml.package) version;
|
|
||||||
src = lib.fileset.toSource {
|
|
||||||
root = ../.;
|
|
||||||
fileset = lib.fileset.unions [
|
|
||||||
(craneLib.fileset.cargoTomlAndLock ../.)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
cargoLock = cargoLock;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
buildFunction ({
|
|
||||||
inherit pname;
|
|
||||||
inherit (cargoToml.package) version;
|
inherit (cargoToml.package) version;
|
||||||
inherit src;
|
inherit src;
|
||||||
|
|
||||||
|
cargoLock.lockFile = cargoLock;
|
||||||
|
|
||||||
|
buildType = "releaselto";
|
||||||
|
|
||||||
RUSTFLAGS = "-Zhigher-ranked-assumptions";
|
RUSTFLAGS = "-Zhigher-ranked-assumptions";
|
||||||
|
|
||||||
|
buildFeatures = lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
|
"systemd"
|
||||||
|
];
|
||||||
|
|
||||||
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
|
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
systemdLibs
|
systemdLibs
|
||||||
];
|
];
|
||||||
@@ -81,16 +41,12 @@ buildFunction ({
|
|||||||
installShellCompletion "--${shell}" --cmd "${command}" "$TMP/${command}.${shell}"
|
installShellCompletion "--${shell}" --cmd "${command}" "$TMP/${command}.${shell}"
|
||||||
'') {
|
'') {
|
||||||
shell = [ "bash" "zsh" "fish" ];
|
shell = [ "bash" "zsh" "fish" ];
|
||||||
command = [ "rwho" "ruptime" "finger" ];
|
command = [ "rwho" "ruptime" ];
|
||||||
};
|
};
|
||||||
in lib.concatStringsSep "\n" installShellCompletions;
|
in lib.concatStringsSep "\n" installShellCompletions;
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with lib; {
|
||||||
license = licenses.bsdOriginalUC;
|
license = licenses.mit;
|
||||||
platforms = platforms.linux ++ platforms.darwin;
|
platforms = platforms.linux ++ platforms.darwin;
|
||||||
inherit mainProgram;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//
|
|
||||||
(if useCrane then craneArgs else rustPlatformArgs)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ let
|
|||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
overlays = [
|
overlays = [
|
||||||
self.overlays.roowho2-crane
|
self.overlays.default
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
@@ -34,7 +34,6 @@ nixpkgs.lib.nixosSystem {
|
|||||||
Try running any of:
|
Try running any of:
|
||||||
rwho
|
rwho
|
||||||
ruptime
|
ruptime
|
||||||
finger "alice"
|
|
||||||
|
|
||||||
To log into other containers, use:
|
To log into other containers, use:
|
||||||
machinectl shell c1
|
machinectl shell c1
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
use anyhow::Context;
|
use clap::Parser;
|
||||||
use clap::{CommandFactory, Parser};
|
|
||||||
use clap_complete::{Shell, generate};
|
|
||||||
use roowho2_lib::server::varlink_api::VarlinkFingerClientProxy;
|
|
||||||
|
|
||||||
/// User information lookup program
|
/// User information lookup program
|
||||||
///
|
///
|
||||||
@@ -25,7 +22,7 @@ use roowho2_lib::server::varlink_api::VarlinkFingerClientProxy;
|
|||||||
#[command(
|
#[command(
|
||||||
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
|
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
|
||||||
about,
|
about,
|
||||||
version
|
version,
|
||||||
)]
|
)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Forces finger to use IPv4 addresses only.
|
/// Forces finger to use IPv4 addresses only.
|
||||||
@@ -54,7 +51,7 @@ pub struct Args {
|
|||||||
|
|
||||||
/// When used in conjunction with the -s option, the name of the remote host
|
/// When used in conjunction with the -s option, the name of the remote host
|
||||||
/// is displayed instead of the office location and office phone.
|
/// is displayed instead of the office location and office phone.
|
||||||
#[arg(long, short = 'H', requires = "short", conflicts_with = "office")]
|
#[arg(long, short, requires = "short", conflicts_with = "office")]
|
||||||
host: bool,
|
host: bool,
|
||||||
|
|
||||||
/// When used in conjunction with the -s option, the office location and
|
/// When used in conjunction with the -s option, the office location and
|
||||||
@@ -109,41 +106,9 @@ pub struct Args {
|
|||||||
/// Output in JSON format
|
/// Output in JSON format
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
||||||
/// Generate shell completion scripts for the specified shell
|
|
||||||
/// and print them to stdout.
|
|
||||||
#[arg(long, value_enum, hide = true)]
|
|
||||||
completions: Option<Shell>,
|
|
||||||
|
|
||||||
users: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
fn main() {
|
||||||
async fn main() -> anyhow::Result<()> {
|
let _args = Args::parse();
|
||||||
let args = Args::parse();
|
unimplemented!()
|
||||||
|
|
||||||
if let Some(shell) = args.completions {
|
|
||||||
generate(shell, &mut Args::command(), "rwho", &mut std::io::stdout());
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut conn = zlink::unix::connect("/run/roowho2/roowho2.varlink")
|
|
||||||
.await
|
|
||||||
.expect("Failed to connect to fingerd server");
|
|
||||||
|
|
||||||
let reply = conn
|
|
||||||
.finger(args.users)
|
|
||||||
.await
|
|
||||||
.context("Failed to send finger request")?
|
|
||||||
.map_err(|e| anyhow::anyhow!("Server returned an error for finger request: {:?}", e))?;
|
|
||||||
|
|
||||||
if args.json {
|
|
||||||
println!("{}", serde_json::to_string_pretty(&reply).unwrap());
|
|
||||||
} else {
|
|
||||||
for user in reply {
|
|
||||||
println!("{:#?}", user.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use std::{
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use tokio::{net::UdpSocket, sync::RwLock};
|
use tokio::{net::UdpSocket, sync::RwLock};
|
||||||
use tokio_util::sync::CancellationToken;
|
|
||||||
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
use roowho2_lib::server::{
|
use roowho2_lib::server::{
|
||||||
@@ -65,16 +64,6 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let whod_status_store = Arc::new(RwLock::new(HashMap::new()));
|
let whod_status_store = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
let client_server_token = CancellationToken::new();
|
|
||||||
let client_server_token_ = client_server_token.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
client_server_token_.cancelled().await;
|
|
||||||
tracing::info!("RWHOD client-server is now accepting connections");
|
|
||||||
#[cfg(feature = "systemd")]
|
|
||||||
sd_notify::notify(true, &[sd_notify::NotifyState::Ready]).ok();
|
|
||||||
Ok::<(), anyhow::Error>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
if config.rwhod.enable {
|
if config.rwhod.enable {
|
||||||
tracing::info!("Starting RWHOD server");
|
tracing::info!("Starting RWHOD server");
|
||||||
|
|
||||||
@@ -100,7 +89,6 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.try_clone()
|
.try_clone()
|
||||||
.context("Failed to clone RWHOD client-server socket fd")?,
|
.context("Failed to clone RWHOD client-server socket fd")?,
|
||||||
whod_status_store.clone(),
|
whod_status_store.clone(),
|
||||||
client_server_token,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
join_set.spawn(ctrl_c_handler());
|
join_set.spawn(ctrl_c_handler());
|
||||||
@@ -138,7 +126,6 @@ async fn rwhod_server(
|
|||||||
async fn client_server(
|
async fn client_server(
|
||||||
socket_fd: OwnedFd,
|
socket_fd: OwnedFd,
|
||||||
whod_status_store: RwhodStatusStore,
|
whod_status_store: RwhodStatusStore,
|
||||||
startup_token: CancellationToken,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// SAFETY: see above
|
// SAFETY: see above
|
||||||
let std_socket =
|
let std_socket =
|
||||||
@@ -147,8 +134,6 @@ async fn client_server(
|
|||||||
let zlink_listener = zlink::unix::Listener::try_from(OwnedFd::from(std_socket))?;
|
let zlink_listener = zlink::unix::Listener::try_from(OwnedFd::from(std_socket))?;
|
||||||
let client_server_task = varlink_client_server_task(zlink_listener, whod_status_store);
|
let client_server_task = varlink_client_server_task(zlink_listener, whod_status_store);
|
||||||
|
|
||||||
startup_token.cancel();
|
|
||||||
|
|
||||||
client_server_task.await?;
|
client_server_task.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{Duration, Utc};
|
|||||||
use clap::{CommandFactory, Parser};
|
use clap::{CommandFactory, Parser};
|
||||||
use clap_complete::{Shell, generate};
|
use clap_complete::{Shell, generate};
|
||||||
|
|
||||||
use roowho2_lib::{proto::WhodStatusUpdate, server::varlink_api::VarlinkRwhodClientProxy};
|
use roowho2_lib::{proto::WhodStatusUpdate, server::varlink_api::RwhodClientProxy};
|
||||||
|
|
||||||
/// Show host status of local machines.
|
/// Show host status of local machines.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::{CommandFactory, Parser};
|
use clap::{CommandFactory, Parser};
|
||||||
use clap_complete::{Shell, generate};
|
use clap_complete::{Shell, generate};
|
||||||
use roowho2_lib::{proto::WhodUserEntry, server::varlink_api::VarlinkRwhodClientProxy};
|
use roowho2_lib::{proto::WhodUserEntry, server::varlink_api::RwhodClientProxy};
|
||||||
|
|
||||||
/// Check who is logged in on local machines.
|
/// Check who is logged in on local machines.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,6 +1,2 @@
|
|||||||
#![feature(iter_map_windows)]
|
|
||||||
#![feature(gethostname)]
|
|
||||||
#![feature(trim_prefix_suffix)]
|
|
||||||
|
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|||||||
@@ -1,769 +1,55 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, TimeDelta, Utc, Weekday};
|
use chrono::{DateTime, Utc};
|
||||||
use itertools::Itertools;
|
use nix::libc::uid_t;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct FingerRequest {
|
pub struct FingerPerson {
|
||||||
long: bool,
|
/// User id
|
||||||
|
uid: uid_t,
|
||||||
|
/// User' home directory
|
||||||
|
dir: PathBuf,
|
||||||
|
/// Home phone no.
|
||||||
|
homephone: String,
|
||||||
|
/// Login name
|
||||||
name: String,
|
name: String,
|
||||||
}
|
/// Office name
|
||||||
|
office: String,
|
||||||
impl FingerRequest {
|
/// Office phone no.
|
||||||
pub fn new(long: bool, name: String) -> Self {
|
officephone: String,
|
||||||
Self { long, name }
|
/// Full name
|
||||||
}
|
realname: String,
|
||||||
|
/// User's shell
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
shell: String,
|
||||||
let mut result = Vec::new();
|
/// Last time mail was read
|
||||||
if self.long {
|
mailread: DateTime<Utc>,
|
||||||
result.extend(b"/W ");
|
/// Last time mail was received
|
||||||
}
|
mailrecv: DateTime<Utc>,
|
||||||
|
/// List of where the user is or has been
|
||||||
result.extend(self.name.as_bytes());
|
where_: Vec<FingerWhere>,
|
||||||
result.extend(b"\r\n");
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Self {
|
|
||||||
let (long, name) = if &bytes[..3] == b"/W " {
|
|
||||||
(true, &bytes[3..])
|
|
||||||
} else {
|
|
||||||
(false, bytes)
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = match name.strip_suffix(b"\r\n") {
|
|
||||||
Some(new_name) => new_name,
|
|
||||||
None => name,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self::new(long, String::from_utf8_lossy(name).to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
||||||
pub struct RawFingerResponse(String);
|
|
||||||
|
|
||||||
impl RawFingerResponse {
|
|
||||||
pub fn new(content: String) -> Self {
|
|
||||||
Self(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_inner(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_inner(self) -> String {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.0.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Self {
|
|
||||||
if bytes.is_empty() {
|
|
||||||
return Self(String::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize(c: u8) -> u8 {
|
|
||||||
if c == (b'\r' | 0x80) || c == (b'\n' | 0x80) {
|
|
||||||
c & 0x7f
|
|
||||||
} else {
|
|
||||||
c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalized: Vec<u8> = bytes
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.map(normalize)
|
|
||||||
.chain(std::iter::once(normalize(*bytes.last().unwrap())))
|
|
||||||
.map_windows(|[a, b]| {
|
|
||||||
if *a == b'\r' && *b == b'\n' {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(*a)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let result = String::from_utf8_lossy(&normalized).to_string();
|
|
||||||
|
|
||||||
Self(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
let mut out = Vec::with_capacity(self.0.len() + 2);
|
|
||||||
|
|
||||||
for &b in self.0.as_bytes() {
|
|
||||||
if b == b'\n' {
|
|
||||||
out.extend_from_slice(b"\r\n");
|
|
||||||
} else {
|
|
||||||
out.push(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.0.ends_with('\n') {
|
|
||||||
out.extend_from_slice(b"\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for RawFingerResponse {
|
|
||||||
fn from(s: String) -> Self {
|
|
||||||
Self::new(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for RawFingerResponse {
|
|
||||||
fn from(s: &str) -> Self {
|
|
||||||
Self::new(s.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse the time serialization format commonly used by bsd-finger
|
|
||||||
fn parse_bsd_finger_time(time: &str) -> anyhow::Result<DateTime<Utc>> {
|
|
||||||
let time_parts: Vec<_> = time.split_ascii_whitespace().collect();
|
|
||||||
|
|
||||||
let time = &time_parts[..time_parts.len() - 1].join(" ");
|
|
||||||
let _timezone = time_parts[time_parts.len() - 1];
|
|
||||||
|
|
||||||
let now = Utc::now();
|
|
||||||
let mut parts = time.split_whitespace();
|
|
||||||
|
|
||||||
let weekday = match parts.next() {
|
|
||||||
Some("Mon") => Weekday::Mon,
|
|
||||||
Some("Tue") => Weekday::Tue,
|
|
||||||
Some("Wed") => Weekday::Wed,
|
|
||||||
Some("Thu") => Weekday::Thu,
|
|
||||||
Some("Fri") => Weekday::Fri,
|
|
||||||
Some("Sat") => Weekday::Sat,
|
|
||||||
Some("Sun") => Weekday::Sun,
|
|
||||||
_ => anyhow::bail!("Invalid weekday in login time: {}", time),
|
|
||||||
};
|
|
||||||
|
|
||||||
let month = match parts.next() {
|
|
||||||
Some("Jan") => 1,
|
|
||||||
Some("Feb") => 2,
|
|
||||||
Some("Mar") => 3,
|
|
||||||
Some("Apr") => 4,
|
|
||||||
Some("May") => 5,
|
|
||||||
Some("Jun") => 6,
|
|
||||||
Some("Jul") => 7,
|
|
||||||
Some("Aug") => 8,
|
|
||||||
Some("Sep") => 9,
|
|
||||||
Some("Oct") => 10,
|
|
||||||
Some("Nov") => 11,
|
|
||||||
Some("Dec") => 12,
|
|
||||||
_ => anyhow::bail!("Invalid month in login time: {}", time),
|
|
||||||
};
|
|
||||||
|
|
||||||
let day: u32 = parts
|
|
||||||
.next()
|
|
||||||
.and_then(|d| d.parse().ok())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Invalid day in login time: {}", time))?;
|
|
||||||
|
|
||||||
let time_part = parts
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing time in login time: {}", time))?;
|
|
||||||
|
|
||||||
let clock = NaiveTime::parse_from_str(time_part, "%H:%M").map_err(|e| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Failed to parse time component in login time: {}: {}",
|
|
||||||
time,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
const MAX_YEARS_BACK: i32 = 10;
|
|
||||||
|
|
||||||
for offset in 0..=MAX_YEARS_BACK {
|
|
||||||
let year = now.year() - offset;
|
|
||||||
|
|
||||||
let date = match NaiveDate::from_ymd_opt(year, month, day) {
|
|
||||||
Some(d) => d,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
if date.weekday() != weekday {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dt = date.and_time(clock);
|
|
||||||
|
|
||||||
if dt <= now.naive_utc() {
|
|
||||||
// TODO: apply timezone if we are able to parse it.
|
|
||||||
// if not, try to get the local timezone offset.
|
|
||||||
// if not, assume UTC.
|
|
||||||
|
|
||||||
return Ok(DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Could not infer year for login time {} within {} years",
|
|
||||||
time,
|
|
||||||
MAX_YEARS_BACK
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct FingerResponseUserEntry {
|
pub struct FingerWhere {
|
||||||
/// The unix username of this user, as noted in passwd
|
/// Type/status of request
|
||||||
pub username: String,
|
status: FingerWhereStatus,
|
||||||
|
/// Tty is writable
|
||||||
/// The full name of this user, as noted in passwd
|
writable: bool,
|
||||||
pub full_name: String,
|
/// Time of (last) login
|
||||||
|
loginat: DateTime<Utc>,
|
||||||
/// The path to the home directory of this user, as noted in passwd
|
/// how long idle (if logged in)
|
||||||
pub home_dir: PathBuf,
|
idletime: DateTime<Utc>,
|
||||||
|
// TODO: are technically limited by UT_LINESIZE and UT_HOSTSIZE
|
||||||
/// The path to the shell of this user, as noted in passwd
|
/// Tty line
|
||||||
pub shell: PathBuf,
|
tty: String,
|
||||||
|
/// Remote hostname
|
||||||
/// Office location, if available
|
host: String,
|
||||||
pub office: Option<String>,
|
|
||||||
|
|
||||||
/// Office phone number, if available
|
|
||||||
pub office_phone: Option<String>,
|
|
||||||
|
|
||||||
/// Home phone number, if available
|
|
||||||
pub home_phone: Option<String>,
|
|
||||||
|
|
||||||
/// Whether the user has never logged in to this host
|
|
||||||
pub never_logged_in: bool,
|
|
||||||
|
|
||||||
/// A list of user sessions, sourced from utmp entries
|
|
||||||
pub sessions: Vec<FingerResponseUserSession>,
|
|
||||||
|
|
||||||
/// Contents of ~/.forward, if it exists
|
|
||||||
pub forward_status: Option<String>,
|
|
||||||
|
|
||||||
/// Whether the user has new or unread mail
|
|
||||||
pub mail_status: Option<MailStatus>,
|
|
||||||
|
|
||||||
/// Contents of ~/.pgpkey, if it exists
|
|
||||||
pub pgp_key: Option<String>,
|
|
||||||
|
|
||||||
/// Contents of ~/.project, if it exists
|
|
||||||
pub project: Option<String>,
|
|
||||||
|
|
||||||
/// Contents of ~/.plan, if it exists
|
|
||||||
pub plan: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FingerResponseUserEntry {
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
|
||||||
username: String,
|
|
||||||
full_name: String,
|
|
||||||
home_dir: PathBuf,
|
|
||||||
shell: PathBuf,
|
|
||||||
office: Option<String>,
|
|
||||||
office_phone: Option<String>,
|
|
||||||
home_phone: Option<String>,
|
|
||||||
never_logged_in: bool,
|
|
||||||
sessions: Vec<FingerResponseUserSession>,
|
|
||||||
forward_status: Option<String>,
|
|
||||||
mail_status: Option<MailStatus>,
|
|
||||||
pgp_key: Option<String>,
|
|
||||||
project: Option<String>,
|
|
||||||
plan: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
debug_assert!(
|
|
||||||
!never_logged_in || sessions.is_empty(),
|
|
||||||
"User cannot be marked as never logged in while having active sessions"
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
username,
|
|
||||||
full_name,
|
|
||||||
home_dir,
|
|
||||||
shell,
|
|
||||||
office,
|
|
||||||
office_phone,
|
|
||||||
home_phone,
|
|
||||||
never_logged_in,
|
|
||||||
sessions,
|
|
||||||
forward_status,
|
|
||||||
mail_status,
|
|
||||||
pgp_key,
|
|
||||||
project,
|
|
||||||
plan,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try parsing a [FingerResponseUserEntry] from the text format used by bsd-finger.
|
|
||||||
pub fn try_from_raw_finger_response(
|
|
||||||
response: &RawFingerResponse,
|
|
||||||
username: String,
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
let content = response.get_inner();
|
|
||||||
let lines: Vec<&str> = content.lines().collect();
|
|
||||||
|
|
||||||
if lines.len() < 2 {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Unexpected finger response format for user {}",
|
|
||||||
username
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let first_line = lines[0];
|
|
||||||
let second_line = lines[1];
|
|
||||||
|
|
||||||
let full_name = first_line
|
|
||||||
.split("Name:")
|
|
||||||
.nth(1)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Failed to parse full name from finger response for user {}",
|
|
||||||
username
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let home_dir = second_line
|
|
||||||
.split("Directory:")
|
|
||||||
.nth(1)
|
|
||||||
.and_then(|s| s.split("Shell:").next())
|
|
||||||
.map(|s| s.trim())
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Failed to parse home directory from finger response for user {}",
|
|
||||||
username
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let shell = second_line
|
|
||||||
.split("Shell:")
|
|
||||||
.nth(1)
|
|
||||||
.map(|s| s.trim())
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Failed to parse shell from finger response for user {}",
|
|
||||||
username
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut current_index = 2;
|
|
||||||
|
|
||||||
let mut office: Option<String> = None;
|
|
||||||
let mut office_phone: Option<String> = None;
|
|
||||||
let mut home_phone: Option<String> = None;
|
|
||||||
|
|
||||||
// TODO: handle case where office details contains comma, use last comma as separator
|
|
||||||
if let Some(line) = lines.get(current_index)
|
|
||||||
&& line.trim().starts_with("Office:")
|
|
||||||
{
|
|
||||||
let office_line = line.trim().trim_start_matches("Office:").trim();
|
|
||||||
if let Some((office_loc, phone)) = office_line.split_once(',') {
|
|
||||||
office = Some(office_loc.trim().to_string());
|
|
||||||
office_phone = Some(phone.trim().to_string());
|
|
||||||
} else {
|
|
||||||
office = Some(office_line.to_string());
|
|
||||||
}
|
|
||||||
current_index += 1;
|
|
||||||
}
|
|
||||||
if let Some(line) = lines.get(current_index)
|
|
||||||
&& line.trim().starts_with("Office Phone:")
|
|
||||||
{
|
|
||||||
let phone = line.trim().trim_start_matches("Office Phone:").trim();
|
|
||||||
office_phone = Some(phone.to_string());
|
|
||||||
current_index += 1;
|
|
||||||
}
|
|
||||||
if let Some(line) = lines.get(current_index)
|
|
||||||
&& line.trim().starts_with("Home Phone:")
|
|
||||||
{
|
|
||||||
let phone = line.trim().trim_start_matches("Home Phone:").trim();
|
|
||||||
home_phone = Some(phone.to_string());
|
|
||||||
current_index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let never_logged_in = lines
|
|
||||||
.iter()
|
|
||||||
.skip(current_index)
|
|
||||||
.take(1)
|
|
||||||
.any(|&line| line.trim() == "Never logged in.");
|
|
||||||
|
|
||||||
let sessions: Vec<_> = lines
|
|
||||||
.iter()
|
|
||||||
.skip(current_index)
|
|
||||||
.take_while(|line| line.starts_with("On since"))
|
|
||||||
.filter_map(|line| {
|
|
||||||
match FingerResponseUserSession::try_from_finger_response_line(line) {
|
|
||||||
Ok(session) => Some(session),
|
|
||||||
Err(_) => {
|
|
||||||
tracing::warn!("Failed to parse user session from line: {}", line);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if never_logged_in {
|
|
||||||
debug_assert!(
|
|
||||||
sessions.is_empty(),
|
|
||||||
"User cannot be marked as never logged in while having active sessions"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
current_index += if never_logged_in { 1 } else { sessions.len() };
|
|
||||||
|
|
||||||
let next_line = lines.get(current_index);
|
|
||||||
|
|
||||||
// TODO: handle multi-line case
|
|
||||||
let forward_status = if let Some(line) = next_line
|
|
||||||
&& line.trim().starts_with("Mail forwarded to ")
|
|
||||||
{
|
|
||||||
Some(line.trim().trim_prefix("Mail forwarded to ").to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: parse forward_status, mail_status, plan from remaining lines
|
|
||||||
|
|
||||||
Ok(Self::new(
|
|
||||||
username,
|
|
||||||
full_name,
|
|
||||||
home_dir,
|
|
||||||
shell,
|
|
||||||
office,
|
|
||||||
office_phone,
|
|
||||||
home_phone,
|
|
||||||
never_logged_in,
|
|
||||||
sessions,
|
|
||||||
forward_status,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum MailStatus {
|
#[repr(C)]
|
||||||
NoMail,
|
pub enum FingerWhereStatus {
|
||||||
NewMailReceived(DateTime<Utc>),
|
LastLog,
|
||||||
UnreadSince(DateTime<Utc>),
|
LoggedIn,
|
||||||
MailLastRead(DateTime<Utc>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MailStatus {
|
|
||||||
pub fn try_from_finger_response_line(str: &str) -> anyhow::Result<Self> {
|
|
||||||
if str.trim() == "No mail." {
|
|
||||||
Ok(Self::NoMail)
|
|
||||||
} else if str.trim().starts_with("New mail received") {
|
|
||||||
let datetime = parse_bsd_finger_time(str.trim().trim_prefix("New mail received "))?;
|
|
||||||
Ok(Self::NewMailReceived(datetime))
|
|
||||||
} else if str.trim().starts_with("Unread since") {
|
|
||||||
let datetime = parse_bsd_finger_time(str.trim().trim_prefix("Unread since "))?;
|
|
||||||
Ok(Self::UnreadSince(datetime))
|
|
||||||
} else if str.trim().starts_with("Mail last read") {
|
|
||||||
let datetime = parse_bsd_finger_time(str.trim().trim_prefix("Mail last read "))?;
|
|
||||||
Ok(Self::MailLastRead(datetime))
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct FingerResponseUserSession {
|
|
||||||
/// The tty on which this session exists
|
|
||||||
pub tty: String,
|
|
||||||
|
|
||||||
/// When the user logged in and created this session
|
|
||||||
pub login_time: DateTime<Utc>,
|
|
||||||
|
|
||||||
/// The amount of time since the use last interacted with the tty
|
|
||||||
pub idle_time: TimeDelta,
|
|
||||||
|
|
||||||
/// The hostname of the machine where this session is running
|
|
||||||
pub host: String,
|
|
||||||
|
|
||||||
/// Whether this tty is writable, and thus can receive messages via `mesg(1)`
|
|
||||||
pub messages_on: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FingerResponseUserSession {
|
|
||||||
pub fn new(
|
|
||||||
tty: String,
|
|
||||||
login_time: DateTime<Utc>,
|
|
||||||
idle_time: TimeDelta,
|
|
||||||
host: String,
|
|
||||||
messages_on: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
tty,
|
|
||||||
login_time,
|
|
||||||
idle_time,
|
|
||||||
host,
|
|
||||||
messages_on,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse the idle time from the text string generated by bsd-finger
|
|
||||||
fn parse_idle_time(str: &str) -> anyhow::Result<Duration> {
|
|
||||||
// Parse idle time from finger response format.
|
|
||||||
// This has four cases: " ", "MMMMM", "HH:MM", "DDd"
|
|
||||||
if str.trim().is_empty() {
|
|
||||||
Ok(Duration::zero())
|
|
||||||
} else if str.contains(':') {
|
|
||||||
let parts: Vec<&str> = str.split(':').collect();
|
|
||||||
if parts.len() != 2 {
|
|
||||||
return Err(anyhow::anyhow!("Invalid idle time format: {}", str));
|
|
||||||
}
|
|
||||||
let hours: i64 = parts[0].parse().map_err(|e| {
|
|
||||||
anyhow::anyhow!("Failed to parse hours from idle time {}: {}", str, e)
|
|
||||||
})?;
|
|
||||||
let minutes: i64 = parts[1].parse().map_err(|e| {
|
|
||||||
anyhow::anyhow!("Failed to parse minutes from idle time {}: {}", str, e)
|
|
||||||
})?;
|
|
||||||
Ok(Duration::hours(hours) + Duration::minutes(minutes))
|
|
||||||
} else if str.ends_with('d') {
|
|
||||||
let days_str = str.trim_end_matches('d');
|
|
||||||
let days: i64 = days_str.parse().map_err(|e| {
|
|
||||||
anyhow::anyhow!("Failed to parse days from idle time {}: {}", str, e)
|
|
||||||
})?;
|
|
||||||
Ok(Duration::days(days))
|
|
||||||
} else {
|
|
||||||
let minutes: i64 = str.parse().map_err(|e| {
|
|
||||||
anyhow::anyhow!("Failed to parse minutes from idle time {}: {}", str, e)
|
|
||||||
})?;
|
|
||||||
Ok(Duration::minutes(minutes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try parsing a [FingerResponseUserSession] from the text format used by bsd-finger.
|
|
||||||
pub fn try_from_finger_response_line(line: &str) -> anyhow::Result<Self> {
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
|
|
||||||
debug_assert!(parts[0] == "On");
|
|
||||||
debug_assert!(parts[1] == "since");
|
|
||||||
|
|
||||||
let login_time_str = parts
|
|
||||||
.iter()
|
|
||||||
.take_while(|&&s| s != "on")
|
|
||||||
.skip(2)
|
|
||||||
.cloned()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
let login_time = parse_bsd_finger_time(&login_time_str)?;
|
|
||||||
|
|
||||||
let tty = parts
|
|
||||||
.iter()
|
|
||||||
.skip_while(|&&s| s != "on")
|
|
||||||
.nth(1)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Failed to find tty in finger session line: {line}"))?
|
|
||||||
.trim_end_matches(',')
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let idle_time_str = parts
|
|
||||||
.iter()
|
|
||||||
.skip_while(|&&s| s != "idle")
|
|
||||||
.nth(1)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!("Failed to find idle time in finger session line: {line}")
|
|
||||||
})?
|
|
||||||
.trim_end_matches(',');
|
|
||||||
let idle_time = Self::parse_idle_time(idle_time_str)?;
|
|
||||||
|
|
||||||
let host = parts
|
|
||||||
.iter()
|
|
||||||
.skip_while(|&&s| s != "from")
|
|
||||||
.nth(1)
|
|
||||||
.unwrap_or(&"")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let messages_on = !line.ends_with("(messages off)");
|
|
||||||
|
|
||||||
Ok(Self::new(tty, login_time, idle_time, host, messages_on))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use chrono::Timelike;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_finger_raw_serialization_roundrip() {
|
|
||||||
let request = FingerRequest::new(true, "alice".to_string());
|
|
||||||
let bytes = request.to_bytes();
|
|
||||||
let deserialized = FingerRequest::from_bytes(&bytes);
|
|
||||||
assert_eq!(request, deserialized);
|
|
||||||
|
|
||||||
let request2 = FingerRequest::new(false, "bob".to_string());
|
|
||||||
let bytes2 = request2.to_bytes();
|
|
||||||
let deserialized2 = FingerRequest::from_bytes(&bytes2);
|
|
||||||
assert_eq!(request2, deserialized2);
|
|
||||||
|
|
||||||
let response = RawFingerResponse::new("Hello, World!\nThis is a test.\n".to_string());
|
|
||||||
let response_bytes = response.to_bytes();
|
|
||||||
let deserialized_response = RawFingerResponse::from_bytes(&response_bytes);
|
|
||||||
assert_eq!(response, deserialized_response);
|
|
||||||
|
|
||||||
let response2 = RawFingerResponse::new("Single line response\n".to_string());
|
|
||||||
let response_bytes2 = response2.to_bytes();
|
|
||||||
let deserialized_response2 = RawFingerResponse::from_bytes(&response_bytes2);
|
|
||||||
assert_eq!(response2, deserialized_response2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_bsd_finger_time() {
|
|
||||||
let cases = vec![
|
|
||||||
"Mon Mar 1 10:00 (UTC)",
|
|
||||||
"Tue Feb 28 23:59 (UTC)",
|
|
||||||
"Wed Dec 31 00:00 (UTC)",
|
|
||||||
"Wed Dec 31 00:00 (GMT)",
|
|
||||||
];
|
|
||||||
|
|
||||||
for input in cases {
|
|
||||||
let datetime = parse_bsd_finger_time(input);
|
|
||||||
assert!(
|
|
||||||
datetime.is_ok(),
|
|
||||||
"Failed to parse datetime for input: {}",
|
|
||||||
input
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_idle_time() {
|
|
||||||
let cases = vec![(" ", 0), ("5", 5), ("1:30", 90), ("2d", 2880)];
|
|
||||||
|
|
||||||
for (input, expected_minutes) in cases {
|
|
||||||
let duration = FingerResponseUserSession::parse_idle_time(input).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
duration.num_minutes(),
|
|
||||||
expected_minutes,
|
|
||||||
"Failed on input: {}",
|
|
||||||
input
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_finger_user_session_parsing() {
|
|
||||||
let line = "On since Mon Mar 1 10:00 (UTC) on pts/0, idle 5:00, from host.example.com";
|
|
||||||
let session = FingerResponseUserSession::try_from_finger_response_line(line).unwrap();
|
|
||||||
assert_eq!(session.tty, "pts/0");
|
|
||||||
assert_eq!(session.host, "host.example.com");
|
|
||||||
assert_eq!(session.login_time.weekday(), Weekday::Mon);
|
|
||||||
assert_eq!(session.login_time.hour(), 10);
|
|
||||||
assert_eq!(session.idle_time.num_minutes(), 300);
|
|
||||||
assert!(session.messages_on);
|
|
||||||
|
|
||||||
let line_off = "On since Mon Mar 1 10:00 (UTC) on pts/1, idle 10, from another.host.com (messages off)";
|
|
||||||
let session_off =
|
|
||||||
FingerResponseUserSession::try_from_finger_response_line(line_off).unwrap();
|
|
||||||
assert_eq!(session_off.tty, "pts/1");
|
|
||||||
assert_eq!(session_off.host, "another.host.com");
|
|
||||||
assert_eq!(session_off.login_time.weekday(), Weekday::Mon);
|
|
||||||
assert_eq!(session_off.login_time.hour(), 10);
|
|
||||||
assert_eq!(session_off.idle_time.num_minutes(), 10);
|
|
||||||
assert!(!session_off.messages_on);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_finger_user_entry_parsing_basic() {
|
|
||||||
let response_content = indoc::indoc! {"
|
|
||||||
Login: alice Name: Alice Wonderland
|
|
||||||
Directory: /home/alice Shell: /bin/bash
|
|
||||||
On since Mon Mar 1 10:00 (UTC) on pts/0, idle 5:00, from host.example.com
|
|
||||||
No Mail.
|
|
||||||
No Plan.
|
|
||||||
"}
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
let response = RawFingerResponse::from(response_content.to_string());
|
|
||||||
let user_entry =
|
|
||||||
FingerResponseUserEntry::try_from_raw_finger_response(&response, "alice".to_string())
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(user_entry.username, "alice");
|
|
||||||
assert_eq!(user_entry.full_name, "Alice Wonderland");
|
|
||||||
assert_eq!(user_entry.home_dir, PathBuf::from("/home/alice"));
|
|
||||||
assert_eq!(user_entry.shell, PathBuf::from("/bin/bash"));
|
|
||||||
assert_eq!(user_entry.sessions.len(), 1);
|
|
||||||
assert_eq!(user_entry.sessions[0].tty, "pts/0");
|
|
||||||
assert_eq!(user_entry.sessions[0].host, "host.example.com");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_finger_user_entry_parsing_single_line_office_phone() {
|
|
||||||
let response_content = indoc::indoc! {"
|
|
||||||
Login: alice Name: Alice Wonderland
|
|
||||||
Directory: /home/alice Shell: /bin/bash
|
|
||||||
Office: 123 Main St, 012-345-6789
|
|
||||||
Home Phone: +0-123-456-7890
|
|
||||||
On since Mon Mar 1 10:00 (UTC) on pts/0, idle 5:00, from host.example.com
|
|
||||||
No Mail.
|
|
||||||
No Plan.
|
|
||||||
"}
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
let response = RawFingerResponse::from(response_content.to_string());
|
|
||||||
let user_entry =
|
|
||||||
FingerResponseUserEntry::try_from_raw_finger_response(&response, "alice".to_string())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(user_entry.office, Some("123 Main St".to_string()));
|
|
||||||
assert_eq!(user_entry.office_phone, Some("012-345-6789".to_string()));
|
|
||||||
assert_eq!(user_entry.home_phone, Some("+0-123-456-7890".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_finger_user_entry_parsing_multiline_office_phone() {
|
|
||||||
let response_content = indoc::indoc! {"
|
|
||||||
Login: alice Name: Alice Wonderland
|
|
||||||
Directory: /home/alice Shell: /bin/bash
|
|
||||||
Office: 123 Main St
|
|
||||||
Office Phone: 012-345-6789
|
|
||||||
Home Phone: +0-123-456-7890
|
|
||||||
On since Mon Mar 1 10:00 (UTC) on pts/0, idle 5:00, from host.example.com
|
|
||||||
No Mail.
|
|
||||||
No Plan.
|
|
||||||
"}
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
let response = RawFingerResponse::from(response_content.to_string());
|
|
||||||
let user_entry =
|
|
||||||
FingerResponseUserEntry::try_from_raw_finger_response(&response, "alice".to_string())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(user_entry.office, Some("123 Main St".to_string()));
|
|
||||||
assert_eq!(user_entry.office_phone, Some("012-345-6789".to_string()));
|
|
||||||
assert_eq!(user_entry.home_phone, Some("+0-123-456-7890".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_finger_user_entry_parsing_never_logged_in() {
|
|
||||||
let response_content = indoc::indoc! {"
|
|
||||||
Login: bob Name: Bob Builder
|
|
||||||
Directory: /home/bob Shell: /bin/zsh
|
|
||||||
Never logged in.
|
|
||||||
No Mail.
|
|
||||||
No Plan.
|
|
||||||
"}
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
let response = RawFingerResponse::from(response_content.to_string());
|
|
||||||
let user_entry =
|
|
||||||
FingerResponseUserEntry::try_from_raw_finger_response(&response, "bob".to_string())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(user_entry.never_logged_in);
|
|
||||||
assert!(user_entry.sessions.is_empty());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod fingerd;
|
|
||||||
pub mod rwhod;
|
pub mod rwhod;
|
||||||
pub mod varlink_api;
|
pub mod varlink_api;
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
use std::{
|
|
||||||
net::hostname,
|
|
||||||
os::unix::fs::{MetadataExt, PermissionsExt},
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Timelike, Utc};
|
|
||||||
use nix::sys::stat::stat;
|
|
||||||
use uucore::utmpx::Utmpx;
|
|
||||||
|
|
||||||
use crate::proto::finger_protocol::{FingerResponseUserEntry, FingerResponseUserSession};
|
|
||||||
|
|
||||||
fn read_file_content_if_exists(path: &Path) -> anyhow::Result<Option<String>> {
|
|
||||||
let file_is_readable = path.exists()
|
|
||||||
&& path.is_file()
|
|
||||||
&& (((path.metadata()?.permissions().mode() & 0o400 != 0
|
|
||||||
&& nix::unistd::geteuid().as_raw() == path.metadata()?.uid())
|
|
||||||
|| (path.metadata()?.permissions().mode() & 0o040 != 0
|
|
||||||
&& nix::unistd::getegid().as_raw() == path.metadata()?.gid())
|
|
||||||
|| (path.metadata()?.permissions().mode() & 0o004 != 0))
|
|
||||||
|| caps::has_cap(
|
|
||||||
None,
|
|
||||||
caps::CapSet::Effective,
|
|
||||||
caps::Capability::CAP_DAC_READ_SEARCH,
|
|
||||||
)?)
|
|
||||||
&& path.metadata()?.len() > 0;
|
|
||||||
|
|
||||||
if file_is_readable {
|
|
||||||
Ok(Some(std::fs::read_to_string(path)?.trim().to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve local user information for the given username.
|
|
||||||
///
|
|
||||||
/// Returns None if the user does not exist.
|
|
||||||
pub fn get_local_user(username: &str) -> anyhow::Result<Option<FingerResponseUserEntry>> {
|
|
||||||
let username = username.to_string();
|
|
||||||
let user_entry = match nix::unistd::User::from_name(&username) {
|
|
||||||
Ok(Some(user)) => user,
|
|
||||||
Ok(None) => return Ok(None),
|
|
||||||
Err(err) => {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Failed to get user entry for {}: {}",
|
|
||||||
username,
|
|
||||||
err
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let nofinger_path = user_entry.dir.join(".nofinger");
|
|
||||||
if nofinger_path.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let full_name = user_entry.name;
|
|
||||||
let home_dir = user_entry.dir.clone();
|
|
||||||
let shell = user_entry.shell;
|
|
||||||
|
|
||||||
let gecos_fields: Vec<&str> = full_name.split(',').collect();
|
|
||||||
|
|
||||||
let office = gecos_fields.get(1).map(|s| s.to_string());
|
|
||||||
let office_phone = gecos_fields.get(2).map(|s| s.to_string());
|
|
||||||
let home_phone = gecos_fields.get(3).map(|s| s.to_string());
|
|
||||||
|
|
||||||
let hostname = hostname()?.to_str().unwrap_or("localhost").to_string();
|
|
||||||
|
|
||||||
let mut utmpx_records = Utmpx::iter_all_records()
|
|
||||||
.filter(|entry| entry.user() == username)
|
|
||||||
.filter(|entry| entry.is_user_process())
|
|
||||||
.peekable();
|
|
||||||
|
|
||||||
// TODO: Don't use utmp entries for this, read from lastlog instead
|
|
||||||
let user_never_logged_in = utmpx_records.peek().is_none();
|
|
||||||
|
|
||||||
let now = Utc::now().with_nanosecond(0).unwrap_or(Utc::now());
|
|
||||||
let sessions: Vec<FingerResponseUserSession> = utmpx_records
|
|
||||||
.filter_map(|entry| {
|
|
||||||
let login_time = entry
|
|
||||||
.login_time()
|
|
||||||
.checked_to_utc()
|
|
||||||
.and_then(|t| DateTime::<Utc>::from_timestamp_secs(t.unix_timestamp()))?;
|
|
||||||
|
|
||||||
let tty_device_stat = stat(&Path::new("/dev").join(entry.tty_device())).ok();
|
|
||||||
|
|
||||||
let idle_time = tty_device_stat
|
|
||||||
.and_then(|st| {
|
|
||||||
let last_active = DateTime::<Utc>::from_timestamp_secs(st.st_atime)?;
|
|
||||||
Some((now - last_active).max(Duration::zero()))
|
|
||||||
})
|
|
||||||
.unwrap_or(Duration::zero());
|
|
||||||
|
|
||||||
// Check if the write permission for "others" is set
|
|
||||||
let messages_on = tty_device_stat
|
|
||||||
.map(|st| st.st_mode & 0o002 != 0)
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
debug_assert!(
|
|
||||||
idle_time.num_seconds() >= 0,
|
|
||||||
"Idle time should never be negative"
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(FingerResponseUserSession::new(
|
|
||||||
entry.tty_device(),
|
|
||||||
login_time,
|
|
||||||
idle_time,
|
|
||||||
hostname.clone(),
|
|
||||||
messages_on,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let forward_path = user_entry.dir.join(".forward");
|
|
||||||
let forward = read_file_content_if_exists(&forward_path)?;
|
|
||||||
|
|
||||||
let pgpkey_path = user_entry.dir.join(".pgpkey");
|
|
||||||
let pgpkey = read_file_content_if_exists(&pgpkey_path)?;
|
|
||||||
|
|
||||||
let project_path = user_entry.dir.join(".project");
|
|
||||||
let project = read_file_content_if_exists(&project_path)?;
|
|
||||||
|
|
||||||
let plan_path = user_entry.dir.join(".plan");
|
|
||||||
let plan = read_file_content_if_exists(&plan_path)?;
|
|
||||||
|
|
||||||
Ok(Some(FingerResponseUserEntry::new(
|
|
||||||
username,
|
|
||||||
full_name,
|
|
||||||
home_dir,
|
|
||||||
shell,
|
|
||||||
office,
|
|
||||||
office_phone,
|
|
||||||
home_phone,
|
|
||||||
user_never_logged_in,
|
|
||||||
sessions,
|
|
||||||
forward,
|
|
||||||
None,
|
|
||||||
pgpkey,
|
|
||||||
project,
|
|
||||||
plan,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// /// Retrieve remote user information for the given username on the specified host.
|
|
||||||
// ///
|
|
||||||
// /// Returns None if the user does not exist or no information is available.
|
|
||||||
// async fn get_remote_user(username: &str, host: &str) -> anyhow::Result<Option<RawFingerResponse>> {
|
|
||||||
// let addr = format!("{}:79", host);
|
|
||||||
// let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
|
|
||||||
|
|
||||||
// if socket_addrs.is_empty() {
|
|
||||||
// return Err(anyhow::anyhow!(
|
|
||||||
// "Could not resolve address for host {}",
|
|
||||||
// host
|
|
||||||
// ));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let socket_addr = socket_addrs[0];
|
|
||||||
|
|
||||||
// let mut stream = TcpStream::connect(socket_addr).await?;
|
|
||||||
|
|
||||||
// let request = FingerRequest::new(false, username.to_string());
|
|
||||||
// let request_bytes = request.to_bytes();
|
|
||||||
// stream.write_all(&request_bytes).await?;
|
|
||||||
|
|
||||||
// let mut response_bytes = Vec::new();
|
|
||||||
// stream.read_to_end(&mut response_bytes).await?;
|
|
||||||
|
|
||||||
// let response = RawFingerResponse::from_bytes(&response_bytes);
|
|
||||||
|
|
||||||
// if response.is_empty() {
|
|
||||||
// Ok(None)
|
|
||||||
// } else {
|
|
||||||
// Ok(Some(response))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_finger_root() {
|
|
||||||
let user_entry = get_local_user("root").unwrap().unwrap();
|
|
||||||
assert_eq!(user_entry.username, "root");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: test serialization roundtrip
|
|
||||||
}
|
|
||||||
@@ -240,22 +240,3 @@ pub async fn rwhod_packet_sender_task(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_determine_relevant_interfaces() {
|
|
||||||
let interfaces = determine_relevant_interfaces().unwrap();
|
|
||||||
for interface in interfaces {
|
|
||||||
println!("Interface: {} Address: {}", interface.name, interface.addr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_generate_rwhod_status_update() {
|
|
||||||
let status_update = generate_rwhod_status_update().unwrap();
|
|
||||||
println!("{:?}", status_update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,30 +3,25 @@ use serde::{Deserialize, Serialize};
|
|||||||
use zlink::{ReplyError, service::MethodReply};
|
use zlink::{ReplyError, service::MethodReply};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
proto::{WhodStatusUpdate, WhodUserEntry, finger_protocol::FingerResponseUserEntry},
|
proto::{WhodStatusUpdate, WhodUserEntry, finger_protocol::FingerPerson},
|
||||||
server::{
|
server::rwhod::RwhodStatusStore,
|
||||||
fingerd::{self},
|
|
||||||
rwhod::RwhodStatusStore,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Types for 'no.ntnu.pvv.roowho2.rwhod'
|
// Types for 'no.ntnu.pvv.roowho2.rwhod'
|
||||||
|
|
||||||
#[zlink::proxy("no.ntnu.pvv.roowho2.rwhod")]
|
#[zlink::proxy("no.ntnu.pvv.roowho2.rwhod")]
|
||||||
pub trait VarlinkRwhodClientProxy {
|
pub trait RwhodClientProxy {
|
||||||
async fn rwho(
|
async fn rwho(
|
||||||
&mut self,
|
&mut self,
|
||||||
all: bool,
|
all: bool,
|
||||||
) -> zlink::Result<Result<VarlinkRwhoResponse, VarlinkRwhodClientError>>;
|
) -> zlink::Result<Result<Vec<(String, WhodUserEntry)>, RwhodClientError>>;
|
||||||
|
|
||||||
async fn ruptime(
|
async fn ruptime(&mut self) -> zlink::Result<Result<Vec<WhodStatusUpdate>, RwhodClientError>>;
|
||||||
&mut self,
|
|
||||||
) -> zlink::Result<Result<VarlinkRuptimeResponse, VarlinkRwhodClientError>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(tag = "method", content = "parameters")]
|
#[serde(tag = "method", content = "parameters")]
|
||||||
pub enum VarlinkRwhodClientRequest {
|
pub enum RwhodClientRequest {
|
||||||
#[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Rwho")]
|
#[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Rwho")]
|
||||||
Rwho {
|
Rwho {
|
||||||
/// Retrieve all users, even those that have been idle for a long time.
|
/// Retrieve all users, even those that have been idle for a long time.
|
||||||
@@ -37,50 +32,50 @@ pub enum VarlinkRwhodClientRequest {
|
|||||||
Ruptime,
|
Ruptime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum VarlinkRwhodClientResponse {
|
pub enum RwhodClientResponse {
|
||||||
Rwho(VarlinkRwhoResponse),
|
Rwho(RwhoResponse),
|
||||||
Ruptime(VarlinkRuptimeResponse),
|
Ruptime(RuptimeResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type VarlinkRwhoResponse = Vec<(String, WhodUserEntry)>;
|
pub type RwhoResponse = Vec<(String, WhodUserEntry)>;
|
||||||
pub type VarlinkRuptimeResponse = Vec<WhodStatusUpdate>;
|
pub type RuptimeResponse = Vec<WhodStatusUpdate>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, ReplyError)]
|
#[derive(Debug, Clone, PartialEq, ReplyError)]
|
||||||
#[zlink(interface = "no.ntnu.pvv.roowho2.rwhod")]
|
#[zlink(interface = "no.ntnu.pvv.roowho2.rwhod")]
|
||||||
pub enum VarlinkRwhodClientError {
|
pub enum RwhodClientError {
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types for 'no.ntnu.pvv.roowho2.finger'
|
// Types for 'no.ntnu.pvv.roowho2.finger'
|
||||||
|
|
||||||
#[zlink::proxy("no.ntnu.pvv.roowho2.finger")]
|
#[zlink::proxy("no.ntnu.pvv.roowho2.finger")]
|
||||||
pub trait VarlinkFingerClientProxy {
|
pub trait FingerClientProxy {
|
||||||
async fn finger(
|
async fn finger(
|
||||||
&mut self,
|
&mut self,
|
||||||
user_queries: Vec<String>,
|
user_queries: Vec<String>,
|
||||||
) -> zlink::Result<Result<VarlinkFingerResponse, VarlinkFingerClientError>>;
|
) -> zlink::Result<Result<Vec<FingerPerson>, FingerClientError>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(tag = "method", content = "parameters")]
|
#[serde(tag = "method", content = "parameters")]
|
||||||
pub enum VarlinkFingerClientRequest {
|
pub enum FingerClientRequest {
|
||||||
#[serde(rename = "no.ntnu.pvv.roowho2.finger.Finger")]
|
#[serde(rename = "no.ntnu.pvv.roowho2.finger.Finger")]
|
||||||
Finger { user_queries: Vec<String> },
|
Finger { user_queries: Vec<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum VarlinkFingerClientResponse {
|
pub enum FingerClientResponse {
|
||||||
Finger(VarlinkFingerResponse),
|
Finger(FingerResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type VarlinkFingerResponse = Vec<Option<FingerResponseUserEntry>>;
|
pub type FingerResponse = Vec<FingerPerson>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, ReplyError)]
|
#[derive(Debug, Clone, PartialEq, ReplyError)]
|
||||||
#[zlink(interface = "no.ntnu.pvv.roowho2.finger")]
|
#[zlink(interface = "no.ntnu.pvv.roowho2.finger")]
|
||||||
pub enum VarlinkFingerClientError {
|
pub enum FingerClientError {
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,41 +84,41 @@ pub enum VarlinkFingerClientError {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub enum VarlinkMethod {
|
enum Method {
|
||||||
Rwhod(VarlinkRwhodClientRequest),
|
Rwhod(RwhodClientRequest),
|
||||||
Finger(VarlinkFingerClientRequest),
|
Finger(FingerClientRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub enum VarlinkReply {
|
enum Reply {
|
||||||
Rwhod(VarlinkRwhodClientResponse),
|
Rwhod(RwhodClientResponse),
|
||||||
Finger(VarlinkFingerClientResponse),
|
Finger(FingerClientResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub enum VarlinkReplyError {
|
enum ReplyError {
|
||||||
Rwhod(VarlinkRwhodClientError),
|
Rwhod(RwhodClientError),
|
||||||
Finger(VarlinkFingerClientError),
|
Finger(FingerClientError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct VarlinkRoowhoo2ClientServer {
|
pub struct Roowhoo2ClientServer {
|
||||||
whod_status_store: RwhodStatusStore,
|
whod_status_store: RwhodStatusStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VarlinkRoowhoo2ClientServer {
|
impl Roowhoo2ClientServer {
|
||||||
pub fn new(whod_status_store: RwhodStatusStore) -> Self {
|
pub fn new(whod_status_store: RwhodStatusStore) -> Self {
|
||||||
Self { whod_status_store }
|
Self { whod_status_store }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VarlinkRoowhoo2ClientServer {
|
impl Roowhoo2ClientServer {
|
||||||
// TODO: handle 'all' parameter
|
// TODO: handle 'all' parameter
|
||||||
async fn handle_rwho_request(&self, _all: bool) -> VarlinkRwhoResponse {
|
async fn handle_rwho_request(&self, _all: bool) -> RwhoResponse {
|
||||||
let store = self.whod_status_store.read().await;
|
let store = self.whod_status_store.read().await;
|
||||||
|
|
||||||
let mut all_user_entries = Vec::with_capacity(store.len());
|
let mut all_user_entries = Vec::with_capacity(store.len());
|
||||||
@@ -140,50 +135,31 @@ impl VarlinkRoowhoo2ClientServer {
|
|||||||
all_user_entries
|
all_user_entries
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_ruptime_request(&self) -> VarlinkRuptimeResponse {
|
async fn handle_ruptime_request(&self) -> RuptimeResponse {
|
||||||
let store = self.whod_status_store.read().await;
|
let store = self.whod_status_store.read().await;
|
||||||
store.values().cloned().collect()
|
store.values().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_finger_request(&self, user_queries: Vec<String>) -> VarlinkFingerResponse {
|
|
||||||
user_queries
|
|
||||||
.into_iter()
|
|
||||||
.map(|username| fingerd::get_local_user(&username).unwrap())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl zlink::Service for VarlinkRoowhoo2ClientServer {
|
impl zlink::Service for Roowhoo2ClientServer {
|
||||||
type MethodCall<'de> = VarlinkMethod;
|
type MethodCall<'de> = RwhodClientRequest;
|
||||||
type ReplyParams<'se> = VarlinkReply;
|
type ReplyParams<'se> = RwhodClientResponse;
|
||||||
type ReplyStreamParams = ();
|
type ReplyStreamParams = ();
|
||||||
type ReplyStream = futures_util::stream::Empty<zlink::Reply<()>>;
|
type ReplyStream = futures_util::stream::Empty<zlink::Reply<()>>;
|
||||||
type ReplyError<'se> = VarlinkReplyError;
|
type ReplyError<'se> = RwhodClientError;
|
||||||
|
|
||||||
async fn handle<'service, Sock: zlink::connection::Socket>(
|
async fn handle<'ser, 'de: 'ser, Sock: zlink::connection::Socket>(
|
||||||
&'service mut self,
|
&'ser mut self,
|
||||||
call: &'service zlink::Call<Self::MethodCall<'_>>,
|
call: zlink::Call<Self::MethodCall<'de>>,
|
||||||
_conn: &mut zlink::Connection<Sock>,
|
_conn: &mut zlink::Connection<Sock>,
|
||||||
) -> MethodReply<Self::ReplyParams<'service>, Self::ReplyStream, Self::ReplyError<'service>>
|
) -> MethodReply<Self::ReplyParams<'ser>, Self::ReplyStream, Self::ReplyError<'ser>> {
|
||||||
{
|
|
||||||
match call.method() {
|
match call.method() {
|
||||||
VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Rwho { all }) => {
|
RwhodClientRequest::Rwho { all } => MethodReply::Single(Some(
|
||||||
MethodReply::Single(Some(VarlinkReply::Rwhod(VarlinkRwhodClientResponse::Rwho(
|
RwhodClientResponse::Rwho(self.handle_rwho_request(*all).await),
|
||||||
self.handle_rwho_request(*all).await,
|
)),
|
||||||
))))
|
RwhodClientRequest::Ruptime => MethodReply::Single(Some(RwhodClientResponse::Ruptime(
|
||||||
}
|
self.handle_ruptime_request().await,
|
||||||
VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Ruptime) => {
|
))),
|
||||||
MethodReply::Single(Some(VarlinkReply::Rwhod(
|
|
||||||
VarlinkRwhodClientResponse::Ruptime(self.handle_ruptime_request().await),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
VarlinkMethod::Finger(VarlinkFingerClientRequest::Finger { user_queries }) => {
|
|
||||||
MethodReply::Single(Some(VarlinkReply::Finger(
|
|
||||||
VarlinkFingerClientResponse::Finger(
|
|
||||||
self.handle_finger_request(user_queries.clone()).await,
|
|
||||||
),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +168,7 @@ pub async fn varlink_client_server_task(
|
|||||||
socket: zlink::unix::Listener,
|
socket: zlink::unix::Listener,
|
||||||
whod_status_store: RwhodStatusStore,
|
whod_status_store: RwhodStatusStore,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let service = VarlinkRoowhoo2ClientServer::new(whod_status_store);
|
let service = Roowhoo2ClientServer::new(whod_status_store);
|
||||||
|
|
||||||
let server = zlink::Server::new(socket, service);
|
let server = zlink::Server::new(socket, service);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user