From e0535a033b676e7a75daeec6f57aa5b5233d7c8a Mon Sep 17 00:00:00 2001 From: jasonwitty Date: Sun, 30 Nov 2025 03:34:08 -0800 Subject: [PATCH] hotfix for telemetry hotfix for profiles config --- Cargo.lock | 335 ++++++++++++++++++++++++++++++------------ Cargo.toml | 5 +- Dockerfile | 9 +- docker-compose.yml | 9 +- docker/entrypoint.sh | 21 ++- docker/init-config.sh | 34 +++-- src/analytics.rs | 161 ++++++++------------ src/server.rs | 32 ++-- 8 files changed, 378 insertions(+), 228 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f0789f..79520f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "base64 0.22.1", + "base64", "bitflags 2.10.0", "brotli", "bytes", @@ -88,7 +88,7 @@ dependencies = [ "foldhash", "futures-core", "h2", - "http", + "http 0.2.12", "httparse", "httpdate", "itoa", @@ -124,7 +124,7 @@ checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if 1.0.4", - "http", + "http 0.2.12", "regex", "regex-lite", "serde", @@ -349,18 +349,18 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -676,6 +676,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -909,6 +930,17 @@ dependencies = [ "version_check 0.9.5", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -932,7 +964,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -980,13 +1012,35 @@ dependencies = [ ] [[package]] -name = "http-body" -version = "0.4.6" +name = "http" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "http", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body", "pin-project-lite", ] @@ -1010,39 +1064,63 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.32" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2", - "http", + "http 1.4.0", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.10", + "pin-utils", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", ] [[package]] @@ -1184,6 +1262,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1226,7 +1314,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] @@ -1258,6 +1346,16 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1463,6 +1561,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1553,6 +1657,22 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "pop-telemetry" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36586f435a76531d01477ce20b10f18d79ffde9ec1bbb7ff0c91640476b11877" +dependencies = [ + "dirs", + "env_logger", + "log", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1586,7 +1706,7 @@ dependencies = [ "shared_library", "shell-words", "winapi", - "winreg 0.10.1", + "winreg", ] [[package]] @@ -1663,7 +1783,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -1675,6 +1795,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "regex" version = "1.12.2" @@ -1712,42 +1843,42 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "base64 0.21.7", + "base64", "bytes", - "encoding_rs", "futures-core", "futures-util", - "h2", - "http", + "http 1.4.0", "http-body", + "http-body-util", "hyper", "hyper-tls", - "ipnet", + "hyper-util", "js-sys", "log", - "mime", + "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", - "winreg 0.50.0", ] [[package]] @@ -1764,12 +1895,12 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-pki-types" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ - "base64 0.21.7", + "zeroize", ] [[package]] @@ -1858,6 +1989,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ + "indexmap", "itoa", "memchr", "ryu", @@ -2035,9 +2167,12 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2050,27 +2185,6 @@ dependencies = [ "syn", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tempfile" version = "3.23.0" @@ -2078,7 +2192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2225,6 +2339,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -2281,18 +2434,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" -[[package]] -name = "umami_metrics" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc9ec451bb0504e32cafb076fe46e0126c70ad167846e3de02f0a2bbebc6839" -dependencies = [ - "anyhow", - "reqwest", - "serde", - "serde_json", -] - [[package]] name = "unicase" version = "2.4.0" @@ -2444,6 +2585,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -2463,21 +2617,20 @@ dependencies = [ "actix-rt", "actix-web", "actix-web-actors", - "anyhow", "bytes", "clap", + "dirs", "env_logger", "futures", "handlebars", "libc", "log", + "pop-telemetry", "portable-pty", - "reqwest", "serde", "serde_json", "tokio", "tokio-util", - "umami_metrics", ] [[package]] @@ -2739,16 +2892,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if 1.0.4", - "windows-sys 0.48.0", -] - [[package]] name = "wit-bindgen" version = "0.46.0" @@ -2825,6 +2968,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 149f681..f2784a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,9 +29,8 @@ bytes = "1.9" log = "0.4" env_logger = "0.11" libc = "0.2" -umami_metrics = "0.1.0" -reqwest = { version = "0.11", features = ["json"] } -anyhow = "1.0" +pop-telemetry = "0.12.1" +dirs = "5.0" [lib] name = "webterm" diff --git a/Dockerfile b/Dockerfile index 6e60efe..60ccc0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # ============================================================================ # Stage 1: Rust Builder # ============================================================================ -FROM rust:1.90-slim-bookworm AS rust-builder +FROM rust:1.91-slim-bookworm AS rust-builder WORKDIR /build @@ -110,7 +110,8 @@ COPY --from=node-builder /build/node_modules ./node_modules # Copy runtime scripts COPY docker/entrypoint.sh /entrypoint.sh COPY docker/init-config.sh /init-config.sh -RUN chmod +x /entrypoint.sh /init-config.sh +COPY docker/restricted-shell.sh /usr/local/bin/restricted-shell.sh +RUN chmod +x /entrypoint.sh /init-config.sh /usr/local/bin/restricted-shell.sh # Expose ports # 8082 - webterm HTTP server @@ -124,5 +125,5 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ # Set entrypoint (init-config.sh runs as root, copies configs, then switches to socktop user) ENTRYPOINT ["/init-config.sh"] -# Default command - pass through init-config.sh to entrypoint.sh to webterm-server -CMD ["/entrypoint.sh", "webterm-server", "--host", "0.0.0.0", "--port", "8082"] +# Default command - use restricted shell that only allows socktop commands +CMD ["/entrypoint.sh", "webterm-server", "--host", "0.0.0.0", "--port", "8082", "--command", "/usr/local/bin/restricted-shell.sh"] diff --git a/docker-compose.yml b/docker-compose.yml index d134fad..8636fbe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,11 +6,10 @@ services: container_name: socktop-webterm restart: unless-stopped - # Use host network mode for direct access to host network - # This allows the container to reach your Pis on port 8443 - # Note: The containerized socktop-agent runs on port 3001 (not 3000) - # to avoid conflicts with any agent running on the host machine - network_mode: "host" + # Standard bridge networking + ports: + - "8082:8082" # Webterm HTTP server + - "3001:3001" # Socktop agent (optional) volumes: # Mount configuration files directly to proper locations diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 1956eb1..8d32091 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -56,10 +56,23 @@ setup_alacritty() { # Start socktop agent start_socktop_agent() { - echo "Starting socktop-agent on port 3000..." + echo "Starting socktop-agent on port 3001..." - # Don't start the agent here - supervisor will handle it - echo "socktop-agent will be started by supervisor" + # Start socktop-agent in the background on port 3001 + /usr/bin/socktop_agent --port 3001 > /tmp/socktop-agent.log 2>&1 & + AGENT_PID=$! + + echo "socktop-agent started (PID: $AGENT_PID)" + + # Give it a moment to start + sleep 1 + + # Check if it's running + if kill -0 $AGENT_PID 2>/dev/null; then + echo " ✓ socktop-agent is running on port 3001" + else + echo " ⚠ socktop-agent may have failed to start (check /tmp/socktop-agent.log)" + fi } # Main initialization @@ -82,7 +95,7 @@ main() { echo "" echo "Services:" echo " - Webterm: http://localhost:8082" - echo " - Socktop Agent: localhost:3001" + echo " - Socktop Agent: ws://localhost:3001/ws" echo "" # Execute the main command diff --git a/docker/init-config.sh b/docker/init-config.sh index b6cf7a2..7e77104 100644 --- a/docker/init-config.sh +++ b/docker/init-config.sh @@ -13,9 +13,18 @@ echo "===================================" SOCKTOP_HOME=$(eval echo ~socktop) echo "Socktop HOME: ${SOCKTOP_HOME}" -# Create necessary directories in the actual HOME -mkdir -p "${SOCKTOP_HOME}/.config/socktop/certs" -mkdir -p "${SOCKTOP_HOME}/.config/alacritty" +# Check if we're running as root +if [ "$(id -u)" -eq 0 ]; then + echo "Running as root, will set permissions" + # Create necessary directories in the actual HOME + mkdir -p "${SOCKTOP_HOME}/.config/socktop/certs" + mkdir -p "${SOCKTOP_HOME}/.config/alacritty" +else + echo "Running as non-root user ($(id -u)), creating directories without root" + # Try to create directories - will work if HOME is writable + mkdir -p "${SOCKTOP_HOME}/.config/socktop/certs" 2>/dev/null || echo " ⚠ Could not create directories (may already exist)" + mkdir -p "${SOCKTOP_HOME}/.config/alacritty" 2>/dev/null || true +fi # Copy files from mounted locations to actual HOME if they exist echo "Copying configuration files..." @@ -56,8 +65,11 @@ else echo " ℹ No certificates directory found (optional)" fi -# Set proper ownership -chown -R socktop:socktop "${SOCKTOP_HOME}/.config" +# Set proper ownership (only if running as root) +if [ "$(id -u)" -eq 0 ]; then + chown -R socktop:socktop "${SOCKTOP_HOME}/.config" + echo " ✓ Set ownership to socktop:socktop" +fi # Fix paths in profiles.json if it exists if [ -f "${SOCKTOP_HOME}/.config/socktop/profiles.json" ]; then @@ -70,7 +82,11 @@ fi echo "===================================" echo "Configuration initialization complete" echo "===================================" -echo "Switching to socktop user..." - -# Switch to socktop user and execute the main command -exec runuser -u socktop -- "$@" +# Switch to socktop user only if running as root +if [ "$(id -u)" -eq 0 ]; then + echo "Switching to socktop user..." + exec runuser -u socktop -- "$@" +else + echo "Already running as non-root user ($(whoami)), continuing..." + exec "$@" +fi diff --git a/src/analytics.rs b/src/analytics.rs index e56644f..e8587e1 100644 --- a/src/analytics.rs +++ b/src/analytics.rs @@ -1,16 +1,17 @@ // Copyright (c) 2024 Jason Witty . // All rights reserved. // -// Umami analytics integration for tracking terminal events +// Umami analytics integration for tracking terminal events using pop-telemetry -use anyhow::Result; +use pop_telemetry::{record_cli_command, Telemetry}; +use serde_json::json; +use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; -use umami_metrics::Umami; /// Umami analytics tracker pub struct Analytics { - client: Arc>>, + telemetry: Arc>>, enabled: bool, } @@ -18,13 +19,12 @@ impl Analytics { /// Create a new Analytics instance /// /// # Arguments - /// * `website_id` - The Umami website ID - /// * `endpoint` - The Umami instance endpoint (e.g., "http://unami.wittyoneoff.com") - pub fn new(website_id: String, endpoint: String) -> Self { - let client = Umami::new(website_id, endpoint); + /// * `config_path` - Path to the telemetry config file (for opt-out checks) + pub fn new(config_path: PathBuf) -> Self { + let telemetry = Telemetry::new(&config_path); Self { - client: Arc::new(Mutex::new(Some(client))), + telemetry: Arc::new(Mutex::new(Some(telemetry))), enabled: true, } } @@ -32,7 +32,7 @@ impl Analytics { /// Create a disabled Analytics instance (no-op) pub fn disabled() -> Self { Self { - client: Arc::new(Mutex::new(None)), + telemetry: Arc::new(Mutex::new(None)), enabled: false, } } @@ -41,138 +41,103 @@ impl Analytics { /// /// # Arguments /// * `command` - The command that was typed (will be sanitized) - /// * `user_agent` - Optional user agent string - pub async fn track_command(&self, command: &str, user_agent: Option) -> Result<()> { + /// * `_user_agent` - Optional user agent string (not used with pop-telemetry) + pub async fn track_command(&self, command: &str, _user_agent: Option) { if !self.enabled { - return Ok(()); + return; } - let client = self.client.lock().await; + let telemetry = self.telemetry.lock().await; - if let Some(umami) = client.as_ref() { + if let Some(t) = telemetry.as_ref() { // Sanitize the command for analytics let sanitized_command = sanitize_command(command); - let ua = user_agent.unwrap_or_else(|| "unknown".to_string()); + // Track as an event using pop-telemetry + let data = json!({ + "command": sanitized_command, + "type": "terminal_command" + }); - // Track as an event - match umami - .event( - "/terminal".to_string(), - "command_typed".to_string(), - ua, - "unknown".to_string(), // hostname - "unknown".to_string(), // language - sanitized_command, // event_data (the command) - ) - .await - { - Ok(_) => log::debug!("Tracked command event"), + match record_cli_command(t.clone(), "command_typed", data).await { + Ok(_) => log::debug!("Tracked command event: {}", sanitized_command), Err(e) => log::warn!("Failed to track command event: {:?}", e), } } - - Ok(()) } /// Track a page view /// /// # Arguments /// * `path` - The page path - /// * `user_agent` - Optional user agent string - pub async fn track_pageview(&self, path: &str, user_agent: Option) -> Result<()> { + /// * `_user_agent` - Optional user agent string (not used with pop-telemetry) + pub async fn track_pageview(&self, path: &str, _user_agent: Option) { if !self.enabled { - return Ok(()); + return; } - let client = self.client.lock().await; + let telemetry = self.telemetry.lock().await; - if let Some(umami) = client.as_ref() { - let ua = user_agent.unwrap_or_else(|| "unknown".to_string()); + if let Some(t) = telemetry.as_ref() { + let data = json!({ + "path": path, + "type": "pageview" + }); - match umami - .pageview( - path.to_string(), - "pageview".to_string(), - ua, - "unknown".to_string(), // hostname - "unknown".to_string(), // language - ) - .await - { + match record_cli_command(t.clone(), "pageview", data).await { Ok(_) => log::debug!("Tracked pageview: {}", path), Err(e) => log::warn!("Failed to track pageview: {:?}", e), } } - - Ok(()) } /// Track a terminal session start - pub async fn track_session_start(&self, user_agent: Option) -> Result<()> { + pub async fn track_session_start(&self, _user_agent: Option) { if !self.enabled { - return Ok(()); + return; } - let client = self.client.lock().await; + let telemetry = self.telemetry.lock().await; - if let Some(umami) = client.as_ref() { - let ua = user_agent.unwrap_or_else(|| "unknown".to_string()); + if let Some(t) = telemetry.as_ref() { + let data = json!({ + "event": "session_start", + "type": "terminal_session" + }); - match umami - .event( - "/terminal".to_string(), - "session_start".to_string(), - ua, - "unknown".to_string(), - "unknown".to_string(), - "terminal_session".to_string(), - ) - .await - { + match record_cli_command(t.clone(), "session_start", data).await { Ok(_) => log::debug!("Tracked session start"), Err(e) => log::warn!("Failed to track session start: {:?}", e), } } - - Ok(()) } /// Track a terminal session end - pub async fn track_session_end(&self, user_agent: Option) -> Result<()> { + pub async fn track_session_end(&self, _user_agent: Option) { if !self.enabled { - return Ok(()); + return; } - let client = self.client.lock().await; + let telemetry = self.telemetry.lock().await; - if let Some(umami) = client.as_ref() { - let ua = user_agent.unwrap_or_else(|| "unknown".to_string()); + if let Some(t) = telemetry.as_ref() { + let data = json!({ + "event": "session_end", + "type": "terminal_session" + }); - match umami - .event( - "/terminal".to_string(), - "session_end".to_string(), - ua, - "unknown".to_string(), - "unknown".to_string(), - "terminal_session".to_string(), - ) - .await - { + match record_cli_command(t.clone(), "session_end", data).await { Ok(_) => log::debug!("Tracked session end"), Err(e) => log::warn!("Failed to track session end: {:?}", e), } } - - Ok(()) } } impl Clone for Analytics { fn clone(&self) -> Self { Self { - client: Arc::clone(&self.client), + telemetry: Arc::clone(&self.telemetry), enabled: self.enabled, } } @@ -212,19 +177,19 @@ fn sanitize_command(command: &str) -> String { ]; if sensitive_commands.iter().any(|&cmd| base_cmd.contains(cmd)) { - return format!("{} [REDACTED]", base_cmd); + return format!("{}_REDACTED", base_cmd); } // For common safe commands, keep the command and count of args let safe_commands = [ "ls", "cd", "pwd", "cat", "less", "more", "head", "tail", "echo", "grep", "find", "which", "whoami", "date", "cal", "clear", "exit", "history", "man", "help", "top", "htop", "ps", - "kill", "df", "du", "free", "uptime", "uname", + "kill", "df", "du", "free", "uptime", "uname", "socktop", ]; if safe_commands.contains(&base_cmd) { if words.len() > 1 { - return format!("{} +{} args", base_cmd, words.len() - 1); + return format!("{}_with_{}_args", base_cmd, words.len() - 1); } else { return base_cmd.to_string(); } @@ -247,23 +212,25 @@ mod tests { #[test] fn test_sanitize_safe_commands() { assert_eq!(sanitize_command("ls"), "ls"); - assert_eq!(sanitize_command("ls -la"), "ls +1 args"); - assert_eq!(sanitize_command("cd /tmp"), "cd +1 args"); + assert_eq!(sanitize_command("ls -la"), "ls_with_1_args"); + assert_eq!(sanitize_command("cd /tmp"), "cd_with_1_args"); assert_eq!(sanitize_command("pwd"), "pwd"); + assert_eq!(sanitize_command("socktop"), "socktop"); + assert_eq!(sanitize_command("socktop -P local"), "socktop_with_2_args"); } #[test] fn test_sanitize_sensitive_commands() { - assert_eq!(sanitize_command("ssh user@host"), "ssh [REDACTED]"); + assert_eq!(sanitize_command("ssh user@host"), "ssh_REDACTED"); assert_eq!( sanitize_command("mysql -u root -p password"), - "mysql [REDACTED]" + "mysql_REDACTED" ); assert_eq!( sanitize_command("curl https://api.com/secret"), - "curl [REDACTED]" + "curl_REDACTED" ); - assert_eq!(sanitize_command("sudo rm -rf /"), "sudo [REDACTED]"); + assert_eq!(sanitize_command("sudo rm -rf /"), "sudo_REDACTED"); } #[test] @@ -281,7 +248,7 @@ mod tests { #[tokio::test] async fn test_track_command_disabled() { let analytics = Analytics::disabled(); - let result = analytics.track_command("ls -la", None).await; - assert!(result.is_ok()); + // Should not panic or error when disabled + analytics.track_command("ls -la", None).await; } } diff --git a/src/server.rs b/src/server.rs index c687edd..6282dd2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,6 +2,7 @@ use actix_web::{App, HttpServer}; use clap::Parser; use webterm::{validate_command, Analytics, WebTermExt}; +use std::path::PathBuf; use std::process::Command; #[derive(Parser, Debug)] @@ -24,13 +25,9 @@ struct Opt { #[arg(long, default_value = "true")] enable_analytics: bool, - /// Umami instance endpoint - #[arg(long, default_value = "http://unami.wittyoneoff.com")] - umami_endpoint: String, - - /// Umami website ID - #[arg(long, default_value = "caefa16f-86af-4835-8b82-c8649aea0e2a")] - umami_website_id: String, + /// Path to telemetry config file (for opt-out management) + #[arg(long)] + telemetry_config: Option, } #[actix_web::main] @@ -63,12 +60,21 @@ async fn main() -> std::io::Result<()> { // Initialize analytics let analytics = if opt.enable_analytics { - log::info!( - "Analytics enabled: {} (website_id: {})", - opt.umami_endpoint, - opt.umami_website_id - ); - Analytics::new(opt.umami_website_id.clone(), opt.umami_endpoint.clone()) + // Use default config path if not specified + let config_path = opt.telemetry_config.unwrap_or_else(|| { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("webterm") + .join("telemetry.json") + }); + + // Create config directory if it doesn't exist + if let Some(parent) = config_path.parent() { + std::fs::create_dir_all(parent).ok(); + } + + log::info!("Analytics enabled (config: {:?})", config_path); + Analytics::new(config_path) } else { log::info!("Analytics disabled"); Analytics::disabled()