From 84c78652115a6b42b365e2298574e86c3b02c935 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 11 Jan 2026 14:30:00 +0100 Subject: [PATCH 01/18] feat: init cargo workspace with rust nix flake --- Cargo.toml | 13 ++ crates/funnel-client/Cargo.toml | 19 +++ crates/funnel-client/src/main.rs | 3 + crates/funnel-core/Cargo.toml | 18 ++ crates/funnel-core/src/error.rs | 19 +++ crates/funnel-core/src/lib.rs | 3 + crates/funnel-core/src/protocol/message.rs | 186 +++++++++++++++++++++ crates/funnel-core/src/protocol/mod.rs | 3 + crates/funnel-core/src/tunnel/id.rs | 179 ++++++++++++++++++++ crates/funnel-core/src/tunnel/mod.rs | 3 + crates/funnel-server/Cargo.toml | 30 ++++ crates/funnel-server/src/main.rs | 3 + crates/funnel-web/Cargo.toml | 10 ++ crates/funnel-web/src/lib.rs | 1 + flake.nix | 133 +++++---------- nix/build-inputs.nix | 25 +++ nix/default.nix | 6 + nix/dev-tools.nix | 18 ++ nix/shells.nix | 56 +++++++ nix/toolchains.nix | 49 ++++++ 20 files changed, 688 insertions(+), 89 deletions(-) create mode 100644 Cargo.toml create mode 100644 crates/funnel-client/Cargo.toml create mode 100644 crates/funnel-client/src/main.rs create mode 100644 crates/funnel-core/Cargo.toml create mode 100644 crates/funnel-core/src/error.rs create mode 100644 crates/funnel-core/src/lib.rs create mode 100644 crates/funnel-core/src/protocol/message.rs create mode 100644 crates/funnel-core/src/protocol/mod.rs create mode 100644 crates/funnel-core/src/tunnel/id.rs create mode 100644 crates/funnel-core/src/tunnel/mod.rs create mode 100644 crates/funnel-server/Cargo.toml create mode 100644 crates/funnel-server/src/main.rs create mode 100644 crates/funnel-web/Cargo.toml create mode 100644 crates/funnel-web/src/lib.rs create mode 100644 nix/build-inputs.nix create mode 100644 nix/default.nix create mode 100644 nix/dev-tools.nix create mode 100644 nix/shells.nix create mode 100644 nix/toolchains.nix diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7a6cf66 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +resolver = "3" +members = [ + "crates/funnel-core", + "crates/funnel-server", + "crates/funnel-client", + "crates/funnel-web", +] + +[workspace.package] +edition = "2024" +license = "MIT" +repository = "https://github.com/karol-broda/funnel" diff --git a/crates/funnel-client/Cargo.toml b/crates/funnel-client/Cargo.toml new file mode 100644 index 0000000..5e9b894 --- /dev/null +++ b/crates/funnel-client/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "funnel-client" +version = "0.1.0" +edition = "2024" + +[dependencies] +funnel-core = { path = "../funnel-core" } + +anyhow = "1" +clap = { version = "4", features = ["derive"] } +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] } +toml = "0.8" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +uuid = { version = "1", features = ["v7"] } diff --git a/crates/funnel-client/src/main.rs b/crates/funnel-client/src/main.rs new file mode 100644 index 0000000..ae062fd --- /dev/null +++ b/crates/funnel-client/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("funnel-client"); +} diff --git a/crates/funnel-core/Cargo.toml b/crates/funnel-core/Cargo.toml new file mode 100644 index 0000000..c1603a6 --- /dev/null +++ b/crates/funnel-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "funnel-core" +version = "0.1.0" +edition = "2024" + +[dependencies] +base64 = "0.22" +bytes = "1" +http = "1" +nanoid = "0.4" +regex = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +uuid = { version = "1", features = ["v7", "serde"] } + +[dev-dependencies] +pretty_assertions = "1" diff --git a/crates/funnel-core/src/error.rs b/crates/funnel-core/src/error.rs new file mode 100644 index 0000000..3876b98 --- /dev/null +++ b/crates/funnel-core/src/error.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ValidationError { + #[error("tunnel ID cannot be empty")] + Empty, + + #[error("tunnel ID must be at least 3 characters, got {0}")] + TooShort(usize), + + #[error("tunnel ID must be at most 63 characters, got {0}")] + TooLong(usize), + + #[error("tunnel ID must be lowercase")] + NotLowercase, + + #[error("tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen")] + InvalidFormat, +} diff --git a/crates/funnel-core/src/lib.rs b/crates/funnel-core/src/lib.rs new file mode 100644 index 0000000..7aed731 --- /dev/null +++ b/crates/funnel-core/src/lib.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod protocol; +pub mod tunnel; diff --git a/crates/funnel-core/src/protocol/message.rs b/crates/funnel-core/src/protocol/message.rs new file mode 100644 index 0000000..4d22e4e --- /dev/null +++ b/crates/funnel-core/src/protocol/message.rs @@ -0,0 +1,186 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::tunnel::TunnelId; + +/// HTTP request payload forwarded through the tunnel. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequestPayload { + pub method: String, + pub path: String, + pub headers: HashMap>, + #[serde(with = "base64_bytes")] + pub body: Vec, +} + +/// HTTP response payload returned through the tunnel. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResponsePayload { + pub status: u16, + pub headers: HashMap>, + #[serde(with = "base64_bytes")] + pub body: Vec, +} + +/// A message sent over the WebSocket tunnel connection. +/// +/// Uses a tagged enum (`"type"` field) for clean pattern matching +/// instead of stringly-typed message kinds. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum TunnelMessage { + Request { + tunnel_id: TunnelId, + request_id: Uuid, + payload: RequestPayload, + }, + Response { + request_id: Uuid, + payload: ResponsePayload, + }, + Ping, + Pong, + RequestCancel { + request_id: Uuid, + }, +} + +/// Serde helper for encoding `Vec` as base64 strings in JSON. +mod base64_bytes { + use base64::prelude::*; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &Vec, s: S) -> Result { + s.serialize_str(&BASE64_STANDARD.encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let s = String::deserialize(d)?; + BASE64_STANDARD.decode(&s).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize_request() { + let msg = TunnelMessage::Request { + tunnel_id: TunnelId::new("my-tunnel").unwrap(), + request_id: Uuid::nil(), + payload: RequestPayload { + method: "GET".into(), + path: "/hello".into(), + headers: HashMap::new(), + body: vec![], + }, + }; + + let json = serde_json::to_value(&msg).unwrap(); + assert_eq!(json["type"], "request"); + assert_eq!(json["tunnel_id"], "my-tunnel"); + assert_eq!(json["payload"]["method"], "GET"); + } + + #[test] + fn serialize_response() { + let msg = TunnelMessage::Response { + request_id: Uuid::nil(), + payload: ResponsePayload { + status: 200, + headers: HashMap::new(), + body: b"hello world".to_vec(), + }, + }; + + let json = serde_json::to_value(&msg).unwrap(); + assert_eq!(json["type"], "response"); + assert_eq!(json["payload"]["status"], 200); + } + + #[test] + fn serialize_ping_pong() { + let ping_json = serde_json::to_value(&TunnelMessage::Ping).unwrap(); + assert_eq!(ping_json["type"], "ping"); + + let pong_json = serde_json::to_value(&TunnelMessage::Pong).unwrap(); + assert_eq!(pong_json["type"], "pong"); + } + + #[test] + fn serialize_request_cancel() { + let id = Uuid::nil(); + let msg = TunnelMessage::RequestCancel { request_id: id }; + let json = serde_json::to_value(&msg).unwrap(); + assert_eq!(json["type"], "request_cancel"); + assert_eq!(json["request_id"], id.to_string()); + } + + #[test] + fn roundtrip_request() { + let msg = TunnelMessage::Request { + tunnel_id: TunnelId::new("test-abc").unwrap(), + request_id: Uuid::now_v7(), + payload: RequestPayload { + method: "POST".into(), + path: "/api/data".into(), + headers: { + let mut h = HashMap::new(); + h.insert("Content-Type".into(), vec!["application/json".into()]); + h + }, + body: b"{\"key\": \"value\"}".to_vec(), + }, + }; + + let json = serde_json::to_string(&msg).unwrap(); + let parsed: TunnelMessage = serde_json::from_str(&json).unwrap(); + + match parsed { + TunnelMessage::Request { + tunnel_id, + payload, + .. + } => { + assert_eq!(tunnel_id.as_ref(), "test-abc"); + assert_eq!(payload.method, "POST"); + assert_eq!(payload.body, b"{\"key\": \"value\"}"); + } + _ => panic!("expected Request variant"), + } + } + + #[test] + fn roundtrip_response_with_body() { + let body = vec![0u8, 1, 2, 255, 128]; + let msg = TunnelMessage::Response { + request_id: Uuid::now_v7(), + payload: ResponsePayload { + status: 201, + headers: HashMap::new(), + body: body.clone(), + }, + }; + + let json = serde_json::to_string(&msg).unwrap(); + let parsed: TunnelMessage = serde_json::from_str(&json).unwrap(); + + match parsed { + TunnelMessage::Response { payload, .. } => { + assert_eq!(payload.status, 201); + assert_eq!(payload.body, body); + } + _ => panic!("expected Response variant"), + } + } + + #[test] + fn deserialize_unknown_type_fails() { + let json = r#"{"type": "unknown_msg"}"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err()); + } +} diff --git a/crates/funnel-core/src/protocol/mod.rs b/crates/funnel-core/src/protocol/mod.rs new file mode 100644 index 0000000..8805705 --- /dev/null +++ b/crates/funnel-core/src/protocol/mod.rs @@ -0,0 +1,3 @@ +mod message; + +pub use message::{RequestPayload, ResponsePayload, TunnelMessage}; diff --git a/crates/funnel-core/src/tunnel/id.rs b/crates/funnel-core/src/tunnel/id.rs new file mode 100644 index 0000000..d21443a --- /dev/null +++ b/crates/funnel-core/src/tunnel/id.rs @@ -0,0 +1,179 @@ +use std::fmt; + +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use crate::error::ValidationError; + +const DOMAIN_SAFE_ALPHABET: &[char] = &[ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', +]; + +const DEFAULT_ID_LENGTH: usize = 8; + +/// A validated tunnel identifier. +/// +/// Guaranteed to be 3-63 characters, lowercase alphanumeric with hyphens, +/// and not starting or ending with a hyphen. Safe for use as a DNS subdomain label. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] +pub struct TunnelId(String); + +impl TunnelId { + /// Parse and validate a raw string as a tunnel ID. + pub fn new(raw: impl Into) -> Result { + let s = raw.into(); + Self::validate(&s)?; + Ok(Self(s)) + } + + /// Generate a random domain-safe tunnel ID. + pub fn generate() -> Self { + let id = nanoid::nanoid!(DEFAULT_ID_LENGTH, DOMAIN_SAFE_ALPHABET); + Self(id) + } + + fn validate(s: &str) -> Result<(), ValidationError> { + if s.is_empty() { + return Err(ValidationError::Empty); + } + if s.len() < 3 { + return Err(ValidationError::TooShort(s.len())); + } + if s.len() > 63 { + return Err(ValidationError::TooLong(s.len())); + } + + let lower = s.to_lowercase(); + if s != lower { + return Err(ValidationError::NotLowercase); + } + + // Must match: starts and ends with alnum, middle can have hyphens + let re = Regex::new(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$").expect("valid regex"); + if !re.is_match(s) { + return Err(ValidationError::InvalidFormat); + } + + Ok(()) + } +} + +impl AsRef for TunnelId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for TunnelId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl From for String { + fn from(id: TunnelId) -> Self { + id.0 + } +} + +impl TryFrom for TunnelId { + type Error = ValidationError; + + fn try_from(s: String) -> Result { + Self::new(s) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_ids() { + assert!(TunnelId::new("abc").is_ok()); + assert!(TunnelId::new("my-tunnel").is_ok()); + assert!(TunnelId::new("test-123").is_ok()); + assert!(TunnelId::new("a1b").is_ok()); + assert!(TunnelId::new("a".repeat(63)).is_ok()); + } + + #[test] + fn empty_id() { + assert!(matches!(TunnelId::new(""), Err(ValidationError::Empty))); + } + + #[test] + fn too_short() { + assert!(matches!(TunnelId::new("ab"), Err(ValidationError::TooShort(2)))); + assert!(matches!(TunnelId::new("a"), Err(ValidationError::TooShort(1)))); + } + + #[test] + fn too_long() { + let long = "a".repeat(64); + assert!(matches!(TunnelId::new(long), Err(ValidationError::TooLong(64)))); + } + + #[test] + fn uppercase_rejected() { + assert!(matches!( + TunnelId::new("MyTunnel"), + Err(ValidationError::NotLowercase) + )); + } + + #[test] + fn invalid_format() { + assert!(matches!( + TunnelId::new("-abc"), + Err(ValidationError::InvalidFormat) + )); + assert!(matches!( + TunnelId::new("abc-"), + Err(ValidationError::InvalidFormat) + )); + assert!(matches!( + TunnelId::new("ab_c"), + Err(ValidationError::InvalidFormat) + )); + assert!(matches!( + TunnelId::new("ab c"), + Err(ValidationError::InvalidFormat) + )); + } + + #[test] + fn generate_produces_valid_id() { + for _ in 0..100 { + let id = TunnelId::generate(); + assert_eq!(id.as_ref().len(), DEFAULT_ID_LENGTH); + assert!(TunnelId::new(id.as_ref()).is_ok()); + } + } + + #[test] + fn display_and_into_string() { + let id = TunnelId::new("my-tunnel").unwrap(); + assert_eq!(id.to_string(), "my-tunnel"); + let s: String = id.into(); + assert_eq!(s, "my-tunnel"); + } + + #[test] + fn serde_roundtrip() { + let id = TunnelId::new("test-123").unwrap(); + let json = serde_json::to_string(&id).unwrap(); + assert_eq!(json, "\"test-123\""); + + let parsed: TunnelId = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.as_ref(), "test-123"); + } + + #[test] + fn serde_rejects_invalid() { + let result: Result = serde_json::from_str("\"AB\""); + assert!(result.is_err()); + } +} diff --git a/crates/funnel-core/src/tunnel/mod.rs b/crates/funnel-core/src/tunnel/mod.rs new file mode 100644 index 0000000..0c73029 --- /dev/null +++ b/crates/funnel-core/src/tunnel/mod.rs @@ -0,0 +1,3 @@ +mod id; + +pub use id::TunnelId; diff --git a/crates/funnel-server/Cargo.toml b/crates/funnel-server/Cargo.toml new file mode 100644 index 0000000..8b075c8 --- /dev/null +++ b/crates/funnel-server/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "funnel-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +funnel-core = { path = "../funnel-core" } + +anyhow = "1" +argon2 = "0.5" +axum = { version = "0.8", features = ["ws"] } +chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4", features = ["derive"] } +dashmap = "6" +jsonwebtoken = "9" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono"] } +thiserror = "2" +tokio = { version = "1", features = ["full"] } +tower = "0.5" +tower-http = { version = "0.6", features = ["cors", "trace"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +uuid = { version = "1", features = ["v7", "serde"] } + +[dev-dependencies] +axum-test = "16" +pretty_assertions = "1" +sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono"] } diff --git a/crates/funnel-server/src/main.rs b/crates/funnel-server/src/main.rs new file mode 100644 index 0000000..d8cea63 --- /dev/null +++ b/crates/funnel-server/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("funnel-server"); +} diff --git a/crates/funnel-web/Cargo.toml b/crates/funnel-web/Cargo.toml new file mode 100644 index 0000000..0945db1 --- /dev/null +++ b/crates/funnel-web/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "funnel-web" +version = "0.1.0" +edition = "2024" + +[dependencies] +funnel-core = { path = "../funnel-core" } + +leptos = { version = "0.7", features = ["csr"] } +serde = { version = "1", features = ["derive"] } diff --git a/crates/funnel-web/src/lib.rs b/crates/funnel-web/src/lib.rs new file mode 100644 index 0000000..e8c5030 --- /dev/null +++ b/crates/funnel-web/src/lib.rs @@ -0,0 +1 @@ +// Leptos frontend - to be implemented in Phase 3 diff --git a/flake.nix b/flake.nix index 2482201..7d5fec3 100644 --- a/flake.nix +++ b/flake.nix @@ -1,96 +1,51 @@ { - description = "go 1.25.0 dev flake"; + description = "rust development flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; - systems.url = "github:nix-systems/default"; + nixpkgs = { + url = "github:NixOS/nixpkgs/nixos-25.05"; + }; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + systems = { + url = "github:nix-systems/default"; + }; }; - outputs = { self, nixpkgs, systems }: - let - supportedSystems = import systems; - forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system); - in - { - overlays.default = final: prev: - let - version = "1.25.0"; - - platformInfo = { - "x86_64-linux" = { suffix = "linux-amd64"; sri = "sha256-KFKvDLIKExObNEiZLmm4aOUO0Pih5ZQO4d6eGaEjthM="; }; - "aarch64-linux" = { suffix = "linux-arm64"; sri = "sha256-Bd511plKJ4NpmBXuVTvVqTJ9i3mZHeNuOLZoYngvVK4="; }; - "i686-linux" = { suffix = "linux-386"; sri = "sha256-jGAt2dmbyUU7OZXSDOS684LMUIVZAKDs5d6ZKd9KmTo="; }; - "armv6l-linux" = { suffix = "linux-armv6l"; sri = "sha256-paj4GY/PAOHkhbjs757gIHeL8ypAik6Iczcb/ORYzQk="; }; - - "x86_64-darwin" = { suffix = "darwin-amd64"; sri = "sha256-W9YOgjA3BiwjB8cegRGAmGURZxTW9rQQWXz1B139gO8="; }; - "aarch64-darwin" = { suffix = "darwin-arm64"; sri = "sha256-VEkyhEFW2Bcveij3fyrJwVojBGaYtiQ/YzsKCwDAdJw="; }; - }; - - hostSystem = prev.stdenv.hostPlatform.system; - - chosen = - if prev.lib.hasAttr hostSystem platformInfo then platformInfo.${hostSystem} - else - throw '' - unsupported system: ${hostSystem} - add a mapping for your platform using the upstream tarball + sri sha256 - ''; - in - { - go_1_25_bin = prev.stdenvNoCC.mkDerivation { - pname = "go"; - version = version; - - src = prev.fetchurl { - url = "https://go.dev/dl/go${version}.${chosen.suffix}.tar.gz"; - hash = chosen.sri; - }; - - dontBuild = true; - - installPhase = '' - runHook preInstall - mkdir -p "$out"/{bin,share} - tar -C "$TMPDIR" -xzf "$src" - cp -a "$TMPDIR/go" "$out/share/go" - ln -s "$out/share/go/bin/go" "$out/bin/go" - ln -s "$out/share/go/bin/gofmt" "$out/bin/gofmt" - runHook postInstall - ''; - - dontPatchELF = true; - dontStrip = true; - - meta = with prev.lib; { - description = "go compiler and tools v${version}"; - homepage = "https://go.dev/dl/"; - license = licenses.bsd3; - platforms = [ hostSystem ]; - }; - }; - }; - - packages = forAllSystems (system: - let pkgs = import nixpkgs { inherit system; overlays = [ self.overlays.default ]; }; - in { - default = pkgs.go_1_25_bin; - go_1_25_bin = pkgs.go_1_25_bin; - } - ); - - devShells = forAllSystems (system: - let pkgs = import nixpkgs { inherit system; overlays = [ self.overlays.default ]; }; - in { - default = pkgs.mkShell { - packages = [ pkgs.go_1_25_bin pkgs.git ]; - - GOTOOLCHAIN = "local"; - - shellHook = '' - echo "go toolchain: $(go version)" - ''; - }; - } - ); + outputs = { + self, + nixpkgs, + rust-overlay, + systems, + }: let + supportedSystems = import systems; + forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system); + + mkPkgs = system: + import nixpkgs { + inherit system; + overlays = [ + rust-overlay.overlays.default + ]; + }; + + mkModules = system: + import ./nix { + pkgs = mkPkgs system; + }; + in { + devShells = forAllSystems ( + system: let + modules = mkModules system; + in { + inherit (modules.shells) default nightly wasm; + } + ); + + overlays = { + default = rust-overlay.overlays.default; }; + }; } diff --git a/nix/build-inputs.nix b/nix/build-inputs.nix new file mode 100644 index 0000000..705b03d --- /dev/null +++ b/nix/build-inputs.nix @@ -0,0 +1,25 @@ +{pkgs}: { + buildInputs = with pkgs; + [ + openssl + zlib + ] + ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.SystemConfiguration + darwin.apple_sdk.frameworks.CoreFoundation + darwin.apple_sdk.frameworks.CoreServices + libiconv + ]; + + nativeBuildInputs = with pkgs; [ + pkg-config + ]; + + env = { + OPENSSL_NO_VENDOR = "1"; + OPENSSL_DIR = "${pkgs.openssl.dev}"; + OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib"; + OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include"; + }; +} diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000..cab37c3 --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,6 @@ +{pkgs}: let + toolchains = import ./toolchains.nix {inherit pkgs;}; + buildInputs = import ./build-inputs.nix {inherit pkgs;}; + devTools = import ./dev-tools.nix {inherit pkgs;}; + shells = import ./shells.nix {inherit pkgs toolchains buildInputs devTools;}; +in {inherit toolchains buildInputs devTools shells;} diff --git a/nix/dev-tools.nix b/nix/dev-tools.nix new file mode 100644 index 0000000..bbbab70 --- /dev/null +++ b/nix/dev-tools.nix @@ -0,0 +1,18 @@ +{pkgs}: +with pkgs; [ + cargo-watch + cargo-edit + cargo-expand + cargo-outdated + cargo-audit + cargo-nextest + cargo-deny + cargo-machete + cargo-flamegraph + cargo-bloat + + bacon + just + hyperfine + tokei +] diff --git a/nix/shells.nix b/nix/shells.nix new file mode 100644 index 0000000..7da23b5 --- /dev/null +++ b/nix/shells.nix @@ -0,0 +1,56 @@ +{ + pkgs, + toolchains, + buildInputs, + devTools, +}: let + mkRustShell = { + toolchain, + includeDevTools ? true, + extraPackages ? [], + extraBuildInputs ? [], + extraEnv ? {}, + shellHookExtra ? "", + }: + pkgs.mkShell ({ + packages = + [toolchain] + ++ buildInputs.nativeBuildInputs + ++ pkgs.lib.optionals includeDevTools devTools + ++ extraPackages; + + buildInputs = buildInputs.buildInputs ++ extraBuildInputs; + + RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library"; + + shellHook = '' + if [ -n "$PS1" ]; then + echo "rust dev shell" + echo " rustc: $(rustc --version)" + echo " cargo: $(cargo --version)" + fi + ${shellHookExtra} + ''; + } + // buildInputs.env // extraEnv); +in { + default = mkRustShell { + toolchain = toolchains.default; + }; + + nightly = mkRustShell { + toolchain = toolchains.nightly; + }; + + wasm = mkRustShell { + toolchain = toolchains.wasm; + extraPackages = with pkgs; [ + wasm-pack + wasm-bindgen-cli + wasmtime + binaryen + ]; + }; + + inherit mkRustShell; +} diff --git a/nix/toolchains.nix b/nix/toolchains.nix new file mode 100644 index 0000000..4c3cf5b --- /dev/null +++ b/nix/toolchains.nix @@ -0,0 +1,49 @@ +{pkgs}: { + default = pkgs.rust-bin.stable.latest.default.override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + "rustfmt" + ]; + }; + + nightly = pkgs.rust-bin.nightly.latest.default.override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + "rustfmt" + "miri" + "llvm-tools" + ]; + }; + + wasm = pkgs.rust-bin.stable.latest.default.override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + "rustfmt" + ]; + targets = [ + "wasm32-unknown-unknown" + "wasm32-wasip1" + ]; + }; + + custom = { + channel ? "stable", + version ? "latest", + extensions ? [], + targets ? [], + }: let + base = + if version == "latest" + then pkgs.rust-bin.${channel}.latest.default + else pkgs.rust-bin.${channel}.${version}.default; + in + base.override { + inherit extensions targets; + }; +} From 93761719348fbf895a6db8234d4e25e3f81434eb Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Mon, 12 Jan 2026 11:15:00 +0100 Subject: [PATCH 02/18] feat(core): add tunnel id, message protocol, and api key types --- crates/funnel-core/Cargo.toml | 3 + crates/funnel-core/src/auth/mod.rs | 3 + crates/funnel-core/src/auth/token.rs | 115 +++++++++++++++++++++++++++ crates/funnel-core/src/lib.rs | 1 + 4 files changed, 122 insertions(+) create mode 100644 crates/funnel-core/src/auth/mod.rs create mode 100644 crates/funnel-core/src/auth/token.rs diff --git a/crates/funnel-core/Cargo.toml b/crates/funnel-core/Cargo.toml index c1603a6..18a0fcc 100644 --- a/crates/funnel-core/Cargo.toml +++ b/crates/funnel-core/Cargo.toml @@ -6,11 +6,14 @@ edition = "2024" [dependencies] base64 = "0.22" bytes = "1" +getrandom = "0.3" +hex = "0.4" http = "1" nanoid = "0.4" regex = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.10" thiserror = "2" uuid = { version = "1", features = ["v7", "serde"] } diff --git a/crates/funnel-core/src/auth/mod.rs b/crates/funnel-core/src/auth/mod.rs new file mode 100644 index 0000000..8798cec --- /dev/null +++ b/crates/funnel-core/src/auth/mod.rs @@ -0,0 +1,3 @@ +mod token; + +pub use token::{ApiKeyPrefix, hash_token, generate_api_key}; diff --git a/crates/funnel-core/src/auth/token.rs b/crates/funnel-core/src/auth/token.rs new file mode 100644 index 0000000..2fa59a4 --- /dev/null +++ b/crates/funnel-core/src/auth/token.rs @@ -0,0 +1,115 @@ +use std::fmt; + +use base64::prelude::*; +use serde::{Deserialize, Serialize}; + +const API_KEY_PREFIX: &str = "sk_"; +const API_KEY_RANDOM_BYTES: usize = 32; +const DISPLAY_PREFIX_LEN: usize = 10; + +/// The visible prefix of an API key (e.g. `"sk_abc123..."`), safe for display and logging. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ApiKeyPrefix(String); + +impl ApiKeyPrefix { + pub fn from_key(full_key: &str) -> Self { + let end = full_key.len().min(DISPLAY_PREFIX_LEN); + Self(full_key[..end].to_string()) + } +} + +impl AsRef for ApiKeyPrefix { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for ApiKeyPrefix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}...", self.0) + } +} + +/// Generate a cryptographically random API key with the `sk_` prefix. +/// +/// Returns the full plaintext key. This should be shown to the user exactly once +/// and then only stored as a hash. +pub fn generate_api_key() -> String { + let mut bytes = [0u8; API_KEY_RANDOM_BYTES]; + getrandom::fill(&mut bytes).expect("failed to generate random bytes"); + format!("{}{}", API_KEY_PREFIX, BASE64_URL_SAFE_NO_PAD.encode(bytes)) +} + +/// Hash a plaintext API key using SHA-256. +/// +/// This is used for storage and constant-time comparison. We use SHA-256 rather +/// than argon2 for API keys because they are high-entropy random tokens, not +/// user-chosen passwords, so brute-force resistance from a slow hash is unnecessary. +pub fn hash_token(token: &str) -> String { + use sha2::{Digest, Sha256}; + let hash = Sha256::digest(token.as_bytes()); + hex::encode(hash) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_key_has_prefix() { + let key = generate_api_key(); + assert!(key.starts_with("sk_")); + } + + #[test] + fn generate_key_has_sufficient_length() { + let key = generate_api_key(); + // sk_ (3) + base64 of 32 bytes (43) = 46 chars + assert!(key.len() >= 40); + } + + #[test] + fn generated_keys_are_unique() { + let a = generate_api_key(); + let b = generate_api_key(); + assert_ne!(a, b); + } + + #[test] + fn hash_is_deterministic() { + let key = "sk_test_token_123"; + assert_eq!(hash_token(key), hash_token(key)); + } + + #[test] + fn hash_differs_for_different_inputs() { + assert_ne!(hash_token("sk_aaa"), hash_token("sk_bbb")); + } + + #[test] + fn hash_is_hex_encoded_sha256() { + let hash = hash_token("test"); + assert_eq!(hash.len(), 64); // SHA-256 = 32 bytes = 64 hex chars + assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn prefix_from_key() { + let key = generate_api_key(); + let prefix = ApiKeyPrefix::from_key(&key); + assert_eq!(prefix.as_ref().len(), DISPLAY_PREFIX_LEN); + assert!(key.starts_with(prefix.as_ref())); + } + + #[test] + fn prefix_display() { + let prefix = ApiKeyPrefix::from_key("sk_abcdefghij_rest"); + assert_eq!(format!("{}", prefix), "sk_abcdefg..."); + } + + #[test] + fn prefix_from_short_key() { + let prefix = ApiKeyPrefix::from_key("sk_ab"); + assert_eq!(prefix.as_ref(), "sk_ab"); + } +} diff --git a/crates/funnel-core/src/lib.rs b/crates/funnel-core/src/lib.rs index 7aed731..3ba0702 100644 --- a/crates/funnel-core/src/lib.rs +++ b/crates/funnel-core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod error; pub mod protocol; pub mod tunnel; From f336be07125a04ab46eff9a16069321bc4cd1f17 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Mon, 12 Jan 2026 13:40:00 +0100 Subject: [PATCH 03/18] feat(db): add migrations for users, api keys, and tunnel sessions --- migrations/001_create_users.sql | 15 +++++++++++++++ migrations/002_create_api_keys.sql | 14 ++++++++++++++ migrations/003_create_tunnel_sessions.sql | 16 ++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 migrations/001_create_users.sql create mode 100644 migrations/002_create_api_keys.sql create mode 100644 migrations/003_create_tunnel_sessions.sql diff --git a/migrations/001_create_users.sql b/migrations/001_create_users.sql new file mode 100644 index 0000000..a8810cd --- /dev/null +++ b/migrations/001_create_users.sql @@ -0,0 +1,15 @@ +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT NOT NULL UNIQUE, + name TEXT, + avatar_url TEXT, + provider TEXT NOT NULL, + provider_id TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + UNIQUE(provider, provider_id) +); + +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_provider ON users(provider, provider_id); diff --git a/migrations/002_create_api_keys.sql b/migrations/002_create_api_keys.sql new file mode 100644 index 0000000..7346561 --- /dev/null +++ b/migrations/002_create_api_keys.sql @@ -0,0 +1,14 @@ +CREATE TABLE api_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + key_hash TEXT NOT NULL, + key_prefix TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + revoked_at TIMESTAMPTZ, + + UNIQUE(user_id, name) +); + +CREATE INDEX idx_api_keys_user ON api_keys(user_id); +CREATE INDEX idx_api_keys_hash ON api_keys(key_hash) WHERE revoked_at IS NULL; diff --git a/migrations/003_create_tunnel_sessions.sql b/migrations/003_create_tunnel_sessions.sql new file mode 100644 index 0000000..683d4f7 --- /dev/null +++ b/migrations/003_create_tunnel_sessions.sql @@ -0,0 +1,16 @@ +CREATE TABLE tunnel_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + tunnel_id TEXT NOT NULL, + client_ip INET, + connected_at TIMESTAMPTZ NOT NULL DEFAULT now(), + disconnected_at TIMESTAMPTZ, + bytes_in BIGINT NOT NULL DEFAULT 0, + bytes_out BIGINT NOT NULL DEFAULT 0, + requests BIGINT NOT NULL DEFAULT 0 +); + +CREATE INDEX idx_tunnel_sessions_user ON tunnel_sessions(user_id); +CREATE INDEX idx_tunnel_sessions_tunnel ON tunnel_sessions(tunnel_id); +CREATE INDEX idx_tunnel_sessions_active ON tunnel_sessions(disconnected_at) + WHERE disconnected_at IS NULL; From 4a43e58be0e49341eae9fc2aa61bcc1314ec0160 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 25 Jan 2026 16:20:00 +0100 Subject: [PATCH 04/18] feat(server): add axum skeleton with db layer, api routes, and clap cli --- crates/funnel-server/Cargo.toml | 2 +- crates/funnel-server/src/api/health.rs | 20 ++ crates/funnel-server/src/api/keys.rs | 60 ++++++ crates/funnel-server/src/api/mod.rs | 3 + crates/funnel-server/src/api/tunnels.rs | 38 ++++ crates/funnel-server/src/app.rs | 44 ++++ crates/funnel-server/src/auth/mod.rs | 1 + crates/funnel-server/src/db/api_keys.rs | 199 ++++++++++++++++++ crates/funnel-server/src/db/mod.rs | 3 + .../funnel-server/src/db/tunnel_sessions.rs | 155 ++++++++++++++ crates/funnel-server/src/db/users.rs | 189 +++++++++++++++++ crates/funnel-server/src/error.rs | 74 +++++++ crates/funnel-server/src/main.rs | 75 ++++++- crates/funnel-server/src/proxy/mod.rs | 1 + crates/funnel-server/src/tls/mod.rs | 1 + crates/funnel-server/src/tunnel/mod.rs | 1 + crates/funnel-server/src/ws/mod.rs | 1 + 17 files changed, 864 insertions(+), 3 deletions(-) create mode 100644 crates/funnel-server/src/api/health.rs create mode 100644 crates/funnel-server/src/api/keys.rs create mode 100644 crates/funnel-server/src/api/mod.rs create mode 100644 crates/funnel-server/src/api/tunnels.rs create mode 100644 crates/funnel-server/src/app.rs create mode 100644 crates/funnel-server/src/auth/mod.rs create mode 100644 crates/funnel-server/src/db/api_keys.rs create mode 100644 crates/funnel-server/src/db/mod.rs create mode 100644 crates/funnel-server/src/db/tunnel_sessions.rs create mode 100644 crates/funnel-server/src/db/users.rs create mode 100644 crates/funnel-server/src/error.rs create mode 100644 crates/funnel-server/src/proxy/mod.rs create mode 100644 crates/funnel-server/src/tls/mod.rs create mode 100644 crates/funnel-server/src/tunnel/mod.rs create mode 100644 crates/funnel-server/src/ws/mod.rs diff --git a/crates/funnel-server/Cargo.toml b/crates/funnel-server/Cargo.toml index 8b075c8..1f2f334 100644 --- a/crates/funnel-server/Cargo.toml +++ b/crates/funnel-server/Cargo.toml @@ -15,7 +15,7 @@ dashmap = "6" jsonwebtoken = "9" serde = { version = "1", features = ["derive"] } serde_json = "1" -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "migrate"] } thiserror = "2" tokio = { version = "1", features = ["full"] } tower = "0.5" diff --git a/crates/funnel-server/src/api/health.rs b/crates/funnel-server/src/api/health.rs new file mode 100644 index 0000000..9d50e27 --- /dev/null +++ b/crates/funnel-server/src/api/health.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use axum::Json; +use axum::extract::State; +use serde::Serialize; + +use crate::app::AppState; + +#[derive(Serialize)] +pub struct HealthResponse { + pub status: &'static str, + pub uptime_secs: u64, +} + +pub async fn handler(State(state): State>) -> Json { + Json(HealthResponse { + status: "healthy", + uptime_secs: state.start_time.elapsed().as_secs(), + }) +} diff --git a/crates/funnel-server/src/api/keys.rs b/crates/funnel-server/src/api/keys.rs new file mode 100644 index 0000000..c612af4 --- /dev/null +++ b/crates/funnel-server/src/api/keys.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use axum::Json; +use axum::extract::{Path, State}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::app::AppState; +use crate::db::api_keys::{self, ApiKeyView}; +use crate::error::AppError; + +#[derive(Deserialize)] +pub struct CreateKeyRequest { + pub name: String, + // TODO: extract user_id from auth middleware instead + pub user_id: Uuid, +} + +#[derive(Serialize)] +pub struct CreateKeyResponse { + pub key: String, + pub info: ApiKeyView, +} + +/// List all active API keys for a user. +pub async fn list(State(state): State>) -> Result>, AppError> { + // TODO: get user_id from auth middleware + // For now, return empty + let _pool = &state.db; + Ok(Json(vec![])) +} + +/// Create a new API key. +pub async fn create( + State(state): State>, + Json(req): Json, +) -> Result, AppError> { + let (plaintext, key) = api_keys::create(&state.db, req.user_id, &req.name).await?; + + Ok(Json(CreateKeyResponse { + key: plaintext, + info: key.into(), + })) +} + +/// Revoke an API key. +pub async fn revoke( + State(state): State>, + Path(id): Path, +) -> Result, AppError> { + // TODO: get user_id from auth middleware + let user_id = Uuid::nil(); // placeholder + + let revoked = api_keys::revoke(&state.db, id, user_id).await?; + if !revoked { + return Err(AppError::NotFound("api key not found".into())); + } + + Ok(Json(serde_json::json!({ "revoked": true }))) +} diff --git a/crates/funnel-server/src/api/mod.rs b/crates/funnel-server/src/api/mod.rs new file mode 100644 index 0000000..0b28d39 --- /dev/null +++ b/crates/funnel-server/src/api/mod.rs @@ -0,0 +1,3 @@ +pub mod health; +pub mod keys; +pub mod tunnels; diff --git a/crates/funnel-server/src/api/tunnels.rs b/crates/funnel-server/src/api/tunnels.rs new file mode 100644 index 0000000..c3465a8 --- /dev/null +++ b/crates/funnel-server/src/api/tunnels.rs @@ -0,0 +1,38 @@ +use std::sync::Arc; + +use axum::Json; +use axum::extract::{Path, State}; +use serde::Serialize; + +use crate::app::AppState; +use crate::error::AppError; + +#[derive(Serialize)] +pub struct TunnelInfo { + pub id: String, + pub status: &'static str, +} + +/// List all active tunnels. +pub async fn list(State(_state): State>) -> Json> { + // TODO: wire to TunnelManager once tunnel module is implemented + Json(vec![]) +} + +/// Get a specific tunnel by ID. +pub async fn get_tunnel( + State(_state): State>, + Path(id): Path, +) -> Result, AppError> { + // TODO: wire to TunnelManager + Err(AppError::TunnelNotFound(id)) +} + +/// Delete (force-close) a tunnel. +pub async fn delete( + State(_state): State>, + Path(id): Path, +) -> Result, AppError> { + // TODO: wire to TunnelManager + Err(AppError::TunnelNotFound(id)) +} diff --git a/crates/funnel-server/src/app.rs b/crates/funnel-server/src/app.rs new file mode 100644 index 0000000..9a18b25 --- /dev/null +++ b/crates/funnel-server/src/app.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use axum::Router; +use axum::routing::get; +use sqlx::PgPool; +use tokio::time::Instant; +use tower_http::cors::CorsLayer; +use tower_http::trace::TraceLayer; + +use crate::api; + +/// Shared application state, available in all handlers via `State>`. +pub struct AppState { + pub db: PgPool, + pub start_time: Instant, +} + +impl AppState { + pub fn new(db: PgPool) -> Self { + Self { + db, + start_time: Instant::now(), + } + } +} + +/// Build the complete Axum router with all routes and middleware. +pub fn build_router(state: Arc) -> Router { + let api_routes = Router::new() + .route("/health", get(api::health::handler)) + .route("/tunnels", get(api::tunnels::list)) + .route( + "/tunnels/{id}", + get(api::tunnels::get_tunnel).delete(api::tunnels::delete), + ) + .route("/keys", get(api::keys::list).post(api::keys::create)) + .route("/keys/{id}", axum::routing::delete(api::keys::revoke)); + + Router::new() + .nest("/api", api_routes) + .with_state(state) + .layer(TraceLayer::new_for_http()) + .layer(CorsLayer::permissive()) +} diff --git a/crates/funnel-server/src/auth/mod.rs b/crates/funnel-server/src/auth/mod.rs new file mode 100644 index 0000000..4e0d17b --- /dev/null +++ b/crates/funnel-server/src/auth/mod.rs @@ -0,0 +1 @@ +// Auth middleware — to be implemented in Phase 2 diff --git a/crates/funnel-server/src/db/api_keys.rs b/crates/funnel-server/src/db/api_keys.rs new file mode 100644 index 0000000..c5ee425 --- /dev/null +++ b/crates/funnel-server/src/db/api_keys.rs @@ -0,0 +1,199 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; +use sqlx::PgPool; +use uuid::Uuid; + +use funnel_core::auth; + +#[derive(Debug, Clone, Serialize, sqlx::FromRow)] +pub struct ApiKey { + pub id: Uuid, + pub user_id: Uuid, + pub name: String, + pub key_hash: String, + pub key_prefix: String, + pub created_at: DateTime, + pub revoked_at: Option>, +} + +/// A view of an API key safe for returning in API responses (no hash). +#[derive(Debug, Clone, Serialize)] +pub struct ApiKeyView { + pub id: Uuid, + pub name: String, + pub key_prefix: String, + pub created_at: DateTime, + pub revoked_at: Option>, +} + +impl From for ApiKeyView { + fn from(key: ApiKey) -> Self { + Self { + id: key.id, + name: key.name, + key_prefix: key.key_prefix, + created_at: key.created_at, + revoked_at: key.revoked_at, + } + } +} + +/// Create a new API key for a user. Returns the full plaintext key (show once) and the DB record. +pub async fn create( + pool: &PgPool, + user_id: Uuid, + name: &str, +) -> Result<(String, ApiKey), sqlx::Error> { + let plaintext = auth::generate_api_key(); + let hash = auth::hash_token(&plaintext); + let prefix = auth::ApiKeyPrefix::from_key(&plaintext); + + let key = sqlx::query_as::<_, ApiKey>( + r#" + INSERT INTO api_keys (user_id, name, key_hash, key_prefix) + VALUES ($1, $2, $3, $4) + RETURNING * + "#, + ) + .bind(user_id) + .bind(name) + .bind(&hash) + .bind(prefix.as_ref()) + .fetch_one(pool) + .await?; + + Ok((plaintext, key)) +} + +/// Find the user ID associated with a plaintext API key, if it's valid and not revoked. +pub async fn validate(pool: &PgPool, plaintext: &str) -> Result, sqlx::Error> { + let hash = auth::hash_token(plaintext); + + sqlx::query_as::<_, ApiKey>( + "SELECT * FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL", + ) + .bind(&hash) + .fetch_optional(pool) + .await +} + +/// List all active (non-revoked) API keys for a user. +pub async fn list_for_user(pool: &PgPool, user_id: Uuid) -> Result, sqlx::Error> { + sqlx::query_as::<_, ApiKey>( + "SELECT * FROM api_keys WHERE user_id = $1 AND revoked_at IS NULL ORDER BY created_at DESC", + ) + .bind(user_id) + .fetch_all(pool) + .await +} + +/// Revoke an API key by setting its `revoked_at` timestamp. +pub async fn revoke(pool: &PgPool, key_id: Uuid, user_id: Uuid) -> Result { + let result = sqlx::query( + "UPDATE api_keys SET revoked_at = now() WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL", + ) + .bind(key_id) + .bind(user_id) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::users; + + async fn setup_user(pool: &PgPool) -> users::User { + users::create( + pool, + users::NewUser { + email: format!("apikey-test-{}@example.com", Uuid::now_v7()), + name: Some("API Key Test".into()), + avatar_url: None, + provider: "github".into(), + provider_id: Uuid::now_v7().to_string(), + }, + ) + .await + .unwrap() + } + + #[tokio::test] + #[ignore = "requires database"] + async fn create_and_validate_key() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let user = setup_user(&pool).await; + let (plaintext, key) = create(&pool, user.id, "test-key").await.unwrap(); + + assert!(plaintext.starts_with("sk_")); + assert_eq!(key.name, "test-key"); + assert_eq!(key.user_id, user.id); + assert!(key.revoked_at.is_none()); + + let validated = validate(&pool, &plaintext).await.unwrap(); + assert!(validated.is_some()); + assert_eq!(validated.unwrap().id, key.id); + } + + #[tokio::test] + #[ignore = "requires database"] + async fn validate_invalid_key() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let result = validate(&pool, "sk_totally_bogus_key").await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + #[ignore = "requires database"] + async fn revoke_key_prevents_validation() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let user = setup_user(&pool).await; + let (plaintext, key) = create(&pool, user.id, "revoke-test").await.unwrap(); + + let revoked = revoke(&pool, key.id, user.id).await.unwrap(); + assert!(revoked); + + let validated = validate(&pool, &plaintext).await.unwrap(); + assert!(validated.is_none()); + } + + #[tokio::test] + #[ignore = "requires database"] + async fn list_keys_for_user() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let user = setup_user(&pool).await; + create(&pool, user.id, "key-1").await.unwrap(); + create(&pool, user.id, "key-2").await.unwrap(); + + let keys = list_for_user(&pool, user.id).await.unwrap(); + assert_eq!(keys.len(), 2); + } + + #[tokio::test] + #[ignore = "requires database"] + async fn duplicate_key_name_rejected() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let user = setup_user(&pool).await; + create(&pool, user.id, "dupe-name").await.unwrap(); + + let result = create(&pool, user.id, "dupe-name").await; + assert!(result.is_err()); + } +} diff --git a/crates/funnel-server/src/db/mod.rs b/crates/funnel-server/src/db/mod.rs new file mode 100644 index 0000000..21ec42e --- /dev/null +++ b/crates/funnel-server/src/db/mod.rs @@ -0,0 +1,3 @@ +pub mod users; +pub mod api_keys; +pub mod tunnel_sessions; diff --git a/crates/funnel-server/src/db/tunnel_sessions.rs b/crates/funnel-server/src/db/tunnel_sessions.rs new file mode 100644 index 0000000..edbcc74 --- /dev/null +++ b/crates/funnel-server/src/db/tunnel_sessions.rs @@ -0,0 +1,155 @@ +use std::net::IpAddr; + +use chrono::{DateTime, Utc}; +use serde::Serialize; +use sqlx::PgPool; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, sqlx::FromRow)] +pub struct TunnelSession { + pub id: Uuid, + pub user_id: Uuid, + pub tunnel_id: String, + pub client_ip: Option, + pub connected_at: DateTime, + pub disconnected_at: Option>, + pub bytes_in: i64, + pub bytes_out: i64, + pub requests: i64, +} + +/// Record a new tunnel connection. +pub async fn create( + pool: &PgPool, + user_id: Uuid, + tunnel_id: &str, + client_ip: Option, +) -> Result { + sqlx::query_as::<_, TunnelSession>( + r#" + INSERT INTO tunnel_sessions (user_id, tunnel_id, client_ip) + VALUES ($1, $2, $3) + RETURNING * + "#, + ) + .bind(user_id) + .bind(tunnel_id) + .bind(client_ip) + .fetch_one(pool) + .await +} + +/// Mark a tunnel session as disconnected and record final stats. +pub async fn disconnect( + pool: &PgPool, + session_id: Uuid, + bytes_in: i64, + bytes_out: i64, + requests: i64, +) -> Result { + let result = sqlx::query( + r#" + UPDATE tunnel_sessions + SET disconnected_at = now(), bytes_in = $2, bytes_out = $3, requests = $4 + WHERE id = $1 AND disconnected_at IS NULL + "#, + ) + .bind(session_id) + .bind(bytes_in) + .bind(bytes_out) + .bind(requests) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} + +/// List recent tunnel sessions for a user, most recent first. +pub async fn list_for_user( + pool: &PgPool, + user_id: Uuid, + limit: i64, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, TunnelSession>( + r#" + SELECT * FROM tunnel_sessions + WHERE user_id = $1 + ORDER BY connected_at DESC + LIMIT $2 + "#, + ) + .bind(user_id) + .bind(limit) + .fetch_all(pool) + .await +} + +/// List currently active (not disconnected) sessions. +pub async fn list_active(pool: &PgPool) -> Result, sqlx::Error> { + sqlx::query_as::<_, TunnelSession>( + "SELECT * FROM tunnel_sessions WHERE disconnected_at IS NULL ORDER BY connected_at DESC", + ) + .fetch_all(pool) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::users; + + async fn setup_user(pool: &PgPool) -> users::User { + users::create( + pool, + users::NewUser { + email: format!("session-test-{}@example.com", Uuid::now_v7()), + name: Some("Session Test".into()), + avatar_url: None, + provider: "github".into(), + provider_id: Uuid::now_v7().to_string(), + }, + ) + .await + .unwrap() + } + + #[tokio::test] + #[ignore = "requires database"] + async fn create_and_disconnect_session() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let user = setup_user(&pool).await; + let session = create(&pool, user.id, "test-tunnel", None).await.unwrap(); + + assert_eq!(session.tunnel_id, "test-tunnel"); + assert!(session.disconnected_at.is_none()); + assert_eq!(session.bytes_in, 0); + + let disconnected = disconnect(&pool, session.id, 1024, 2048, 10) + .await + .unwrap(); + assert!(disconnected); + } + + #[tokio::test] + #[ignore = "requires database"] + async fn list_active_sessions() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let user = setup_user(&pool).await; + let s1 = create(&pool, user.id, "active-1", None).await.unwrap(); + let s2 = create(&pool, user.id, "active-2", None).await.unwrap(); + + // Disconnect one + disconnect(&pool, s1.id, 0, 0, 0).await.unwrap(); + + let active = list_active(&pool).await.unwrap(); + let active_ids: Vec = active.iter().map(|s| s.id).collect(); + assert!(active_ids.contains(&s2.id)); + assert!(!active_ids.contains(&s1.id)); + } +} diff --git a/crates/funnel-server/src/db/users.rs b/crates/funnel-server/src/db/users.rs new file mode 100644 index 0000000..bec8d08 --- /dev/null +++ b/crates/funnel-server/src/db/users.rs @@ -0,0 +1,189 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; +use sqlx::PgPool; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, sqlx::FromRow)] +pub struct User { + pub id: Uuid, + pub email: String, + pub name: Option, + pub avatar_url: Option, + pub provider: String, + pub provider_id: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +pub struct NewUser { + pub email: String, + pub name: Option, + pub avatar_url: Option, + pub provider: String, + pub provider_id: String, +} + +/// Find a user by their unique ID. +pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { + sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1") + .bind(id) + .fetch_optional(pool) + .await +} + +/// Find a user by email address. +pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = $1") + .bind(email) + .fetch_optional(pool) + .await +} + +/// Find a user by their OAuth provider and provider-specific ID. +pub async fn find_by_provider( + pool: &PgPool, + provider: &str, + provider_id: &str, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, User>( + "SELECT * FROM users WHERE provider = $1 AND provider_id = $2", + ) + .bind(provider) + .bind(provider_id) + .fetch_optional(pool) + .await +} + +/// Create a new user, returning the created record. +pub async fn create(pool: &PgPool, new_user: NewUser) -> Result { + sqlx::query_as::<_, User>( + r#" + INSERT INTO users (email, name, avatar_url, provider, provider_id) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + "#, + ) + .bind(&new_user.email) + .bind(&new_user.name) + .bind(&new_user.avatar_url) + .bind(&new_user.provider) + .bind(&new_user.provider_id) + .fetch_one(pool) + .await +} + +/// Upsert a user based on OAuth provider identity. +/// +/// If a user with the same (provider, provider_id) exists, update their profile. +/// Otherwise, create a new user. Returns the user in either case. +pub async fn upsert_from_oauth(pool: &PgPool, new_user: NewUser) -> Result { + sqlx::query_as::<_, User>( + r#" + INSERT INTO users (email, name, avatar_url, provider, provider_id) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (provider, provider_id) + DO UPDATE SET + email = EXCLUDED.email, + name = EXCLUDED.name, + avatar_url = EXCLUDED.avatar_url, + updated_at = now() + RETURNING * + "#, + ) + .bind(&new_user.email) + .bind(&new_user.name) + .bind(&new_user.avatar_url) + .bind(&new_user.provider) + .bind(&new_user.provider_id) + .fetch_one(pool) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_new_user() -> NewUser { + NewUser { + email: format!("test-{}@example.com", Uuid::now_v7()), + name: Some("Test User".into()), + avatar_url: None, + provider: "github".into(), + provider_id: Uuid::now_v7().to_string(), + } + } + + // Integration tests require a running Postgres instance. + // Run with: DATABASE_URL=postgres://... cargo test -- --ignored + #[tokio::test] + #[ignore = "requires database"] + async fn create_and_find_user() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let new = test_new_user(); + let email = new.email.clone(); + + let created = create(&pool, new).await.unwrap(); + assert_eq!(created.email, email); + assert!(created.id != Uuid::nil()); + + let found = find_by_id(&pool, created.id).await.unwrap(); + assert!(found.is_some()); + assert_eq!(found.unwrap().email, email); + } + + #[tokio::test] + #[ignore = "requires database"] + async fn find_nonexistent_user() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let found = find_by_id(&pool, Uuid::now_v7()).await.unwrap(); + assert!(found.is_none()); + } + + #[tokio::test] + #[ignore = "requires database"] + async fn upsert_creates_then_updates() { + let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) + .await + .unwrap(); + + let provider_id = Uuid::now_v7().to_string(); + + let first = upsert_from_oauth( + &pool, + NewUser { + email: format!("upsert-{}@example.com", Uuid::now_v7()), + name: Some("First".into()), + avatar_url: None, + provider: "github".into(), + provider_id: provider_id.clone(), + }, + ) + .await + .unwrap(); + + let second = upsert_from_oauth( + &pool, + NewUser { + email: format!("upsert-updated-{}@example.com", Uuid::now_v7()), + name: Some("Updated".into()), + avatar_url: Some("https://example.com/avatar.png".into()), + provider: "github".into(), + provider_id: provider_id.clone(), + }, + ) + .await + .unwrap(); + + // Same user row + assert_eq!(first.id, second.id); + // But fields updated + assert_eq!(second.name.as_deref(), Some("Updated")); + assert!(second.avatar_url.is_some()); + } +} diff --git a/crates/funnel-server/src/error.rs b/crates/funnel-server/src/error.rs new file mode 100644 index 0000000..74c9906 --- /dev/null +++ b/crates/funnel-server/src/error.rs @@ -0,0 +1,74 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use funnel_core::error::ValidationError; +use serde::Serialize; + +#[derive(Debug, Serialize)] +struct ApiErrorBody { + error: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error("tunnel not found: {0}")] + TunnelNotFound(String), + + #[error("tunnel ID already in use: {0}")] + TunnelConflict(String), + + #[error("unauthorized: {0}")] + Unauthorized(String), + + #[error("forbidden: {0}")] + Forbidden(String), + + #[error("not found: {0}")] + NotFound(String), + + #[error("conflict: {0}")] + Conflict(String), + + #[error("bad request: {0}")] + BadRequest(String), + + #[error("validation error: {0}")] + Validation(#[from] ValidationError), + + #[error("database error")] + Database(#[from] sqlx::Error), + + #[error("internal error: {0}")] + Internal(#[from] anyhow::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match &self { + Self::TunnelNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()), + Self::TunnelConflict(_) => (StatusCode::CONFLICT, self.to_string()), + Self::Unauthorized(_) => (StatusCode::UNAUTHORIZED, self.to_string()), + Self::Forbidden(_) => (StatusCode::FORBIDDEN, self.to_string()), + Self::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()), + Self::Conflict(_) => (StatusCode::CONFLICT, self.to_string()), + Self::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()), + Self::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()), + Self::Database(e) => { + tracing::error!(error = %e, "database error"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "internal error".to_string(), + ) + } + Self::Internal(e) => { + tracing::error!(error = %e, "internal error"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "internal error".to_string(), + ) + } + }; + + (status, Json(ApiErrorBody { error: message })).into_response() + } +} diff --git a/crates/funnel-server/src/main.rs b/crates/funnel-server/src/main.rs index d8cea63..993d7fa 100644 --- a/crates/funnel-server/src/main.rs +++ b/crates/funnel-server/src/main.rs @@ -1,3 +1,74 @@ -fn main() { - println!("funnel-server"); +mod api; +mod app; +mod auth; +mod db; +mod error; +mod proxy; +mod tls; +mod tunnel; +mod ws; + +use std::sync::Arc; + +use clap::Parser; +use sqlx::postgres::PgPoolOptions; +use tracing_subscriber::EnvFilter; + +#[derive(Parser)] +#[command(name = "funnel-server", about = "Funnel tunnel server")] +struct Cli { + /// Port to listen on + #[arg(short, long, default_value_t = 8080)] + port: u16, + + /// Host to bind to + #[arg(long, default_value = "0.0.0.0")] + host: String, + + /// PostgreSQL connection URL + #[arg(long, env = "DATABASE_URL")] + database_url: String, + + /// Maximum database connections + #[arg(long, default_value_t = 10)] + db_max_connections: u32, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .json() + .init(); + + let cli = Cli::parse(); + + tracing::info!(host = %cli.host, port = cli.port, "starting funnel server"); + + let pool = PgPoolOptions::new() + .max_connections(cli.db_max_connections) + .connect(&cli.database_url) + .await?; + + tracing::info!("connected to database"); + + sqlx::migrate!("../../migrations") + .run(&pool) + .await?; + + tracing::info!("database migrations applied"); + + let state = Arc::new(app::AppState::new(pool)); + let router = app::build_router(state); + + let addr = format!("{}:{}", cli.host, cli.port); + let listener = tokio::net::TcpListener::bind(&addr).await?; + + tracing::info!(addr = %addr, "server listening"); + + axum::serve(listener, router).await?; + + Ok(()) } diff --git a/crates/funnel-server/src/proxy/mod.rs b/crates/funnel-server/src/proxy/mod.rs new file mode 100644 index 0000000..b48c7fd --- /dev/null +++ b/crates/funnel-server/src/proxy/mod.rs @@ -0,0 +1 @@ +// Proxy/routing — to be implemented in Phase 1, increment 2 diff --git a/crates/funnel-server/src/tls/mod.rs b/crates/funnel-server/src/tls/mod.rs new file mode 100644 index 0000000..9fb262b --- /dev/null +++ b/crates/funnel-server/src/tls/mod.rs @@ -0,0 +1 @@ +// TLS/ACME certificate management — to be implemented in Phase 4 diff --git a/crates/funnel-server/src/tunnel/mod.rs b/crates/funnel-server/src/tunnel/mod.rs new file mode 100644 index 0000000..bb00f67 --- /dev/null +++ b/crates/funnel-server/src/tunnel/mod.rs @@ -0,0 +1 @@ +// Tunnel management — to be implemented in Phase 1, increment 2 diff --git a/crates/funnel-server/src/ws/mod.rs b/crates/funnel-server/src/ws/mod.rs new file mode 100644 index 0000000..ef3549a --- /dev/null +++ b/crates/funnel-server/src/ws/mod.rs @@ -0,0 +1 @@ +// WebSocket handler — to be implemented in Phase 1, increment 2 From 77d842ca105b1fa7a7c7e80d907607d4a14a3516 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 25 Jan 2026 16:45:00 +0100 Subject: [PATCH 05/18] chore: update gitignore for rust --- .gitignore | 60 ++++++++++++------------------------------------------ 1 file changed, 13 insertions(+), 47 deletions(-) diff --git a/.gitignore b/.gitignore index c3c9c1a..f4b6ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,62 +1,28 @@ -# binaries -bin/ -dist/ -*.exe -*.exe~ -*.dll -*.so -*.dylib +# Rust +/target +**/*.rs.bk +Cargo.lock -# test binary, built with `go test -c` -*.test +# Environment +.env +.direnv +result -# output of the go coverage tool -*.out - -# dependency directories -vendor/ - -# IDE files +# IDE .vscode/ .idea/ *.swp *.swo *~ -# OS files +# OS .DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db Thumbs.db -# logs +# Logs *.log -# temporary files +# Temporary *.tmp *.temp - -# Built binaries -client_main -server_main -/client_main -/server_main -tunneling-client -tunneling-server - -# Coverage reports -coverage.out -coverage.html - -# Profiling data -*.prof -*.pprof - -# Debug files -debug -*.debug - -/tmp \ No newline at end of file +/tmp From 4fc45b2b695bb086a42c063fd0c743c6796b6d9b Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Mon, 26 Jan 2026 10:00:00 +0100 Subject: [PATCH 06/18] style: clean up comments and use named constants --- crates/funnel-core/src/auth/token.rs | 34 +++++------- crates/funnel-core/src/protocol/message.rs | 7 --- crates/funnel-core/src/tunnel/id.rs | 53 +++++++------------ crates/funnel-server/src/app.rs | 2 - crates/funnel-server/src/auth/mod.rs | 1 - crates/funnel-server/src/db/api_keys.rs | 5 -- .../funnel-server/src/db/tunnel_sessions.rs | 5 -- crates/funnel-server/src/db/users.rs | 15 ++---- crates/funnel-server/src/proxy/mod.rs | 1 - crates/funnel-server/src/tls/mod.rs | 1 - crates/funnel-server/src/tunnel/mod.rs | 1 - crates/funnel-server/src/ws/mod.rs | 1 - crates/funnel-web/src/lib.rs | 2 +- 13 files changed, 37 insertions(+), 91 deletions(-) diff --git a/crates/funnel-core/src/auth/token.rs b/crates/funnel-core/src/auth/token.rs index 2fa59a4..04e5e7f 100644 --- a/crates/funnel-core/src/auth/token.rs +++ b/crates/funnel-core/src/auth/token.rs @@ -3,17 +3,17 @@ use std::fmt; use base64::prelude::*; use serde::{Deserialize, Serialize}; -const API_KEY_PREFIX: &str = "sk_"; -const API_KEY_RANDOM_BYTES: usize = 32; -const DISPLAY_PREFIX_LEN: usize = 10; +const PREFIX: &str = "sk_"; +const RANDOM_BYTES: usize = 32; +const VISIBLE_PREFIX_LEN: usize = 10; -/// The visible prefix of an API key (e.g. `"sk_abc123..."`), safe for display and logging. +/// the visible prefix of an api key, safe for display and logging. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ApiKeyPrefix(String); impl ApiKeyPrefix { pub fn from_key(full_key: &str) -> Self { - let end = full_key.len().min(DISPLAY_PREFIX_LEN); + let end = full_key.len().min(VISIBLE_PREFIX_LEN); Self(full_key[..end].to_string()) } } @@ -30,21 +30,14 @@ impl fmt::Display for ApiKeyPrefix { } } -/// Generate a cryptographically random API key with the `sk_` prefix. -/// -/// Returns the full plaintext key. This should be shown to the user exactly once -/// and then only stored as a hash. pub fn generate_api_key() -> String { - let mut bytes = [0u8; API_KEY_RANDOM_BYTES]; + let mut bytes = [0u8; RANDOM_BYTES]; getrandom::fill(&mut bytes).expect("failed to generate random bytes"); - format!("{}{}", API_KEY_PREFIX, BASE64_URL_SAFE_NO_PAD.encode(bytes)) + format!("{}{}", PREFIX, BASE64_URL_SAFE_NO_PAD.encode(bytes)) } -/// Hash a plaintext API key using SHA-256. -/// -/// This is used for storage and constant-time comparison. We use SHA-256 rather -/// than argon2 for API keys because they are high-entropy random tokens, not -/// user-chosen passwords, so brute-force resistance from a slow hash is unnecessary. +/// sha256 is fine here since api keys are high entropy random tokens, +/// not user chosen passwords, so slow hashing is unnecessary. pub fn hash_token(token: &str) -> String { use sha2::{Digest, Sha256}; let hash = Sha256::digest(token.as_bytes()); @@ -58,13 +51,13 @@ mod tests { #[test] fn generate_key_has_prefix() { let key = generate_api_key(); - assert!(key.starts_with("sk_")); + assert!(key.starts_with(PREFIX)); } #[test] fn generate_key_has_sufficient_length() { let key = generate_api_key(); - // sk_ (3) + base64 of 32 bytes (43) = 46 chars + // prefix (3) + base64 of 32 bytes (43) = 46 chars assert!(key.len() >= 40); } @@ -89,7 +82,8 @@ mod tests { #[test] fn hash_is_hex_encoded_sha256() { let hash = hash_token("test"); - assert_eq!(hash.len(), 64); // SHA-256 = 32 bytes = 64 hex chars + // 32 bytes = 64 hex chars + assert_eq!(hash.len(), 64); assert!(hash.chars().all(|c| c.is_ascii_hexdigit())); } @@ -97,7 +91,7 @@ mod tests { fn prefix_from_key() { let key = generate_api_key(); let prefix = ApiKeyPrefix::from_key(&key); - assert_eq!(prefix.as_ref().len(), DISPLAY_PREFIX_LEN); + assert_eq!(prefix.as_ref().len(), VISIBLE_PREFIX_LEN); assert!(key.starts_with(prefix.as_ref())); } diff --git a/crates/funnel-core/src/protocol/message.rs b/crates/funnel-core/src/protocol/message.rs index 4d22e4e..a022cb7 100644 --- a/crates/funnel-core/src/protocol/message.rs +++ b/crates/funnel-core/src/protocol/message.rs @@ -5,7 +5,6 @@ use uuid::Uuid; use crate::tunnel::TunnelId; -/// HTTP request payload forwarded through the tunnel. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RequestPayload { pub method: String, @@ -15,7 +14,6 @@ pub struct RequestPayload { pub body: Vec, } -/// HTTP response payload returned through the tunnel. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponsePayload { pub status: u16, @@ -24,10 +22,6 @@ pub struct ResponsePayload { pub body: Vec, } -/// A message sent over the WebSocket tunnel connection. -/// -/// Uses a tagged enum (`"type"` field) for clean pattern matching -/// instead of stringly-typed message kinds. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum TunnelMessage { @@ -47,7 +41,6 @@ pub enum TunnelMessage { }, } -/// Serde helper for encoding `Vec` as base64 strings in JSON. mod base64_bytes { use base64::prelude::*; use serde::{Deserialize, Deserializer, Serializer}; diff --git a/crates/funnel-core/src/tunnel/id.rs b/crates/funnel-core/src/tunnel/id.rs index d21443a..821b9be 100644 --- a/crates/funnel-core/src/tunnel/id.rs +++ b/crates/funnel-core/src/tunnel/id.rs @@ -10,27 +10,24 @@ const DOMAIN_SAFE_ALPHABET: &[char] = &[ 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -const DEFAULT_ID_LENGTH: usize = 8; +const DEFAULT_LENGTH: usize = 8; +const MIN_LENGTH: usize = 3; +const MAX_LENGTH: usize = 63; -/// A validated tunnel identifier. -/// -/// Guaranteed to be 3-63 characters, lowercase alphanumeric with hyphens, -/// and not starting or ending with a hyphen. Safe for use as a DNS subdomain label. +/// a validated tunnel identifier, guaranteed to be a valid dns subdomain label. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(try_from = "String", into = "String")] pub struct TunnelId(String); impl TunnelId { - /// Parse and validate a raw string as a tunnel ID. pub fn new(raw: impl Into) -> Result { let s = raw.into(); Self::validate(&s)?; Ok(Self(s)) } - /// Generate a random domain-safe tunnel ID. pub fn generate() -> Self { - let id = nanoid::nanoid!(DEFAULT_ID_LENGTH, DOMAIN_SAFE_ALPHABET); + let id = nanoid::nanoid!(DEFAULT_LENGTH, DOMAIN_SAFE_ALPHABET); Self(id) } @@ -38,19 +35,16 @@ impl TunnelId { if s.is_empty() { return Err(ValidationError::Empty); } - if s.len() < 3 { + if s.len() < MIN_LENGTH { return Err(ValidationError::TooShort(s.len())); } - if s.len() > 63 { + if s.len() > MAX_LENGTH { return Err(ValidationError::TooLong(s.len())); } - - let lower = s.to_lowercase(); - if s != lower { + if s != s.to_lowercase() { return Err(ValidationError::NotLowercase); } - // Must match: starts and ends with alnum, middle can have hyphens let re = Regex::new(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$").expect("valid regex"); if !re.is_match(s) { return Err(ValidationError::InvalidFormat); @@ -96,7 +90,7 @@ mod tests { assert!(TunnelId::new("my-tunnel").is_ok()); assert!(TunnelId::new("test-123").is_ok()); assert!(TunnelId::new("a1b").is_ok()); - assert!(TunnelId::new("a".repeat(63)).is_ok()); + assert!(TunnelId::new("a".repeat(MAX_LENGTH)).is_ok()); } #[test] @@ -112,8 +106,11 @@ mod tests { #[test] fn too_long() { - let long = "a".repeat(64); - assert!(matches!(TunnelId::new(long), Err(ValidationError::TooLong(64)))); + let long = "a".repeat(MAX_LENGTH + 1); + assert!(matches!( + TunnelId::new(long), + Err(ValidationError::TooLong(64)) + )); } #[test] @@ -126,29 +123,17 @@ mod tests { #[test] fn invalid_format() { - assert!(matches!( - TunnelId::new("-abc"), - Err(ValidationError::InvalidFormat) - )); - assert!(matches!( - TunnelId::new("abc-"), - Err(ValidationError::InvalidFormat) - )); - assert!(matches!( - TunnelId::new("ab_c"), - Err(ValidationError::InvalidFormat) - )); - assert!(matches!( - TunnelId::new("ab c"), - Err(ValidationError::InvalidFormat) - )); + assert!(matches!(TunnelId::new("-abc"), Err(ValidationError::InvalidFormat))); + assert!(matches!(TunnelId::new("abc-"), Err(ValidationError::InvalidFormat))); + assert!(matches!(TunnelId::new("ab_c"), Err(ValidationError::InvalidFormat))); + assert!(matches!(TunnelId::new("ab c"), Err(ValidationError::InvalidFormat))); } #[test] fn generate_produces_valid_id() { for _ in 0..100 { let id = TunnelId::generate(); - assert_eq!(id.as_ref().len(), DEFAULT_ID_LENGTH); + assert_eq!(id.as_ref().len(), DEFAULT_LENGTH); assert!(TunnelId::new(id.as_ref()).is_ok()); } } diff --git a/crates/funnel-server/src/app.rs b/crates/funnel-server/src/app.rs index 9a18b25..29db85f 100644 --- a/crates/funnel-server/src/app.rs +++ b/crates/funnel-server/src/app.rs @@ -9,7 +9,6 @@ use tower_http::trace::TraceLayer; use crate::api; -/// Shared application state, available in all handlers via `State>`. pub struct AppState { pub db: PgPool, pub start_time: Instant, @@ -24,7 +23,6 @@ impl AppState { } } -/// Build the complete Axum router with all routes and middleware. pub fn build_router(state: Arc) -> Router { let api_routes = Router::new() .route("/health", get(api::health::handler)) diff --git a/crates/funnel-server/src/auth/mod.rs b/crates/funnel-server/src/auth/mod.rs index 4e0d17b..e69de29 100644 --- a/crates/funnel-server/src/auth/mod.rs +++ b/crates/funnel-server/src/auth/mod.rs @@ -1 +0,0 @@ -// Auth middleware — to be implemented in Phase 2 diff --git a/crates/funnel-server/src/db/api_keys.rs b/crates/funnel-server/src/db/api_keys.rs index c5ee425..320e0c1 100644 --- a/crates/funnel-server/src/db/api_keys.rs +++ b/crates/funnel-server/src/db/api_keys.rs @@ -16,7 +16,6 @@ pub struct ApiKey { pub revoked_at: Option>, } -/// A view of an API key safe for returning in API responses (no hash). #[derive(Debug, Clone, Serialize)] pub struct ApiKeyView { pub id: Uuid, @@ -38,7 +37,6 @@ impl From for ApiKeyView { } } -/// Create a new API key for a user. Returns the full plaintext key (show once) and the DB record. pub async fn create( pool: &PgPool, user_id: Uuid, @@ -65,7 +63,6 @@ pub async fn create( Ok((plaintext, key)) } -/// Find the user ID associated with a plaintext API key, if it's valid and not revoked. pub async fn validate(pool: &PgPool, plaintext: &str) -> Result, sqlx::Error> { let hash = auth::hash_token(plaintext); @@ -77,7 +74,6 @@ pub async fn validate(pool: &PgPool, plaintext: &str) -> Result, .await } -/// List all active (non-revoked) API keys for a user. pub async fn list_for_user(pool: &PgPool, user_id: Uuid) -> Result, sqlx::Error> { sqlx::query_as::<_, ApiKey>( "SELECT * FROM api_keys WHERE user_id = $1 AND revoked_at IS NULL ORDER BY created_at DESC", @@ -87,7 +83,6 @@ pub async fn list_for_user(pool: &PgPool, user_id: Uuid) -> Result, .await } -/// Revoke an API key by setting its `revoked_at` timestamp. pub async fn revoke(pool: &PgPool, key_id: Uuid, user_id: Uuid) -> Result { let result = sqlx::query( "UPDATE api_keys SET revoked_at = now() WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL", diff --git a/crates/funnel-server/src/db/tunnel_sessions.rs b/crates/funnel-server/src/db/tunnel_sessions.rs index edbcc74..1be0e17 100644 --- a/crates/funnel-server/src/db/tunnel_sessions.rs +++ b/crates/funnel-server/src/db/tunnel_sessions.rs @@ -18,7 +18,6 @@ pub struct TunnelSession { pub requests: i64, } -/// Record a new tunnel connection. pub async fn create( pool: &PgPool, user_id: Uuid, @@ -39,7 +38,6 @@ pub async fn create( .await } -/// Mark a tunnel session as disconnected and record final stats. pub async fn disconnect( pool: &PgPool, session_id: Uuid, @@ -64,7 +62,6 @@ pub async fn disconnect( Ok(result.rows_affected() > 0) } -/// List recent tunnel sessions for a user, most recent first. pub async fn list_for_user( pool: &PgPool, user_id: Uuid, @@ -84,7 +81,6 @@ pub async fn list_for_user( .await } -/// List currently active (not disconnected) sessions. pub async fn list_active(pool: &PgPool) -> Result, sqlx::Error> { sqlx::query_as::<_, TunnelSession>( "SELECT * FROM tunnel_sessions WHERE disconnected_at IS NULL ORDER BY connected_at DESC", @@ -144,7 +140,6 @@ mod tests { let s1 = create(&pool, user.id, "active-1", None).await.unwrap(); let s2 = create(&pool, user.id, "active-2", None).await.unwrap(); - // Disconnect one disconnect(&pool, s1.id, 0, 0, 0).await.unwrap(); let active = list_active(&pool).await.unwrap(); diff --git a/crates/funnel-server/src/db/users.rs b/crates/funnel-server/src/db/users.rs index bec8d08..9466d4f 100644 --- a/crates/funnel-server/src/db/users.rs +++ b/crates/funnel-server/src/db/users.rs @@ -23,7 +23,6 @@ pub struct NewUser { pub provider_id: String, } -/// Find a user by their unique ID. pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1") .bind(id) @@ -31,7 +30,6 @@ pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::E .await } -/// Find a user by email address. pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error> { sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = $1") .bind(email) @@ -39,7 +37,6 @@ pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, s .await } -/// Find a user by their OAuth provider and provider-specific ID. pub async fn find_by_provider( pool: &PgPool, provider: &str, @@ -54,7 +51,6 @@ pub async fn find_by_provider( .await } -/// Create a new user, returning the created record. pub async fn create(pool: &PgPool, new_user: NewUser) -> Result { sqlx::query_as::<_, User>( r#" @@ -72,10 +68,8 @@ pub async fn create(pool: &PgPool, new_user: NewUser) -> Result Result { sqlx::query_as::<_, User>( r#" @@ -113,8 +107,7 @@ mod tests { } } - // Integration tests require a running Postgres instance. - // Run with: DATABASE_URL=postgres://... cargo test -- --ignored + // run with: DATABASE_URL=postgres://... cargo test -- --ignored #[tokio::test] #[ignore = "requires database"] async fn create_and_find_user() { @@ -180,9 +173,7 @@ mod tests { .await .unwrap(); - // Same user row assert_eq!(first.id, second.id); - // But fields updated assert_eq!(second.name.as_deref(), Some("Updated")); assert!(second.avatar_url.is_some()); } diff --git a/crates/funnel-server/src/proxy/mod.rs b/crates/funnel-server/src/proxy/mod.rs index b48c7fd..e69de29 100644 --- a/crates/funnel-server/src/proxy/mod.rs +++ b/crates/funnel-server/src/proxy/mod.rs @@ -1 +0,0 @@ -// Proxy/routing — to be implemented in Phase 1, increment 2 diff --git a/crates/funnel-server/src/tls/mod.rs b/crates/funnel-server/src/tls/mod.rs index 9fb262b..e69de29 100644 --- a/crates/funnel-server/src/tls/mod.rs +++ b/crates/funnel-server/src/tls/mod.rs @@ -1 +0,0 @@ -// TLS/ACME certificate management — to be implemented in Phase 4 diff --git a/crates/funnel-server/src/tunnel/mod.rs b/crates/funnel-server/src/tunnel/mod.rs index bb00f67..e69de29 100644 --- a/crates/funnel-server/src/tunnel/mod.rs +++ b/crates/funnel-server/src/tunnel/mod.rs @@ -1 +0,0 @@ -// Tunnel management — to be implemented in Phase 1, increment 2 diff --git a/crates/funnel-server/src/ws/mod.rs b/crates/funnel-server/src/ws/mod.rs index ef3549a..e69de29 100644 --- a/crates/funnel-server/src/ws/mod.rs +++ b/crates/funnel-server/src/ws/mod.rs @@ -1 +0,0 @@ -// WebSocket handler — to be implemented in Phase 1, increment 2 diff --git a/crates/funnel-web/src/lib.rs b/crates/funnel-web/src/lib.rs index e8c5030..433d7dd 100644 --- a/crates/funnel-web/src/lib.rs +++ b/crates/funnel-web/src/lib.rs @@ -1 +1 @@ -// Leptos frontend - to be implemented in Phase 3 +// leptos frontend From a87fe75dd4068e009a425122553d74aeeafbbfb3 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 15 Feb 2026 15:30:00 +0100 Subject: [PATCH 07/18] feat(tunnel): add tunnel manager, active tunnel, and stats --- crates/funnel-server/Cargo.toml | 2 + crates/funnel-server/src/tunnel/connection.rs | 236 ++++++++++++++++++ crates/funnel-server/src/tunnel/manager.rs | 102 ++++++++ crates/funnel-server/src/tunnel/mod.rs | 3 + crates/funnel-server/src/tunnel/stats.rs | 75 ++++++ 5 files changed, 418 insertions(+) create mode 100644 crates/funnel-server/src/tunnel/connection.rs create mode 100644 crates/funnel-server/src/tunnel/manager.rs create mode 100644 crates/funnel-server/src/tunnel/stats.rs diff --git a/crates/funnel-server/Cargo.toml b/crates/funnel-server/Cargo.toml index 1f2f334..4ff9699 100644 --- a/crates/funnel-server/Cargo.toml +++ b/crates/funnel-server/Cargo.toml @@ -12,12 +12,14 @@ axum = { version = "0.8", features = ["ws"] } chrono = { version = "0.4", features = ["serde"] } clap = { version = "4", features = ["derive"] } dashmap = "6" +futures = "0.3" jsonwebtoken = "9" serde = { version = "1", features = ["derive"] } serde_json = "1" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "migrate"] } thiserror = "2" tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" tower = "0.5" tower-http = { version = "0.6", features = ["cors", "trace"] } tracing = "0.1" diff --git a/crates/funnel-server/src/tunnel/connection.rs b/crates/funnel-server/src/tunnel/connection.rs new file mode 100644 index 0000000..0a05a63 --- /dev/null +++ b/crates/funnel-server/src/tunnel/connection.rs @@ -0,0 +1,236 @@ +use std::sync::Arc; +use std::time::Duration; + +use axum::extract::ws::{Message, WebSocket}; +use dashmap::DashMap; +use futures::{SinkExt, StreamExt}; +use tokio::sync::{mpsc, oneshot}; +use tokio_util::sync::CancellationToken; +use uuid::Uuid; + +use funnel_core::protocol::{ResponsePayload, TunnelMessage}; +use funnel_core::tunnel::TunnelId; + +use super::stats::TunnelStats; + +const CHANNEL_BUFFER: usize = 128; +const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + +pub struct ActiveTunnel { + id: TunnelId, + outgoing_tx: mpsc::Sender, + pending_requests: DashMap>, + stats: TunnelStats, + connected_at: tokio::time::Instant, + cancel: CancellationToken, +} + +impl ActiveTunnel { + pub fn id(&self) -> &TunnelId { + &self.id + } + + pub fn stats(&self) -> super::stats::TunnelStatsSnapshot { + self.stats.snapshot() + } + + pub fn connected_at(&self) -> tokio::time::Instant { + self.connected_at + } + + pub fn is_alive(&self) -> bool { + !self.cancel.is_cancelled() + } + + /// wait until the tunnel connection has been closed. + pub async fn cancelled(&self) { + self.cancel.cancelled().await; + } + + /// send a request through the tunnel and wait for the client's response. + pub async fn send_request( + &self, + payload: funnel_core::protocol::RequestPayload, + ) -> Result { + let request_id = Uuid::now_v7(); + let (tx, rx) = oneshot::channel(); + + self.pending_requests.insert(request_id, tx); + self.stats.inc_requests(); + + let msg = TunnelMessage::Request { + tunnel_id: self.id.clone(), + request_id, + payload, + }; + + if self.outgoing_tx.send(msg).await.is_err() { + self.pending_requests.remove(&request_id); + return Err(SendError::TunnelClosed); + } + + let result = tokio::time::timeout(REQUEST_TIMEOUT, rx).await; + + // clean up regardless of outcome + self.pending_requests.remove(&request_id); + + match result { + Ok(Ok(response)) => Ok(response), + Ok(Err(_)) => Err(SendError::TunnelClosed), + Err(_) => { + // send cancel so the client can stop working on this request + let _ = self.outgoing_tx.send(TunnelMessage::RequestCancel { request_id }).await; + Err(SendError::Timeout) + } + } + } + + /// shut down the tunnel connection. + pub fn close(&self) { + self.cancel.cancel(); + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SendError { + #[error("tunnel connection closed")] + TunnelClosed, + #[error("request timed out")] + Timeout, +} + +/// spawn the read/write tasks for a websocket connection and return the active tunnel. +/// the returned tunnel is live immediately; the background tasks run until the +/// connection drops or `close()` is called. +pub fn spawn(id: TunnelId, ws: WebSocket) -> Arc { + let (ws_sink, ws_stream) = ws.split(); + let (outgoing_tx, outgoing_rx) = mpsc::channel(CHANNEL_BUFFER); + let cancel = CancellationToken::new(); + + let tunnel = Arc::new(ActiveTunnel { + id: id.clone(), + outgoing_tx, + pending_requests: DashMap::new(), + stats: TunnelStats::new(), + connected_at: tokio::time::Instant::now(), + cancel: cancel.clone(), + }); + + let read_tunnel = Arc::clone(&tunnel); + let read_cancel = cancel.clone(); + tokio::spawn(async move { + read_loop(ws_stream, &read_tunnel, read_cancel).await; + }); + + let write_cancel = cancel.clone(); + tokio::spawn(async move { + write_loop(ws_sink, outgoing_rx, write_cancel).await; + }); + + tunnel +} + +async fn read_loop( + mut stream: futures::stream::SplitStream, + tunnel: &ActiveTunnel, + cancel: CancellationToken, +) { + loop { + let msg = tokio::select! { + msg = stream.next() => msg, + _ = cancel.cancelled() => break, + }; + + let Some(result) = msg else { break }; + + let frame = match result { + Ok(frame) => frame, + Err(e) => { + tracing::debug!(tunnel_id = %tunnel.id, error = %e, "websocket read error"); + break; + } + }; + + match frame { + Message::Text(text) => { + tunnel.stats.add_bytes_in(text.len() as u64); + handle_incoming_text(tunnel, &text); + } + Message::Close(_) => break, + _ => {} + } + } + + tracing::debug!(tunnel_id = %tunnel.id, "read loop exiting"); + cancel.cancel(); +} + +fn handle_incoming_text(tunnel: &ActiveTunnel, text: &str) { + let msg: TunnelMessage = match serde_json::from_str(text) { + Ok(m) => m, + Err(e) => { + tracing::warn!(tunnel_id = %tunnel.id, error = %e, "malformed message from client"); + return; + } + }; + + match msg { + TunnelMessage::Response { + request_id, + payload, + } => { + if let Some((_, tx)) = tunnel.pending_requests.remove(&request_id) { + let _ = tx.send(payload); + } + } + TunnelMessage::Pong => {} + other => { + tracing::debug!( + tunnel_id = %tunnel.id, + msg_type = ?other, + "unexpected message type from client" + ); + } + } +} + +async fn write_loop( + mut sink: futures::stream::SplitSink, + mut rx: mpsc::Receiver, + cancel: CancellationToken, +) { + loop { + let msg = tokio::select! { + msg = rx.recv() => msg, + _ = cancel.cancelled() => break, + }; + + let Some(msg) = msg else { break }; + + let json = match serde_json::to_string(&msg) { + Ok(j) => j, + Err(e) => { + tracing::error!(error = %e, "failed to serialize tunnel message"); + continue; + } + }; + + if sink.send(Message::text(json)).await.is_err() { + break; + } + } + + tracing::debug!("write loop exiting"); + cancel.cancel(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn send_error_display() { + assert_eq!(SendError::TunnelClosed.to_string(), "tunnel connection closed"); + assert_eq!(SendError::Timeout.to_string(), "request timed out"); + } +} diff --git a/crates/funnel-server/src/tunnel/manager.rs b/crates/funnel-server/src/tunnel/manager.rs new file mode 100644 index 0000000..5de137e --- /dev/null +++ b/crates/funnel-server/src/tunnel/manager.rs @@ -0,0 +1,102 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use dashmap::mapref::entry::Entry; +use serde::Serialize; + +use funnel_core::tunnel::TunnelId; + +use super::connection::ActiveTunnel; +use super::stats::TunnelStatsSnapshot; + +pub struct TunnelManager { + tunnels: DashMap>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct TunnelInfo { + pub id: String, + pub uptime_secs: f64, + pub stats: TunnelStatsSnapshot, +} + +impl TunnelManager { + pub fn new() -> Self { + Self { + tunnels: DashMap::new(), + } + } + + pub fn insert(&self, id: TunnelId, tunnel: Arc) -> Result<(), Arc> { + match self.tunnels.entry(id) { + Entry::Occupied(_) => Err(tunnel), + Entry::Vacant(e) => { + e.insert(tunnel); + Ok(()) + } + } + } + + pub fn remove(&self, id: &TunnelId) -> Option> { + self.tunnels.remove(id).map(|(_, t)| t) + } + + pub fn get(&self, id: &TunnelId) -> Option> { + self.tunnels.get(id).map(|r| Arc::clone(r.value())) + } + + pub fn exists(&self, id: &TunnelId) -> bool { + self.tunnels.contains_key(id) + } + + pub fn list(&self) -> Vec { + self.tunnels + .iter() + .map(|entry| { + let tunnel = entry.value(); + TunnelInfo { + id: entry.key().to_string(), + uptime_secs: tunnel.connected_at().elapsed().as_secs_f64(), + stats: tunnel.stats(), + } + }) + .collect() + } + + pub fn count(&self) -> usize { + self.tunnels.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_manager_is_empty() { + let mgr = TunnelManager::new(); + assert_eq!(mgr.count(), 0); + assert!(mgr.list().is_empty()); + } + + #[test] + fn exists_returns_false_for_missing() { + let mgr = TunnelManager::new(); + let id = TunnelId::new("test-abc").unwrap(); + assert!(!mgr.exists(&id)); + } + + #[test] + fn get_returns_none_for_missing() { + let mgr = TunnelManager::new(); + let id = TunnelId::new("test-abc").unwrap(); + assert!(mgr.get(&id).is_none()); + } + + #[test] + fn remove_returns_none_for_missing() { + let mgr = TunnelManager::new(); + let id = TunnelId::new("test-abc").unwrap(); + assert!(mgr.remove(&id).is_none()); + } +} diff --git a/crates/funnel-server/src/tunnel/mod.rs b/crates/funnel-server/src/tunnel/mod.rs index e69de29..ef787ad 100644 --- a/crates/funnel-server/src/tunnel/mod.rs +++ b/crates/funnel-server/src/tunnel/mod.rs @@ -0,0 +1,3 @@ +pub mod connection; +pub mod manager; +pub mod stats; diff --git a/crates/funnel-server/src/tunnel/stats.rs b/crates/funnel-server/src/tunnel/stats.rs new file mode 100644 index 0000000..d90738f --- /dev/null +++ b/crates/funnel-server/src/tunnel/stats.rs @@ -0,0 +1,75 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +use serde::Serialize; + +pub struct TunnelStats { + bytes_in: AtomicU64, + bytes_out: AtomicU64, + requests: AtomicU64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct TunnelStatsSnapshot { + pub bytes_in: u64, + pub bytes_out: u64, + pub requests: u64, +} + +impl TunnelStats { + pub fn new() -> Self { + Self { + bytes_in: AtomicU64::new(0), + bytes_out: AtomicU64::new(0), + requests: AtomicU64::new(0), + } + } + + pub fn add_bytes_in(&self, n: u64) { + self.bytes_in.fetch_add(n, Ordering::Relaxed); + } + + pub fn add_bytes_out(&self, n: u64) { + self.bytes_out.fetch_add(n, Ordering::Relaxed); + } + + pub fn inc_requests(&self) { + self.requests.fetch_add(1, Ordering::Relaxed); + } + + pub fn snapshot(&self) -> TunnelStatsSnapshot { + TunnelStatsSnapshot { + bytes_in: self.bytes_in.load(Ordering::Relaxed), + bytes_out: self.bytes_out.load(Ordering::Relaxed), + requests: self.requests.load(Ordering::Relaxed), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tracks_bytes_and_requests() { + let stats = TunnelStats::new(); + stats.add_bytes_in(100); + stats.add_bytes_in(200); + stats.add_bytes_out(50); + stats.inc_requests(); + stats.inc_requests(); + + let snap = stats.snapshot(); + assert_eq!(snap.bytes_in, 300); + assert_eq!(snap.bytes_out, 50); + assert_eq!(snap.requests, 2); + } + + #[test] + fn snapshot_starts_at_zero() { + let stats = TunnelStats::new(); + let snap = stats.snapshot(); + assert_eq!(snap.bytes_in, 0); + assert_eq!(snap.bytes_out, 0); + assert_eq!(snap.requests, 0); + } +} From 7e946578be1372f2a44a54202388806bec6e8210 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Mon, 16 Feb 2026 11:00:00 +0100 Subject: [PATCH 08/18] feat(ws): add websocket upgrade handler for tunnel clients --- crates/funnel-server/src/ws/handler.rs | 68 ++++++++++++++++++++++++++ crates/funnel-server/src/ws/mod.rs | 1 + 2 files changed, 69 insertions(+) create mode 100644 crates/funnel-server/src/ws/handler.rs diff --git a/crates/funnel-server/src/ws/handler.rs b/crates/funnel-server/src/ws/handler.rs new file mode 100644 index 0000000..13f2f54 --- /dev/null +++ b/crates/funnel-server/src/ws/handler.rs @@ -0,0 +1,68 @@ +use std::sync::Arc; + +use axum::extract::ws::WebSocket; +use axum::extract::{Query, State, WebSocketUpgrade}; +use axum::response::Response; +use serde::Deserialize; + +use funnel_core::tunnel::TunnelId; + +use crate::app::AppState; +use crate::error::AppError; +use crate::tunnel::connection; + +#[derive(Deserialize)] +pub struct ConnectParams { + id: Option, +} + +pub async fn upgrade( + State(state): State>, + Query(params): Query, + ws: WebSocketUpgrade, +) -> Result { + let tunnel_id = match params.id { + Some(raw) => TunnelId::new(raw)?, + None => TunnelId::generate(), + }; + + if state.tunnels.exists(&tunnel_id) { + return Err(AppError::TunnelConflict(tunnel_id.to_string())); + } + + tracing::info!(tunnel_id = %tunnel_id, "websocket upgrade accepted"); + + Ok(ws.on_upgrade(move |socket| handle_socket(state, tunnel_id, socket))) +} + +async fn handle_socket(state: Arc, id: TunnelId, socket: WebSocket) { + let tunnel = connection::spawn(id.clone(), socket); + + if let Err(_returned) = state.tunnels.insert(id.clone(), Arc::clone(&tunnel)) { + tracing::warn!(tunnel_id = %id, "tunnel id became occupied during upgrade"); + tunnel.close(); + return; + } + + tracing::info!( + tunnel_id = %id, + total_tunnels = state.tunnels.count(), + "tunnel connected" + ); + + // wait until the tunnel connection drops + tunnel.cancelled().await; + + let stats = tunnel.stats(); + state.tunnels.remove(&id); + + tracing::info!( + tunnel_id = %id, + bytes_in = stats.bytes_in, + bytes_out = stats.bytes_out, + requests = stats.requests, + uptime_secs = tunnel.connected_at().elapsed().as_secs_f64(), + remaining_tunnels = state.tunnels.count(), + "tunnel disconnected" + ); +} diff --git a/crates/funnel-server/src/ws/mod.rs b/crates/funnel-server/src/ws/mod.rs index e69de29..062ae9d 100644 --- a/crates/funnel-server/src/ws/mod.rs +++ b/crates/funnel-server/src/ws/mod.rs @@ -0,0 +1 @@ +pub mod handler; From ecf579dcca30cfc0dcfd64a7b3f3e092a9aa8f3c Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 8 Mar 2026 14:00:00 +0100 Subject: [PATCH 09/18] feat(proxy): add subdomain router and forwarding headers --- crates/funnel-server/src/proxy/headers.rs | 94 +++++++++++++ crates/funnel-server/src/proxy/mod.rs | 2 + crates/funnel-server/src/proxy/router.rs | 158 ++++++++++++++++++++++ 3 files changed, 254 insertions(+) create mode 100644 crates/funnel-server/src/proxy/headers.rs create mode 100644 crates/funnel-server/src/proxy/router.rs diff --git a/crates/funnel-server/src/proxy/headers.rs b/crates/funnel-server/src/proxy/headers.rs new file mode 100644 index 0000000..b171f59 --- /dev/null +++ b/crates/funnel-server/src/proxy/headers.rs @@ -0,0 +1,94 @@ +use std::collections::HashMap; +use std::net::SocketAddr; + +use axum::http::{HeaderMap, HeaderValue}; + +/// copy request headers and add standard proxy headers (X-Forwarded-For, etc). +pub fn prepare_forwarding_headers( + original: &HeaderMap, + host: &str, + remote_addr: SocketAddr, + is_tls: bool, +) -> HashMap> { + let mut headers: HashMap> = HashMap::new(); + + for (name, value) in original.iter() { + let key = name.as_str().to_string(); + let val = value.to_str().unwrap_or("").to_string(); + headers.entry(key).or_default().push(val); + } + + let client_ip = remote_addr.ip().to_string(); + + // append to existing X-Forwarded-For or create new + match headers.get("x-forwarded-for") { + Some(existing) if !existing.is_empty() => { + let combined = format!("{}, {}", existing[0], client_ip); + headers.insert("x-forwarded-for".to_string(), vec![combined]); + } + _ => { + headers.insert("x-forwarded-for".to_string(), vec![client_ip.clone()]); + } + } + + if !host.is_empty() { + headers.insert("x-forwarded-host".to_string(), vec![host.to_string()]); + } + + let proto = if is_tls { "https" } else { "http" }; + headers.insert("x-forwarded-proto".to_string(), vec![proto.to_string()]); + headers.insert("x-real-ip".to_string(), vec![client_ip]); + + headers +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{IpAddr, Ipv4Addr}; + + fn test_addr() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 54321) + } + + #[test] + fn adds_forwarding_headers() { + let headers = HeaderMap::new(); + let result = prepare_forwarding_headers(&headers, "my-tunnel.example.com", test_addr(), false); + + assert_eq!(result["x-forwarded-for"], vec!["192.168.1.100"]); + assert_eq!(result["x-forwarded-host"], vec!["my-tunnel.example.com"]); + assert_eq!(result["x-forwarded-proto"], vec!["http"]); + assert_eq!(result["x-real-ip"], vec!["192.168.1.100"]); + } + + #[test] + fn appends_to_existing_forwarded_for() { + let mut headers = HeaderMap::new(); + headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.1")); + + let result = prepare_forwarding_headers(&headers, "t.example.com", test_addr(), false); + + assert_eq!(result["x-forwarded-for"], vec!["10.0.0.1, 192.168.1.100"]); + } + + #[test] + fn sets_https_proto_when_tls() { + let headers = HeaderMap::new(); + let result = prepare_forwarding_headers(&headers, "t.example.com", test_addr(), true); + + assert_eq!(result["x-forwarded-proto"], vec!["https"]); + } + + #[test] + fn copies_original_headers() { + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_static("application/json")); + headers.insert("authorization", HeaderValue::from_static("Bearer token123")); + + let result = prepare_forwarding_headers(&headers, "t.example.com", test_addr(), false); + + assert_eq!(result["content-type"], vec!["application/json"]); + assert_eq!(result["authorization"], vec!["Bearer token123"]); + } +} diff --git a/crates/funnel-server/src/proxy/mod.rs b/crates/funnel-server/src/proxy/mod.rs index e69de29..9fd8957 100644 --- a/crates/funnel-server/src/proxy/mod.rs +++ b/crates/funnel-server/src/proxy/mod.rs @@ -0,0 +1,2 @@ +pub mod headers; +pub mod router; diff --git a/crates/funnel-server/src/proxy/router.rs b/crates/funnel-server/src/proxy/router.rs new file mode 100644 index 0000000..3320b3a --- /dev/null +++ b/crates/funnel-server/src/proxy/router.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use axum::body::Body; +use axum::extract::{ConnectInfo, Host, State}; +use axum::http::{Request, Response, StatusCode}; + +use funnel_core::protocol::RequestPayload; +use funnel_core::tunnel::TunnelId; + +use crate::app::AppState; +use super::headers::prepare_forwarding_headers; + +const MAX_BODY_SIZE: usize = 10 * 1024 * 1024; // 10 mb + +/// axum fallback handler that routes requests based on subdomain. +/// requests to `{tunnel_id}.{base_domain}` are forwarded through the matching tunnel. +pub async fn handle_tunnel_request( + State(state): State>, + Host(host): Host, + ConnectInfo(remote_addr): ConnectInfo, + request: Request, +) -> Response { + let subdomain = match extract_subdomain(&host) { + Some(s) => s, + None => return not_found("tunnel not found"), + }; + + let tunnel_id = match TunnelId::new(subdomain) { + Ok(id) => id, + Err(_) => return not_found("tunnel not found"), + }; + + let tunnel = match state.tunnels.get(&tunnel_id) { + Some(t) => t, + None => return not_found("tunnel not found"), + }; + + let method = request.method().to_string(); + let path = request.uri().to_string(); + let is_tls = false; // will be determined by tls termination layer later + + let headers = prepare_forwarding_headers(request.headers(), &host, remote_addr, is_tls); + + let body_bytes = match axum::body::to_bytes(request.into_body(), MAX_BODY_SIZE).await { + Ok(b) => b.to_vec(), + Err(_) => return error_response(StatusCode::BAD_REQUEST, "request body too large"), + }; + + let payload = RequestPayload { + method, + path, + headers, + body: body_bytes, + }; + + match tunnel.send_request(payload).await { + Ok(response) => build_response(response), + Err(crate::tunnel::connection::SendError::Timeout) => { + error_response(StatusCode::GATEWAY_TIMEOUT, "request timed out") + } + Err(crate::tunnel::connection::SendError::TunnelClosed) => { + error_response(StatusCode::BAD_GATEWAY, "tunnel connection lost") + } + } +} + +/// extract the first subdomain label from a host string. +/// `"my-tunnel.example.com"` -> `Some("my-tunnel")` +/// `"example.com"` -> `None` (no subdomain) +/// `"localhost:8080"` -> `None` +fn extract_subdomain(host: &str) -> Option<&str> { + // strip port if present + let host = host.split(':').next().unwrap_or(host); + + let dot = host.find('.')?; + if dot == 0 { + return None; + } + + let subdomain = &host[..dot]; + let rest = &host[dot + 1..]; + + // need at least one more dot for it to be a real subdomain + // (subdomain.domain.tld), not just (domain.tld) + if rest.contains('.') { + Some(subdomain) + } else { + None + } +} + +fn build_response(payload: funnel_core::protocol::ResponsePayload) -> Response { + let mut builder = Response::builder().status(payload.status); + + if let Some(headers) = builder.headers_mut() { + for (name, values) in &payload.headers { + for value in values { + if let (Ok(name), Ok(value)) = ( + axum::http::HeaderName::try_from(name.as_str()), + axum::http::HeaderValue::from_str(value), + ) { + headers.append(name, value); + } + } + } + } + + builder + .body(Body::from(payload.body)) + .unwrap_or_else(|_| error_response(StatusCode::INTERNAL_SERVER_ERROR, "internal error")) +} + +fn not_found(msg: &str) -> Response { + error_response(StatusCode::NOT_FOUND, msg) +} + +fn error_response(status: StatusCode, msg: &str) -> Response { + Response::builder() + .status(status) + .header("content-type", "text/plain") + .body(Body::from(msg.to_string())) + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_subdomain_basic() { + assert_eq!(extract_subdomain("my-tunnel.example.com"), Some("my-tunnel")); + assert_eq!(extract_subdomain("abc.example.com"), Some("abc")); + } + + #[test] + fn extract_subdomain_with_port() { + assert_eq!(extract_subdomain("my-tunnel.example.com:8080"), Some("my-tunnel")); + } + + #[test] + fn extract_subdomain_no_subdomain() { + assert_eq!(extract_subdomain("example.com"), None); + assert_eq!(extract_subdomain("localhost"), None); + assert_eq!(extract_subdomain("localhost:8080"), None); + } + + #[test] + fn extract_subdomain_empty_or_dot_prefix() { + assert_eq!(extract_subdomain(""), None); + assert_eq!(extract_subdomain(".example.com"), None); + } + + #[test] + fn extract_subdomain_deep_nesting() { + // only extracts the first label + assert_eq!(extract_subdomain("a.b.example.com"), Some("a")); + } +} From 7821caa92d2c196f5b63e4708088a899ae60b303 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 8 Mar 2026 15:20:00 +0100 Subject: [PATCH 10/18] feat(server): wire tunnel engine into app state and routes --- crates/funnel-server/src/api/tunnels.rs | 48 +++++++++++++++---------- crates/funnel-server/src/app.rs | 7 ++++ crates/funnel-server/src/main.rs | 6 +++- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/crates/funnel-server/src/api/tunnels.rs b/crates/funnel-server/src/api/tunnels.rs index c3465a8..0b7dc94 100644 --- a/crates/funnel-server/src/api/tunnels.rs +++ b/crates/funnel-server/src/api/tunnels.rs @@ -2,37 +2,47 @@ use std::sync::Arc; use axum::Json; use axum::extract::{Path, State}; -use serde::Serialize; + +use funnel_core::tunnel::TunnelId; use crate::app::AppState; use crate::error::AppError; +use crate::tunnel::manager::TunnelInfo; -#[derive(Serialize)] -pub struct TunnelInfo { - pub id: String, - pub status: &'static str, -} - -/// List all active tunnels. -pub async fn list(State(_state): State>) -> Json> { - // TODO: wire to TunnelManager once tunnel module is implemented - Json(vec![]) +pub async fn list(State(state): State>) -> Json> { + Json(state.tunnels.list()) } -/// Get a specific tunnel by ID. pub async fn get_tunnel( - State(_state): State>, + State(state): State>, Path(id): Path, ) -> Result, AppError> { - // TODO: wire to TunnelManager - Err(AppError::TunnelNotFound(id)) + let tunnel_id = TunnelId::new(id.clone())?; + + let tunnel = state + .tunnels + .get(&tunnel_id) + .ok_or_else(|| AppError::TunnelNotFound(id))?; + + Ok(Json(TunnelInfo { + id: tunnel.id().to_string(), + uptime_secs: tunnel.connected_at().elapsed().as_secs_f64(), + stats: tunnel.stats(), + })) } -/// Delete (force-close) a tunnel. pub async fn delete( - State(_state): State>, + State(state): State>, Path(id): Path, ) -> Result, AppError> { - // TODO: wire to TunnelManager - Err(AppError::TunnelNotFound(id)) + let tunnel_id = TunnelId::new(id.clone())?; + + let tunnel = state + .tunnels + .remove(&tunnel_id) + .ok_or_else(|| AppError::TunnelNotFound(id))?; + + tunnel.close(); + + Ok(Json(serde_json::json!({ "deleted": true }))) } diff --git a/crates/funnel-server/src/app.rs b/crates/funnel-server/src/app.rs index 29db85f..95bfcaa 100644 --- a/crates/funnel-server/src/app.rs +++ b/crates/funnel-server/src/app.rs @@ -8,9 +8,13 @@ use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; use crate::api; +use crate::tunnel::manager::TunnelManager; +use crate::ws; +use crate::proxy; pub struct AppState { pub db: PgPool, + pub tunnels: TunnelManager, pub start_time: Instant, } @@ -18,6 +22,7 @@ impl AppState { pub fn new(db: PgPool) -> Self { Self { db, + tunnels: TunnelManager::new(), start_time: Instant::now(), } } @@ -36,6 +41,8 @@ pub fn build_router(state: Arc) -> Router { Router::new() .nest("/api", api_routes) + .route("/ws", get(ws::handler::upgrade)) + .fallback(proxy::router::handle_tunnel_request) .with_state(state) .layer(TraceLayer::new_for_http()) .layer(CorsLayer::permissive()) diff --git a/crates/funnel-server/src/main.rs b/crates/funnel-server/src/main.rs index 993d7fa..c60dd8e 100644 --- a/crates/funnel-server/src/main.rs +++ b/crates/funnel-server/src/main.rs @@ -68,7 +68,11 @@ async fn main() -> anyhow::Result<()> { tracing::info!(addr = %addr, "server listening"); - axum::serve(listener, router).await?; + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await?; Ok(()) } From 9fd7fdfef3aa466188e7fcc163a99129ffe1b509 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 5 Apr 2026 13:00:00 +0200 Subject: [PATCH 11/18] feat(client): add tunnel client with websocket, forwarding, and cli --- crates/funnel-client/Cargo.toml | 9 +- crates/funnel-client/src/config.rs | 120 ++++++++++ crates/funnel-client/src/forwarder.rs | 123 +++++++++++ crates/funnel-client/src/main.rs | 207 ++++++++++++++++- crates/funnel-client/src/runner.rs | 116 ++++++++++ crates/funnel-client/src/tunnel.rs | 305 ++++++++++++++++++++++++++ 6 files changed, 876 insertions(+), 4 deletions(-) create mode 100644 crates/funnel-client/src/config.rs create mode 100644 crates/funnel-client/src/forwarder.rs create mode 100644 crates/funnel-client/src/runner.rs create mode 100644 crates/funnel-client/src/tunnel.rs diff --git a/crates/funnel-client/Cargo.toml b/crates/funnel-client/Cargo.toml index 5e9b894..a87bb2c 100644 --- a/crates/funnel-client/Cargo.toml +++ b/crates/funnel-client/Cargo.toml @@ -8,12 +8,17 @@ funnel-core = { path = "../funnel-core" } anyhow = "1" clap = { version = "4", features = ["derive"] } -reqwest = { version = "0.12", features = ["json"] } +dirs = "6" +futures = "0.3" +http = "1" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] } +tokio-util = "0.7" toml = "0.8" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2" uuid = { version = "1", features = ["v7"] } diff --git a/crates/funnel-client/src/config.rs b/crates/funnel-client/src/config.rs new file mode 100644 index 0000000..f0c9ac5 --- /dev/null +++ b/crates/funnel-client/src/config.rs @@ -0,0 +1,120 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +const CONFIG_DIR: &str = "funnel"; +const CONFIG_FILE: &str = "config.toml"; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + #[serde(default)] + pub inlets: HashMap, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Inlet { + #[serde(default)] + pub server: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub domain: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token: Option, +} + +pub fn config_path() -> PathBuf { + if let Some(dir) = dirs::config_dir() { + return dir.join(CONFIG_DIR).join(CONFIG_FILE); + } + PathBuf::from(CONFIG_FILE) +} + +pub fn load() -> anyhow::Result { + let path = config_path(); + if !path.exists() { + return Ok(Config::default()); + } + let content = std::fs::read_to_string(&path)?; + let config: Config = toml::from_str(&content)?; + Ok(config) +} + +pub fn save(config: &Config) -> anyhow::Result<()> { + let path = config_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let content = toml::to_string_pretty(config)?; + std::fs::write(&path, content)?; + + // restrict permissions on unix (file contains tokens) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?; + } + + Ok(()) +} + +pub fn get_inlet(config: &Config, name: &str) -> Option<&Inlet> { + config.inlets.get(name) +} + +pub fn set_token(inlet_name: &str, token: &str) -> anyhow::Result<()> { + let mut config = load()?; + let inlet = config.inlets.entry(inlet_name.to_string()).or_default(); + inlet.token = Some(token.to_string()); + save(&config) +} + +pub fn set_server(inlet_name: &str, server: &str) -> anyhow::Result<()> { + let mut config = load()?; + let inlet = config.inlets.entry(inlet_name.to_string()).or_default(); + inlet.server = server.to_string(); + save(&config) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_config_has_no_inlets() { + let config = Config::default(); + assert!(config.inlets.is_empty()); + } + + #[test] + fn roundtrip_toml() { + let mut config = Config::default(); + config.inlets.insert( + "default".to_string(), + Inlet { + server: "https://tunnel.example.com".to_string(), + domain: None, + token: Some("sk_test123".to_string()), + }, + ); + + let serialized = toml::to_string_pretty(&config).unwrap(); + let parsed: Config = toml::from_str(&serialized).unwrap(); + + let inlet = parsed.inlets.get("default").unwrap(); + assert_eq!(inlet.server, "https://tunnel.example.com"); + assert_eq!(inlet.token.as_deref(), Some("sk_test123")); + assert!(inlet.domain.is_none()); + } + + #[test] + fn get_inlet_returns_none_for_missing() { + let config = Config::default(); + assert!(get_inlet(&config, "default").is_none()); + } + + #[test] + fn config_path_is_not_empty() { + let path = config_path(); + assert!(!path.as_os_str().is_empty()); + } +} diff --git a/crates/funnel-client/src/forwarder.rs b/crates/funnel-client/src/forwarder.rs new file mode 100644 index 0000000..d9f9f87 --- /dev/null +++ b/crates/funnel-client/src/forwarder.rs @@ -0,0 +1,123 @@ +use std::collections::HashMap; + +use funnel_core::protocol::{RequestPayload, ResponsePayload}; + +const HOP_BY_HOP_HEADERS: &[&str] = &[ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "proxy-connection", +]; + +pub struct Forwarder { + client: reqwest::Client, + local_addr: String, +} + +impl Forwarder { + pub fn new(local_addr: String) -> Self { + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .timeout(std::time::Duration::from_secs(30)) + .pool_max_idle_per_host(10) + .build() + .expect("failed to build http client"); + + Self { client, local_addr } + } + + pub async fn forward(&self, req: RequestPayload) -> ResponsePayload { + match self.try_forward(&req).await { + Ok(resp) => resp, + Err(e) => { + tracing::error!(error = %e, method = %req.method, path = %req.path, "forwarding failed"); + error_response(502, &format!("local service error: {e}")) + } + } + } + + async fn try_forward(&self, req: &RequestPayload) -> anyhow::Result { + let url = format!("http://{}{}", self.local_addr, req.path); + + let method: http::Method = req.method.parse()?; + let mut builder = self.client.request(method, &url); + + for (name, values) in &req.headers { + if is_hop_by_hop(name) || name.eq_ignore_ascii_case("host") { + continue; + } + for value in values { + builder = builder.header(name.as_str(), value.as_str()); + } + } + + builder = builder.header("host", &self.local_addr); + builder = builder.body(req.body.clone()); + + let resp = builder.send().await?; + + let status = resp.status().as_u16(); + let headers = collect_response_headers(resp.headers()); + let body = resp.bytes().await?.to_vec(); + + Ok(ResponsePayload { + status, + headers, + body, + }) + } +} + +fn is_hop_by_hop(name: &str) -> bool { + let lower = name.to_ascii_lowercase(); + HOP_BY_HOP_HEADERS.iter().any(|h| *h == lower) +} + +fn collect_response_headers(headers: &reqwest::header::HeaderMap) -> HashMap> { + let mut map: HashMap> = HashMap::new(); + for (name, value) in headers.iter() { + if is_hop_by_hop(name.as_str()) { + continue; + } + let val = value.to_str().unwrap_or("").to_string(); + map.entry(name.to_string()).or_default().push(val); + } + map +} + +fn error_response(status: u16, msg: &str) -> ResponsePayload { + let mut headers = HashMap::new(); + headers.insert("content-type".to_string(), vec!["text/plain".to_string()]); + ResponsePayload { + status, + headers, + body: msg.as_bytes().to_vec(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hop_by_hop_detection() { + assert!(is_hop_by_hop("Connection")); + assert!(is_hop_by_hop("transfer-encoding")); + assert!(is_hop_by_hop("Keep-Alive")); + assert!(!is_hop_by_hop("Content-Type")); + assert!(!is_hop_by_hop("Authorization")); + } + + #[test] + fn error_response_has_correct_status() { + let resp = error_response(502, "test error"); + assert_eq!(resp.status, 502); + assert_eq!(resp.body, b"test error"); + assert!(resp.headers.contains_key("content-type")); + } +} diff --git a/crates/funnel-client/src/main.rs b/crates/funnel-client/src/main.rs index ae062fd..4602c08 100644 --- a/crates/funnel-client/src/main.rs +++ b/crates/funnel-client/src/main.rs @@ -1,3 +1,206 @@ -fn main() { - println!("funnel-client"); +mod config; +mod forwarder; +mod runner; +mod tunnel; + +use clap::{Parser, Subcommand}; +use tokio_util::sync::CancellationToken; +use tracing_subscriber::EnvFilter; + +use funnel_core::tunnel::TunnelId; + +#[derive(Parser)] +#[command(name = "funnel", about = "tunnel client for exposing local services")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// create an http tunnel + Http { + /// local address or port to forward to (e.g. "3000" or "localhost:3000") + address: String, + + /// tunnel server url (overrides config) + #[arg(short, long)] + server: Option, + + /// tunnel id (subdomain), generated if omitted + #[arg(short, long)] + id: Option, + + /// inlet configuration to use + #[arg(long, default_value = "default")] + inlet: String, + + /// authentication token (overrides config) + #[arg(short, long)] + token: Option, + }, + /// manage client configuration + Config { + #[command(subcommand)] + action: ConfigAction, + }, +} + +#[derive(Subcommand)] +enum ConfigAction { + /// save authentication token + SetToken { + token: String, + #[arg(long, default_value = "default")] + inlet: String, + }, + /// save server url + SetServer { + url: String, + #[arg(long, default_value = "default")] + inlet: String, + }, + /// show current configuration + Show, + /// show config file path + Path, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + let cli = Cli::parse(); + + match cli.command { + Command::Http { + address, + server, + id, + inlet, + token, + } => run_http(address, server, id, inlet, token).await?, + Command::Config { action } => run_config(action)?, + } + + Ok(()) +} + +async fn run_http( + address: String, + server_flag: Option, + id_flag: Option, + inlet_name: String, + token_flag: Option, +) -> anyhow::Result<()> { + let local_addr = normalize_address(&address); + + let cfg = config::load().unwrap_or_default(); + let inlet = config::get_inlet(&cfg, &inlet_name); + + let server_url = server_flag + .or_else(|| inlet.map(|i| i.server.clone())) + .filter(|s| !s.is_empty()) + .ok_or_else(|| { + anyhow::anyhow!( + "no server configured. use --server or run: funnel config set-server " + ) + })?; + + let token = token_flag.or_else(|| inlet.and_then(|i| i.token.clone())); + + let tunnel_id = match id_flag { + Some(raw) => TunnelId::new(raw)?, + None => TunnelId::generate(), + }; + + if let Some(public_url) = runner::build_public_url(&server_url, &tunnel_id) { + tracing::info!( + tunnel_id = %tunnel_id, + local = %local_addr, + public_url = %public_url, + "starting tunnel" + ); + } + + let shutdown = CancellationToken::new(); + let shutdown_signal = shutdown.clone(); + + tokio::spawn(async move { + tokio::signal::ctrl_c().await.ok(); + tracing::info!("received shutdown signal"); + shutdown_signal.cancel(); + }); + + runner::run(tunnel_id, server_url, local_addr, token, shutdown).await; + + Ok(()) +} + +fn normalize_address(addr: &str) -> String { + if addr.contains(':') { + addr.to_string() + } else { + format!("localhost:{addr}") + } +} + +fn run_config(action: ConfigAction) -> anyhow::Result<()> { + match action { + ConfigAction::SetToken { token, inlet } => { + config::set_token(&inlet, &token)?; + println!("token saved to inlet \"{inlet}\""); + println!(" config: {}", config::config_path().display()); + } + ConfigAction::SetServer { url, inlet } => { + config::set_server(&inlet, &url)?; + println!("server saved to inlet \"{inlet}\""); + println!(" config: {}", config::config_path().display()); + } + ConfigAction::Show => { + let cfg = config::load()?; + println!("config: {}\n", config::config_path().display()); + if cfg.inlets.is_empty() { + println!("no inlets configured."); + } else { + for (name, inlet) in &cfg.inlets { + println!("[{name}]"); + if !inlet.server.is_empty() { + println!(" server: {}", inlet.server); + } + if let Some(domain) = &inlet.domain { + println!(" domain: {domain}"); + } + if let Some(token) = &inlet.token { + let visible = &token[..token.len().min(10)]; + println!(" token: {visible}..."); + } + println!(); + } + } + } + ConfigAction::Path => { + println!("{}", config::config_path().display()); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalize_port_only() { + assert_eq!(normalize_address("3000"), "localhost:3000"); + } + + #[test] + fn normalize_full_address() { + assert_eq!(normalize_address("127.0.0.1:8080"), "127.0.0.1:8080"); + } } diff --git a/crates/funnel-client/src/runner.rs b/crates/funnel-client/src/runner.rs new file mode 100644 index 0000000..a6ab15a --- /dev/null +++ b/crates/funnel-client/src/runner.rs @@ -0,0 +1,116 @@ +use std::time::Duration; + +use tokio_util::sync::CancellationToken; +use url::Url; + +use funnel_core::tunnel::TunnelId; + +use crate::tunnel::TunnelClient; + +const INITIAL_BACKOFF: Duration = Duration::from_secs(1); +const MAX_BACKOFF: Duration = Duration::from_secs(30); + +pub async fn run( + tunnel_id: TunnelId, + server_url: String, + local_addr: String, + token: Option, + shutdown: CancellationToken, +) { + let mut attempt: u32 = 0; + + loop { + if shutdown.is_cancelled() { + break; + } + + let client = TunnelClient::new( + tunnel_id.clone(), + server_url.clone(), + local_addr.clone(), + token.clone(), + ); + + tracing::info!(attempt = attempt + 1, "connecting to server"); + + let run_cancel = shutdown.child_token(); + match client.run(run_cancel).await { + Ok(()) => { + attempt = 0; + tracing::warn!("connection lost, reconnecting"); + } + Err(e) => { + tracing::error!(error = %e, "connection failed"); + } + } + + let delay = backoff_delay(attempt); + attempt = attempt.saturating_add(1); + + tracing::info!(delay_secs = delay.as_secs_f64(), "reconnecting"); + + tokio::select! { + _ = tokio::time::sleep(delay) => {}, + _ = shutdown.cancelled() => break, + } + } + + tracing::info!("client shut down"); +} + +fn backoff_delay(attempt: u32) -> Duration { + let secs = 2u64.saturating_pow(attempt); + let delay = Duration::from_secs(secs).min(MAX_BACKOFF); + delay.max(INITIAL_BACKOFF) +} + +pub fn build_public_url(server_url: &str, tunnel_id: &TunnelId) -> Option { + let url = Url::parse(server_url).ok()?; + let host = url.host_str()?; + let scheme = if url.scheme() == "https" { "https" } else { "http" }; + + match url.port() { + Some(port) => Some(format!("{scheme}://{tunnel_id}.{host}:{port}")), + None => Some(format!("{scheme}://{tunnel_id}.{host}")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn backoff_increases_exponentially() { + assert_eq!(backoff_delay(0), Duration::from_secs(1)); + assert_eq!(backoff_delay(1), Duration::from_secs(2)); + assert_eq!(backoff_delay(2), Duration::from_secs(4)); + assert_eq!(backoff_delay(3), Duration::from_secs(8)); + } + + #[test] + fn backoff_caps_at_max() { + assert_eq!(backoff_delay(10), MAX_BACKOFF); + assert_eq!(backoff_delay(20), MAX_BACKOFF); + } + + #[test] + fn public_url_http() { + let id = TunnelId::new("my-tunnel").unwrap(); + let url = build_public_url("http://tunnel.example.com", &id); + assert_eq!(url.as_deref(), Some("http://my-tunnel.tunnel.example.com")); + } + + #[test] + fn public_url_https() { + let id = TunnelId::new("my-tunnel").unwrap(); + let url = build_public_url("https://tunnel.example.com", &id); + assert_eq!(url.as_deref(), Some("https://my-tunnel.tunnel.example.com")); + } + + #[test] + fn public_url_with_port() { + let id = TunnelId::new("abc").unwrap(); + let url = build_public_url("http://localhost:8080", &id); + assert_eq!(url.as_deref(), Some("http://abc.localhost:8080")); + } +} diff --git a/crates/funnel-client/src/tunnel.rs b/crates/funnel-client/src/tunnel.rs new file mode 100644 index 0000000..d805151 --- /dev/null +++ b/crates/funnel-client/src/tunnel.rs @@ -0,0 +1,305 @@ +use std::sync::Arc; +use std::time::Duration; + +use futures::{SinkExt, StreamExt}; +use tokio::sync::mpsc; +use tokio_tungstenite::tungstenite::Message; +use tokio_util::sync::CancellationToken; +use url::Url; + +use funnel_core::protocol::TunnelMessage; +use funnel_core::tunnel::TunnelId; + +use crate::forwarder::Forwarder; + +const CHANNEL_BUFFER: usize = 128; +const PING_INTERVAL: Duration = Duration::from_secs(30); +const PONG_TIMEOUT: Duration = Duration::from_secs(90); +const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(15); +const MAX_CONCURRENT_REQUESTS: usize = 128; + +pub struct TunnelClient { + pub tunnel_id: TunnelId, + server_url: String, + local_addr: String, + token: Option, +} + +impl TunnelClient { + pub fn new( + tunnel_id: TunnelId, + server_url: String, + local_addr: String, + token: Option, + ) -> Self { + Self { + tunnel_id, + server_url, + local_addr, + token, + } + } + + /// connect to the server and run the tunnel until it disconnects. + pub async fn run(&self, cancel: CancellationToken) -> anyhow::Result<()> { + let ws = self.connect().await?; + let (ws_sink, ws_stream) = ws.split(); + + let (outgoing_tx, outgoing_rx) = mpsc::channel::(CHANNEL_BUFFER); + let forwarder = Arc::new(Forwarder::new(self.local_addr.clone())); + let semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_REQUESTS)); + + let read_cancel = cancel.clone(); + let read_tx = outgoing_tx.clone(); + let read_forwarder = Arc::clone(&forwarder); + let read_semaphore = Arc::clone(&semaphore); + let read_tunnel_id = self.tunnel_id.clone(); + let read_handle = tokio::spawn(async move { + read_loop( + ws_stream, + read_tx, + read_forwarder, + read_semaphore, + read_tunnel_id, + read_cancel, + ) + .await; + }); + + let write_cancel = cancel.clone(); + let write_handle = tokio::spawn(async move { + write_loop(ws_sink, outgoing_rx, write_cancel).await; + }); + + let ping_cancel = cancel.clone(); + let ping_tx = outgoing_tx.clone(); + let ping_handle = tokio::spawn(async move { + heartbeat_loop(ping_tx, ping_cancel).await; + }); + + tokio::select! { + _ = read_handle => {}, + _ = write_handle => {}, + _ = ping_handle => {}, + _ = cancel.cancelled() => {}, + } + + Ok(()) + } + + async fn connect( + &self, + ) -> anyhow::Result< + tokio_tungstenite::WebSocketStream< + tokio_tungstenite::MaybeTlsStream, + >, + > { + let mut url = Url::parse(&self.server_url)?; + + match url.scheme() { + "https" => url.set_scheme("wss").unwrap(), + _ => url.set_scheme("ws").unwrap(), + }; + + url.set_path("/ws"); + url.query_pairs_mut() + .append_pair("id", self.tunnel_id.as_ref()); + + let mut request = tokio_tungstenite::tungstenite::http::Request::builder() + .uri(url.as_str()) + .body(())?; + + if let Some(token) = &self.token { + request.headers_mut().insert( + "Authorization", + format!("Bearer {token}").parse()?, + ); + } + + tracing::debug!(url = %url, "connecting to server"); + + let (ws, _response) = + tokio::time::timeout(HANDSHAKE_TIMEOUT, tokio_tungstenite::connect_async(request)) + .await + .map_err(|_| anyhow::anyhow!("connection timed out"))??; + + tracing::info!(tunnel_id = %self.tunnel_id, "websocket connected"); + Ok(ws) + } +} + +async fn read_loop( + mut stream: futures::stream::SplitStream< + tokio_tungstenite::WebSocketStream< + tokio_tungstenite::MaybeTlsStream, + >, + >, + outgoing_tx: mpsc::Sender, + forwarder: Arc, + semaphore: Arc, + tunnel_id: TunnelId, + cancel: CancellationToken, +) { + loop { + let msg = tokio::select! { + msg = stream.next() => msg, + _ = cancel.cancelled() => break, + }; + + let Some(result) = msg else { break }; + + let frame = match result { + Ok(f) => f, + Err(e) => { + tracing::debug!(error = %e, "websocket read error"); + break; + } + }; + + match frame { + Message::Text(text) => { + handle_text_message( + &text, + &outgoing_tx, + &forwarder, + &semaphore, + &tunnel_id, + &cancel, + ); + } + Message::Close(_) => break, + Message::Ping(_) => { + // tungstenite handles pong automatically + } + _ => {} + } + } + + tracing::debug!("read loop exiting"); + cancel.cancel(); +} + +fn handle_text_message( + text: &str, + outgoing_tx: &mpsc::Sender, + forwarder: &Arc, + semaphore: &Arc, + tunnel_id: &TunnelId, + cancel: &CancellationToken, +) { + let msg: TunnelMessage = match serde_json::from_str(text) { + Ok(m) => m, + Err(e) => { + tracing::warn!(error = %e, "malformed message from server"); + return; + } + }; + + match msg { + TunnelMessage::Request { + request_id, + payload, + .. + } => { + tracing::debug!( + request_id = %request_id, + method = %payload.method, + path = %payload.path, + "received request" + ); + + let tx = outgoing_tx.clone(); + let fwd = Arc::clone(forwarder); + let sem = Arc::clone(semaphore); + let cancel = cancel.clone(); + let tid = tunnel_id.clone(); + + tokio::spawn(async move { + let _permit = tokio::select! { + permit = sem.acquire() => match permit { + Ok(p) => p, + Err(_) => return, + }, + _ = cancel.cancelled() => return, + }; + + let response = fwd.forward(payload).await; + + let msg = TunnelMessage::Response { + request_id, + payload: response, + }; + + if tx.send(msg).await.is_err() { + tracing::debug!(tunnel_id = %tid, "outgoing channel closed"); + } + }); + } + TunnelMessage::RequestCancel { request_id } => { + tracing::debug!(request_id = %request_id, "request cancelled by server"); + // cancellation of individual in flight requests would require + // tracking cancel tokens per request. for now we just log it; + // the forwarded request will complete and its response will be + // silently dropped by the server since the pending entry is gone. + } + TunnelMessage::Pong => { + tracing::debug!("pong received"); + } + other => { + tracing::debug!(msg_type = ?other, "unhandled message type"); + } + } +} + +async fn write_loop( + mut sink: futures::stream::SplitSink< + tokio_tungstenite::WebSocketStream< + tokio_tungstenite::MaybeTlsStream, + >, + Message, + >, + mut rx: mpsc::Receiver, + cancel: CancellationToken, +) { + loop { + let msg = tokio::select! { + msg = rx.recv() => msg, + _ = cancel.cancelled() => break, + }; + + let Some(msg) = msg else { break }; + + let json = match serde_json::to_string(&msg) { + Ok(j) => j, + Err(e) => { + tracing::error!(error = %e, "failed to serialize message"); + continue; + } + }; + + if sink.send(Message::text(json)).await.is_err() { + break; + } + } + + tracing::debug!("write loop exiting"); + cancel.cancel(); +} + +async fn heartbeat_loop(tx: mpsc::Sender, cancel: CancellationToken) { + let mut interval = tokio::time::interval(PING_INTERVAL); + interval.tick().await; // skip first immediate tick + + loop { + tokio::select! { + _ = interval.tick() => {}, + _ = cancel.cancelled() => break, + } + + if tx.send(TunnelMessage::Ping).await.is_err() { + break; + } + } + + tracing::debug!("heartbeat loop exiting"); +} From 11c65442de80563d15ea7c9c4deee7816385d76d Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 5 Apr 2026 14:30:00 +0200 Subject: [PATCH 12/18] fix: resolve build errors in server and client --- crates/funnel-client/src/config.rs | 2 +- crates/funnel-client/src/tunnel.rs | 1 - crates/funnel-server/Cargo.toml | 2 +- .../funnel-server/src/db/tunnel_sessions.rs | 18 +++++---- crates/funnel-server/src/proxy/headers.rs | 3 +- crates/funnel-server/src/proxy/router.rs | 40 +++++++++++-------- 6 files changed, 38 insertions(+), 28 deletions(-) diff --git a/crates/funnel-client/src/config.rs b/crates/funnel-client/src/config.rs index f0c9ac5..c79cd74 100644 --- a/crates/funnel-client/src/config.rs +++ b/crates/funnel-client/src/config.rs @@ -57,7 +57,7 @@ pub fn save(config: &Config) -> anyhow::Result<()> { Ok(()) } -pub fn get_inlet(config: &Config, name: &str) -> Option<&Inlet> { +pub fn get_inlet<'a>(config: &'a Config, name: &str) -> Option<&'a Inlet> { config.inlets.get(name) } diff --git a/crates/funnel-client/src/tunnel.rs b/crates/funnel-client/src/tunnel.rs index d805151..2e24ceb 100644 --- a/crates/funnel-client/src/tunnel.rs +++ b/crates/funnel-client/src/tunnel.rs @@ -14,7 +14,6 @@ use crate::forwarder::Forwarder; const CHANNEL_BUFFER: usize = 128; const PING_INTERVAL: Duration = Duration::from_secs(30); -const PONG_TIMEOUT: Duration = Duration::from_secs(90); const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(15); const MAX_CONCURRENT_REQUESTS: usize = 128; diff --git a/crates/funnel-server/Cargo.toml b/crates/funnel-server/Cargo.toml index 4ff9699..1a0c71f 100644 --- a/crates/funnel-server/Cargo.toml +++ b/crates/funnel-server/Cargo.toml @@ -10,7 +10,7 @@ anyhow = "1" argon2 = "0.5" axum = { version = "0.8", features = ["ws"] } chrono = { version = "0.4", features = ["serde"] } -clap = { version = "4", features = ["derive"] } +clap = { version = "4", features = ["derive", "env"] } dashmap = "6" futures = "0.3" jsonwebtoken = "9" diff --git a/crates/funnel-server/src/db/tunnel_sessions.rs b/crates/funnel-server/src/db/tunnel_sessions.rs index 1be0e17..443575e 100644 --- a/crates/funnel-server/src/db/tunnel_sessions.rs +++ b/crates/funnel-server/src/db/tunnel_sessions.rs @@ -1,5 +1,3 @@ -use std::net::IpAddr; - use chrono::{DateTime, Utc}; use serde::Serialize; use sqlx::PgPool; @@ -10,7 +8,7 @@ pub struct TunnelSession { pub id: Uuid, pub user_id: Uuid, pub tunnel_id: String, - pub client_ip: Option, + pub client_ip: Option, pub connected_at: DateTime, pub disconnected_at: Option>, pub bytes_in: i64, @@ -22,13 +20,13 @@ pub async fn create( pool: &PgPool, user_id: Uuid, tunnel_id: &str, - client_ip: Option, + client_ip: Option<&str>, ) -> Result { sqlx::query_as::<_, TunnelSession>( r#" INSERT INTO tunnel_sessions (user_id, tunnel_id, client_ip) - VALUES ($1, $2, $3) - RETURNING * + VALUES ($1, $2, $3::inet) + RETURNING id, user_id, tunnel_id, client_ip::text, connected_at, disconnected_at, bytes_in, bytes_out, requests "#, ) .bind(user_id) @@ -69,7 +67,8 @@ pub async fn list_for_user( ) -> Result, sqlx::Error> { sqlx::query_as::<_, TunnelSession>( r#" - SELECT * FROM tunnel_sessions + SELECT id, user_id, tunnel_id, client_ip::text, connected_at, disconnected_at, bytes_in, bytes_out, requests + FROM tunnel_sessions WHERE user_id = $1 ORDER BY connected_at DESC LIMIT $2 @@ -83,7 +82,10 @@ pub async fn list_for_user( pub async fn list_active(pool: &PgPool) -> Result, sqlx::Error> { sqlx::query_as::<_, TunnelSession>( - "SELECT * FROM tunnel_sessions WHERE disconnected_at IS NULL ORDER BY connected_at DESC", + r#" + SELECT id, user_id, tunnel_id, client_ip::text, connected_at, disconnected_at, bytes_in, bytes_out, requests + FROM tunnel_sessions WHERE disconnected_at IS NULL ORDER BY connected_at DESC + "#, ) .fetch_all(pool) .await diff --git a/crates/funnel-server/src/proxy/headers.rs b/crates/funnel-server/src/proxy/headers.rs index b171f59..5c54f9d 100644 --- a/crates/funnel-server/src/proxy/headers.rs +++ b/crates/funnel-server/src/proxy/headers.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::net::SocketAddr; -use axum::http::{HeaderMap, HeaderValue}; +use axum::http::HeaderMap; /// copy request headers and add standard proxy headers (X-Forwarded-For, etc). pub fn prepare_forwarding_headers( @@ -45,6 +45,7 @@ pub fn prepare_forwarding_headers( #[cfg(test)] mod tests { use super::*; + use axum::http::HeaderValue; use std::net::{IpAddr, Ipv4Addr}; fn test_addr() -> SocketAddr { diff --git a/crates/funnel-server/src/proxy/router.rs b/crates/funnel-server/src/proxy/router.rs index 3320b3a..e970358 100644 --- a/crates/funnel-server/src/proxy/router.rs +++ b/crates/funnel-server/src/proxy/router.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use axum::body::Body; -use axum::extract::{ConnectInfo, Host, State}; +use axum::extract::State; use axum::http::{Request, Response, StatusCode}; use funnel_core::protocol::RequestPayload; @@ -16,16 +16,20 @@ const MAX_BODY_SIZE: usize = 10 * 1024 * 1024; // 10 mb /// requests to `{tunnel_id}.{base_domain}` are forwarded through the matching tunnel. pub async fn handle_tunnel_request( State(state): State>, - Host(host): Host, - ConnectInfo(remote_addr): ConnectInfo, request: Request, ) -> Response { - let subdomain = match extract_subdomain(&host) { - Some(s) => s, + let host = request + .headers() + .get("host") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + let subdomain = match extract_subdomain(host) { + Some(s) => s.to_string(), None => return not_found("tunnel not found"), }; - let tunnel_id = match TunnelId::new(subdomain) { + let tunnel_id = match TunnelId::new(&subdomain) { Ok(id) => id, Err(_) => return not_found("tunnel not found"), }; @@ -37,9 +41,21 @@ pub async fn handle_tunnel_request( let method = request.method().to_string(); let path = request.uri().to_string(); - let is_tls = false; // will be determined by tls termination layer later - let headers = prepare_forwarding_headers(request.headers(), &host, remote_addr, is_tls); + let remote_addr = request + .extensions() + .get::>() + .map(|ci| ci.0); + + let headers = match remote_addr { + Some(addr) => prepare_forwarding_headers(request.headers(), host, addr, false), + None => prepare_forwarding_headers( + request.headers(), + host, + std::net::SocketAddr::from(([0, 0, 0, 0], 0)), + false, + ), + }; let body_bytes = match axum::body::to_bytes(request.into_body(), MAX_BODY_SIZE).await { Ok(b) => b.to_vec(), @@ -64,12 +80,7 @@ pub async fn handle_tunnel_request( } } -/// extract the first subdomain label from a host string. -/// `"my-tunnel.example.com"` -> `Some("my-tunnel")` -/// `"example.com"` -> `None` (no subdomain) -/// `"localhost:8080"` -> `None` fn extract_subdomain(host: &str) -> Option<&str> { - // strip port if present let host = host.split(':').next().unwrap_or(host); let dot = host.find('.')?; @@ -80,8 +91,6 @@ fn extract_subdomain(host: &str) -> Option<&str> { let subdomain = &host[..dot]; let rest = &host[dot + 1..]; - // need at least one more dot for it to be a real subdomain - // (subdomain.domain.tld), not just (domain.tld) if rest.contains('.') { Some(subdomain) } else { @@ -152,7 +161,6 @@ mod tests { #[test] fn extract_subdomain_deep_nesting() { - // only extracts the first label assert_eq!(extract_subdomain("a.b.example.com"), Some("a")); } } From d57361e88a594ffe41323fa03669d847eeb8f01e Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Mon, 6 Apr 2026 10:15:00 +0200 Subject: [PATCH 13/18] fix: use ipnetwork for inet columns and suppress dead code warnings --- crates/funnel-server/Cargo.toml | 3 ++- crates/funnel-server/src/db/mod.rs | 5 +++- .../funnel-server/src/db/tunnel_sessions.rs | 23 ++++++++++--------- crates/funnel-server/src/error.rs | 1 + crates/funnel-server/src/tunnel/connection.rs | 1 + crates/funnel-server/src/tunnel/stats.rs | 1 + 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/funnel-server/Cargo.toml b/crates/funnel-server/Cargo.toml index 1a0c71f..066fdb3 100644 --- a/crates/funnel-server/Cargo.toml +++ b/crates/funnel-server/Cargo.toml @@ -16,7 +16,8 @@ futures = "0.3" jsonwebtoken = "9" serde = { version = "1", features = ["derive"] } serde_json = "1" -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "migrate"] } +ipnetwork = "0.20" +sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "migrate", "ipnetwork"] } thiserror = "2" tokio = { version = "1", features = ["full"] } tokio-util = "0.7" diff --git a/crates/funnel-server/src/db/mod.rs b/crates/funnel-server/src/db/mod.rs index 21ec42e..6847d25 100644 --- a/crates/funnel-server/src/db/mod.rs +++ b/crates/funnel-server/src/db/mod.rs @@ -1,3 +1,6 @@ -pub mod users; +#[allow(dead_code)] pub mod api_keys; +#[allow(dead_code)] pub mod tunnel_sessions; +#[allow(dead_code)] +pub mod users; diff --git a/crates/funnel-server/src/db/tunnel_sessions.rs b/crates/funnel-server/src/db/tunnel_sessions.rs index 443575e..a710b03 100644 --- a/crates/funnel-server/src/db/tunnel_sessions.rs +++ b/crates/funnel-server/src/db/tunnel_sessions.rs @@ -1,4 +1,5 @@ use chrono::{DateTime, Utc}; +use ipnetwork::IpNetwork; use serde::Serialize; use sqlx::PgPool; use uuid::Uuid; @@ -8,7 +9,7 @@ pub struct TunnelSession { pub id: Uuid, pub user_id: Uuid, pub tunnel_id: String, - pub client_ip: Option, + pub client_ip: Option, pub connected_at: DateTime, pub disconnected_at: Option>, pub bytes_in: i64, @@ -20,13 +21,13 @@ pub async fn create( pool: &PgPool, user_id: Uuid, tunnel_id: &str, - client_ip: Option<&str>, + client_ip: Option, ) -> Result { sqlx::query_as::<_, TunnelSession>( r#" INSERT INTO tunnel_sessions (user_id, tunnel_id, client_ip) - VALUES ($1, $2, $3::inet) - RETURNING id, user_id, tunnel_id, client_ip::text, connected_at, disconnected_at, bytes_in, bytes_out, requests + VALUES ($1, $2, $3) + RETURNING * "#, ) .bind(user_id) @@ -67,8 +68,7 @@ pub async fn list_for_user( ) -> Result, sqlx::Error> { sqlx::query_as::<_, TunnelSession>( r#" - SELECT id, user_id, tunnel_id, client_ip::text, connected_at, disconnected_at, bytes_in, bytes_out, requests - FROM tunnel_sessions + SELECT * FROM tunnel_sessions WHERE user_id = $1 ORDER BY connected_at DESC LIMIT $2 @@ -82,10 +82,7 @@ pub async fn list_for_user( pub async fn list_active(pool: &PgPool) -> Result, sqlx::Error> { sqlx::query_as::<_, TunnelSession>( - r#" - SELECT id, user_id, tunnel_id, client_ip::text, connected_at, disconnected_at, bytes_in, bytes_out, requests - FROM tunnel_sessions WHERE disconnected_at IS NULL ORDER BY connected_at DESC - "#, + "SELECT * FROM tunnel_sessions WHERE disconnected_at IS NULL ORDER BY connected_at DESC", ) .fetch_all(pool) .await @@ -119,11 +116,15 @@ mod tests { .unwrap(); let user = setup_user(&pool).await; - let session = create(&pool, user.id, "test-tunnel", None).await.unwrap(); + let ip: IpNetwork = "192.168.1.1".parse::().unwrap().into(); + let session = create(&pool, user.id, "test-tunnel", Some(ip)) + .await + .unwrap(); assert_eq!(session.tunnel_id, "test-tunnel"); assert!(session.disconnected_at.is_none()); assert_eq!(session.bytes_in, 0); + assert!(session.client_ip.is_some()); let disconnected = disconnect(&pool, session.id, 1024, 2048, 10) .await diff --git a/crates/funnel-server/src/error.rs b/crates/funnel-server/src/error.rs index 74c9906..88a056f 100644 --- a/crates/funnel-server/src/error.rs +++ b/crates/funnel-server/src/error.rs @@ -10,6 +10,7 @@ struct ApiErrorBody { } #[derive(Debug, thiserror::Error)] +#[allow(dead_code)] pub enum AppError { #[error("tunnel not found: {0}")] TunnelNotFound(String), diff --git a/crates/funnel-server/src/tunnel/connection.rs b/crates/funnel-server/src/tunnel/connection.rs index 0a05a63..c9f4f15 100644 --- a/crates/funnel-server/src/tunnel/connection.rs +++ b/crates/funnel-server/src/tunnel/connection.rs @@ -38,6 +38,7 @@ impl ActiveTunnel { self.connected_at } + #[allow(dead_code)] pub fn is_alive(&self) -> bool { !self.cancel.is_cancelled() } diff --git a/crates/funnel-server/src/tunnel/stats.rs b/crates/funnel-server/src/tunnel/stats.rs index d90738f..b9a26a7 100644 --- a/crates/funnel-server/src/tunnel/stats.rs +++ b/crates/funnel-server/src/tunnel/stats.rs @@ -28,6 +28,7 @@ impl TunnelStats { self.bytes_in.fetch_add(n, Ordering::Relaxed); } + #[allow(dead_code)] pub fn add_bytes_out(&self, n: u64) { self.bytes_out.fetch_add(n, Ordering::Relaxed); } From fc8e6992d4669a05ae880bdeb5849a0c076d5cd0 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sat, 2 May 2026 12:00:00 +0200 Subject: [PATCH 14/18] chore: remove go source code in favor of rust rewrite --- client/client.go | 74 -- client/client_test.go | 377 ------- client/config.go | 214 ---- client/connection.go | 87 -- client/go.mod | 18 - client/go.sum | 20 - client/request_handler.go | 364 ------- client/request_handler_header_test.go | 294 ----- client/runner.go | 121 --- cmd/funnel/go.mod | 28 - cmd/funnel/go.sum | 30 - cmd/funnel/main.go | 253 ----- cmd/server/go.mod | 225 ---- cmd/server/go.sum | 1430 ------------------------- cmd/server/main.go | 310 ------ server/api.go | 700 ------------ server/auth.go | 240 ----- server/auth_test.go | 288 ----- server/certs.go | 569 ---------- server/certs_test.go | 176 --- server/docs/docs.go | 972 ----------------- server/docs/swagger.json | 952 ---------------- server/docs/swagger.yaml | 658 ------------ server/go.mod | 214 ---- server/go.sum | 1424 ------------------------ server/router.go | 550 ---------- server/router_bench_test.go | 96 -- server/router_header_test.go | 266 ----- server/server.go | 76 -- server/statistics.go | 242 ----- server/swagger.go | 30 - server/tunnel.go | 411 ------- server/tunnel_test.go | 239 ----- server/websocket.go | 137 --- server/websocket_test.go | 214 ---- shared/go.mod | 11 - shared/go.sum | 15 - shared/logging.go | 81 -- shared/message.go | 12 - shared/message_test.go | 383 ------- shared/utils.go | 188 ---- shared/utils_test.go | 540 ---------- version/go.mod | 3 - version/version.go | 57 - 44 files changed, 13589 deletions(-) delete mode 100644 client/client.go delete mode 100644 client/client_test.go delete mode 100644 client/config.go delete mode 100644 client/connection.go delete mode 100644 client/go.mod delete mode 100644 client/go.sum delete mode 100644 client/request_handler.go delete mode 100644 client/request_handler_header_test.go delete mode 100644 client/runner.go delete mode 100644 cmd/funnel/go.mod delete mode 100644 cmd/funnel/go.sum delete mode 100644 cmd/funnel/main.go delete mode 100644 cmd/server/go.mod delete mode 100644 cmd/server/go.sum delete mode 100644 cmd/server/main.go delete mode 100644 server/api.go delete mode 100644 server/auth.go delete mode 100644 server/auth_test.go delete mode 100644 server/certs.go delete mode 100644 server/certs_test.go delete mode 100644 server/docs/docs.go delete mode 100644 server/docs/swagger.json delete mode 100644 server/docs/swagger.yaml delete mode 100644 server/go.mod delete mode 100644 server/go.sum delete mode 100644 server/router.go delete mode 100644 server/router_bench_test.go delete mode 100644 server/router_header_test.go delete mode 100644 server/server.go delete mode 100644 server/statistics.go delete mode 100644 server/swagger.go delete mode 100644 server/tunnel.go delete mode 100644 server/tunnel_test.go delete mode 100644 server/websocket.go delete mode 100644 server/websocket_test.go delete mode 100644 shared/go.mod delete mode 100644 shared/go.sum delete mode 100644 shared/logging.go delete mode 100644 shared/message.go delete mode 100644 shared/message_test.go delete mode 100644 shared/utils.go delete mode 100644 shared/utils_test.go delete mode 100644 version/go.mod delete mode 100644 version/version.go diff --git a/client/client.go b/client/client.go deleted file mode 100644 index 54b464c..0000000 --- a/client/client.go +++ /dev/null @@ -1,74 +0,0 @@ -package client - -import ( - "context" - "sync" - "time" - - "github.com/gorilla/websocket" - "github.com/karol-broda/funnel/shared" -) - -type Client struct { - TunnelID string - ServerURL string - LocalAddr string - Token string - Conn *websocket.Conn - connMu sync.Mutex - lastPong time.Time - lastPongMu sync.Mutex - ongoingRequests map[string]context.CancelFunc - ongoingRequestsMu sync.Mutex - requestSemaphore chan struct{} - outgoingMessages chan *shared.Message - closeOnce sync.Once - requestWg sync.WaitGroup - ctx context.Context - cancel context.CancelFunc -} - -func New(tunnelID, serverURL, localAddr, token string) *Client { - ctx, cancel := context.WithCancel(context.Background()) - return &Client{ - TunnelID: tunnelID, - ServerURL: serverURL, - LocalAddr: localAddr, - Token: token, - ongoingRequests: make(map[string]context.CancelFunc), - requestSemaphore: make(chan struct{}, 128), - outgoingMessages: make(chan *shared.Message, 100), - lastPong: time.Now(), - ctx: ctx, - cancel: cancel, - } -} - -func (c *Client) Close() { - c.closeOnce.Do(func() { - logger := shared.GetTunnelLogger("client", c.TunnelID) - logger.Debug().Msg("closing client connection") - - c.cancel() - - if c.Conn != nil { - c.Conn.Close() - } - - if c.outgoingMessages != nil { - close(c.outgoingMessages) - } - }) -} - -func (c *Client) updateLastPong() { - c.lastPongMu.Lock() - defer c.lastPongMu.Unlock() - c.lastPong = time.Now() -} - -func (c *Client) getLastPong() time.Time { - c.lastPongMu.Lock() - defer c.lastPongMu.Unlock() - return c.lastPong -} diff --git a/client/client_test.go b/client/client_test.go deleted file mode 100644 index 7fea9fa..0000000 --- a/client/client_test.go +++ /dev/null @@ -1,377 +0,0 @@ -package client - -import ( - "context" - "net/http" - "net/url" - "strings" - "testing" - - "github.com/karol-broda/funnel/shared" -) - -func TestClientConnectionHandling(t *testing.T) { - tests := []struct { - name string - serverURL string - expectError bool - }{ - { - name: "valid http URL", - serverURL: "http://localhost:8080", - expectError: false, - }, - { - name: "valid https URL", - serverURL: "https://example.com", - expectError: false, - }, - { - name: "invalid URL", - serverURL: "invalid-url", - expectError: true, - }, - { - name: "empty URL", - serverURL: "", - expectError: true, - }, - { - name: "URL with port", - serverURL: "http://localhost:9000", - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := &Client{ - ServerURL: tt.serverURL, - LocalAddr: "localhost:3000", - TunnelID: "test-tunnel", - } - - err := client.connect(context.Background()) - - if tt.expectError { - if err == nil { - t.Error("expected error but got none") - } - return - } - - if err != nil { - if _, parseErr := url.Parse(tt.serverURL); parseErr != nil { - t.Errorf("URL parsing failed: %v", parseErr) - } - } - }) - } -} - -func TestClientURLConstruction(t *testing.T) { - tests := []struct { - name string - serverURL string - tunnelID string - expectedScheme string - expectedHost string - expectedQuery string - }{ - { - name: "http to ws", - serverURL: "http://localhost:8080", - tunnelID: "test123", - expectedScheme: "ws", - expectedHost: "localhost:8080", - expectedQuery: "id=test123", - }, - { - name: "https to wss", - serverURL: "https://tunnel.example.com", - tunnelID: "secure-tunnel", - expectedScheme: "wss", - expectedHost: "tunnel.example.com", - expectedQuery: "id=secure-tunnel", - }, - { - name: "http with path", - serverURL: "http://localhost:8080/api", - tunnelID: "path-test", - expectedScheme: "ws", - expectedHost: "localhost:8080", - expectedQuery: "id=path-test", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - u, err := url.Parse(tt.serverURL) - if err != nil { - t.Fatalf("failed to parse server URL: %v", err) - } - - wsScheme := "ws" - if u.Scheme == "https" { - wsScheme = "wss" - } - - if wsScheme != tt.expectedScheme { - t.Errorf("expected scheme %s, got %s", tt.expectedScheme, wsScheme) - } - - if u.Host != tt.expectedHost { - t.Errorf("expected host %s, got %s", tt.expectedHost, u.Host) - } - - expectedURL := wsScheme + "://" + u.Host + "/?id=" + tt.tunnelID - wsURL := expectedURL - - if !strings.Contains(wsURL, tt.expectedQuery) { - t.Errorf("expected query %s in URL %s", tt.expectedQuery, wsURL) - } - }) - } -} - -func TestClientHeaderFiltering(t *testing.T) { - client := &Client{ - TunnelID: "test-headers", - } - - tests := []struct { - name string - inputHeaders map[string][]string - expectedSkipped []string - expectedKept []string - }{ - { - name: "forward x-headers and proxy headers", - inputHeaders: map[string][]string{ - "X-Real-IP": {"192.168.1.1"}, - "X-Forwarded-For": {"10.0.0.1"}, - "Content-Type": {"application/json"}, - "Accept": {"*/*"}, - }, - expectedSkipped: []string{}, - expectedKept: []string{"X-Real-IP", "X-Forwarded-For", "Content-Type", "Accept"}, - }, - { - name: "skip connection-specific headers", - inputHeaders: map[string][]string{ - "Connection": {"keep-alive"}, - "Upgrade": {"websocket"}, - "User-Agent": {"test-client"}, - "Content-Type": {"application/json"}, - }, - expectedSkipped: []string{"Connection", "Upgrade"}, - expectedKept: []string{"User-Agent", "Content-Type"}, - }, - { - name: "keep standard headers and forward proxy headers", - inputHeaders: map[string][]string{ - "Content-Type": {"text/plain"}, - "Content-Length": {"42"}, - "Accept": {"text/html"}, - "User-Agent": {"test-browser"}, - "X-Forwarded-Host": {"example.com"}, - "X-Forwarded-Proto": {"https"}, - }, - expectedSkipped: []string{}, - expectedKept: []string{"Content-Type", "Content-Length", "Accept", "User-Agent", "X-Forwarded-Host", "X-Forwarded-Proto"}, - }, - { - name: "handle host header specially", - inputHeaders: map[string][]string{ - "Host": {"tunnel.example.com"}, - "Content-Type": {"application/json"}, - "Accept": {"*/*"}, - }, - expectedSkipped: []string{}, - expectedKept: []string{"Content-Type", "Accept"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest("GET", "http://localhost:3000/test", nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - - client.setRequestHeaders(req, tt.inputHeaders) - - for _, header := range tt.expectedSkipped { - if values := req.Header.Values(header); len(values) > 0 { - t.Errorf("header %s should be skipped but found values: %v", header, values) - } - } - - for _, header := range tt.expectedKept { - if values := req.Header.Values(header); len(values) == 0 { - t.Errorf("header %s should be kept but not found", header) - } - } - - if tt.inputHeaders["Host"] != nil { - if req.Host != client.LocalAddr { - t.Errorf("Host should be set to local address %s, but got %s", client.LocalAddr, req.Host) - } - } - }) - } -} - -func TestClientErrorHandling(t *testing.T) { - tests := []struct { - name string - requestID string - expectedStatus int - expectedBody string - }{ - { - name: "internal server error", - requestID: "test-500", - expectedStatus: 500, - expectedBody: "Internal Server Error", - }, - { - name: "bad gateway", - requestID: "test-502", - expectedStatus: 502, - expectedBody: "Bad Gateway", - }, - { - name: "service unavailable", - requestID: "test-503", - expectedStatus: 503, - expectedBody: "Service Unavailable", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - response := shared.Message{ - Type: "response", - RequestID: tt.requestID, - Status: tt.expectedStatus, - Headers: map[string][]string{ - "Content-Type": {"text/plain"}, - }, - Body: []byte(http.StatusText(tt.expectedStatus)), - } - - if response.Type != "response" { - t.Errorf("expected response type, got %s", response.Type) - } - - if response.RequestID != tt.requestID { - t.Errorf("expected request ID %s, got %s", tt.requestID, response.RequestID) - } - - if response.Status != tt.expectedStatus { - t.Errorf("expected status %d, got %d", tt.expectedStatus, response.Status) - } - - if string(response.Body) != tt.expectedBody { - t.Errorf("expected body %s, got %s", tt.expectedBody, string(response.Body)) - } - }) - } -} - -func TestClientConfiguration(t *testing.T) { - tests := []struct { - name string - serverURL string - localAddr string - tunnelID string - valid bool - }{ - { - name: "valid configuration", - serverURL: "http://localhost:8080", - localAddr: "localhost:3000", - tunnelID: "test-tunnel", - valid: true, - }, - { - name: "empty server URL", - serverURL: "", - localAddr: "localhost:3000", - tunnelID: "test-tunnel", - valid: false, - }, - { - name: "empty local address", - serverURL: "http://localhost:8080", - localAddr: "", - tunnelID: "test-tunnel", - valid: false, - }, - { - name: "empty tunnel ID", - serverURL: "http://localhost:8080", - localAddr: "localhost:3000", - tunnelID: "", - valid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := &Client{ - ServerURL: tt.serverURL, - LocalAddr: tt.localAddr, - TunnelID: tt.tunnelID, - } - - // validate configuration - valid := client.ServerURL != "" && client.LocalAddr != "" && client.TunnelID != "" - - if valid != tt.valid { - t.Errorf("expected valid=%v, got valid=%v", tt.valid, valid) - } - }) - } -} - -func BenchmarkClientHeaderFiltering(b *testing.B) { - client := &Client{TunnelID: "bench-test"} - - headers := map[string][]string{ - "X-Real-IP": {"192.168.1.1"}, - "X-Forwarded-For": {"10.0.0.1"}, - "X-Forwarded-Proto": {"https"}, - "Host": {"example.com"}, - "Connection": {"keep-alive"}, - "Content-Type": {"application/json"}, - "Accept": {"application/json"}, - "User-Agent": {"test-client/1.0"}, - "Authorization": {"Bearer token123"}, - "Content-Length": {"42"}, - } - - b.ReportAllocs() - for i := 0; i < b.N; i++ { - req, _ := http.NewRequest("GET", "http://localhost:3000/test", nil) - client.setRequestHeaders(req, headers) - } -} - -func BenchmarkClientURLConstruction(b *testing.B) { - client := &Client{ - ServerURL: "http://localhost:8080", - TunnelID: "benchmark-tunnel", - } - - b.ReportAllocs() - for i := 0; i < b.N; i++ { - u, _ := url.Parse(client.ServerURL) - wsScheme := "ws" - if u.Scheme == "https" { - wsScheme = "wss" - } - _ = wsScheme + "://" + u.Host + "/?id=" + client.TunnelID - } -} diff --git a/client/config.go b/client/config.go deleted file mode 100644 index 1ea25a9..0000000 --- a/client/config.go +++ /dev/null @@ -1,214 +0,0 @@ -package client - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/BurntSushi/toml" -) - -type Config struct { - Inlets map[string]Inlet `toml:"inlets"` -} - -// Inlet represents a tunnel server configuration -type Inlet struct { - Server string `toml:"server"` - Domain string `toml:"domain,omitempty"` - Token string `toml:"token,omitempty"` -} - -// ConfigManager handles configuration loading and management -type ConfigManager struct { - configPath string - config *Config -} - -// NewConfigManager creates a new configuration manager -func NewConfigManager() *ConfigManager { - return &ConfigManager{} -} - -// getConfigPath determines the configuration file path -func (cm *ConfigManager) getConfigPath() string { - if cm.configPath != "" { - return cm.configPath - } - - // try XDG config directory first - if configDir := os.Getenv("XDG_CONFIG_HOME"); configDir != "" { - cm.configPath = filepath.Join(configDir, "funnel", "config.toml") - return cm.configPath - } - - // fallback to ~/.config/funnel/config.toml - if homeDir, err := os.UserHomeDir(); err == nil { - configDir := filepath.Join(homeDir, ".config", "funnel") - cm.configPath = filepath.Join(configDir, "config.toml") - return cm.configPath - } - - // if all else fails, use current directory - cm.configPath = "funnel.toml" - return cm.configPath -} - -// LoadConfig loads the configuration from file -func (cm *ConfigManager) LoadConfig() (*Config, error) { - configPath := cm.getConfigPath() - - // check if config file exists - if _, err := os.Stat(configPath); os.IsNotExist(err) { - return nil, fmt.Errorf("config file not found at %s", configPath) - } - - // read and parse existing config - data, err := os.ReadFile(configPath) - if err != nil { - return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err) - } - - config := &Config{ - Inlets: make(map[string]Inlet), - } - - if err := toml.Unmarshal(data, config); err != nil { - return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err) - } - - cm.config = config - return config, nil -} - -// SaveConfig saves the configuration to file -func (cm *ConfigManager) SaveConfig(config *Config) error { - configPath := cm.getConfigPath() - - // ensure the directory exists - configDir := filepath.Dir(configPath) - if err := os.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("failed to create config directory %s: %w", configDir, err) - } - - // marshal config to TOML - data, err := toml.Marshal(config) - if err != nil { - return fmt.Errorf("failed to marshal config to TOML: %w", err) - } - - // write to file with restricted permissions (contains token) - if err := os.WriteFile(configPath, data, 0600); err != nil { - return fmt.Errorf("failed to write config file %s: %w", configPath, err) - } - - cm.config = config - return nil -} - -// GetInlet retrieves a specific inlet configuration by name -func (cm *ConfigManager) GetInlet(name string) (*Inlet, error) { - if cm.config == nil { - if _, err := cm.LoadConfig(); err != nil { - return nil, err - } - } - - inlet, exists := cm.config.Inlets[name] - if !exists { - return nil, fmt.Errorf("inlet '%s' not found in configuration", name) - } - - if inlet.Server == "" { - return nil, fmt.Errorf("inlet '%s' has no server configured", name) - } - - return &inlet, nil -} - -// GetDefaultInlet retrieves the default inlet configuration -func (cm *ConfigManager) GetDefaultInlet() (*Inlet, error) { - return cm.GetInlet("default") -} - -// ListInlets returns all available inlet names -func (cm *ConfigManager) ListInlets() ([]string, error) { - if cm.config == nil { - if _, err := cm.LoadConfig(); err != nil { - return nil, err - } - } - - inlets := make([]string, 0, len(cm.config.Inlets)) - for name := range cm.config.Inlets { - inlets = append(inlets, name) - } - - return inlets, nil -} - -// ValidateConfig validates the loaded configuration -func (cm *ConfigManager) ValidateConfig() error { - if cm.config == nil { - return fmt.Errorf("no configuration loaded") - } - - if len(cm.config.Inlets) == 0 { - return fmt.Errorf("no inlets configured") - } - - for name, inlet := range cm.config.Inlets { - if inlet.Server == "" { - return fmt.Errorf("inlet '%s' has no server configured", name) - } - } - - return nil -} - -// SetToken sets the token for a specific inlet, creating the inlet if it doesn't exist -func (cm *ConfigManager) SetToken(inletName, token string) error { - if cm.config == nil { - cm.config = &Config{Inlets: make(map[string]Inlet)} - cm.LoadConfig() - if cm.config == nil { - cm.config = &Config{Inlets: make(map[string]Inlet)} - } - } - - inlet := cm.config.Inlets[inletName] - inlet.Token = token - cm.config.Inlets[inletName] = inlet - - return cm.SaveConfig(cm.config) -} - -// SetServer sets the server URL for a specific inlet, creating the inlet if it doesn't exist -func (cm *ConfigManager) SetServer(inletName, server string) error { - if cm.config == nil { - cm.config = &Config{Inlets: make(map[string]Inlet)} - cm.LoadConfig() - if cm.config == nil { - cm.config = &Config{Inlets: make(map[string]Inlet)} - } - } - - inlet := cm.config.Inlets[inletName] - inlet.Server = server - cm.config.Inlets[inletName] = inlet - - return cm.SaveConfig(cm.config) -} - -// GetConfigPath returns the path to the config file -func (cm *ConfigManager) GetConfigPath() string { - return cm.getConfigPath() -} - -// GetConfig returns the loaded config, loading it if necessary -func (cm *ConfigManager) GetConfig() *Config { - if cm.config == nil { - cm.LoadConfig() - } - return cm.config -} diff --git a/client/connection.go b/client/connection.go deleted file mode 100644 index a2ddb56..0000000 --- a/client/connection.go +++ /dev/null @@ -1,87 +0,0 @@ -package client - -import ( - "context" - "net/http" - "net/url" - "time" - - "github.com/karol-broda/funnel/shared" - - "github.com/gorilla/websocket" -) - -func (c *Client) connect(ctx context.Context) error { - logger := shared.GetTunnelLogger("client.connection", c.TunnelID) - logger.Debug().Msg("starting connection process") - - connectionStart := time.Now() - - u, err := url.Parse(c.ServerURL) - if err != nil { - logger.Error().Err(err).Msg("failed to parse server url") - return err - } - - wsScheme := "ws" - if u.Scheme == "https" { - wsScheme = "wss" - } - - u.Scheme = wsScheme - u.RawQuery = "id=" + c.TunnelID - wsURL := u.String() - - logger.Debug().Str("websocket_url", wsURL).Msg("constructed websocket URL") - - dialer := *websocket.DefaultDialer - dialer.HandshakeTimeout = 15 * time.Second - dialer.EnableCompression = true - dialer.ReadBufferSize = 65536 - dialer.WriteBufferSize = 65536 - - logger.Debug(). - Dur("handshake_timeout", dialer.HandshakeTimeout). - Int("read_buffer_size", dialer.ReadBufferSize). - Int("write_buffer_size", dialer.WriteBufferSize). - Bool("compression_enabled", dialer.EnableCompression). - Msg("websocket dialer configured") - - var headers http.Header - if c.Token != "" { - headers = http.Header{} - headers.Set("Authorization", "Bearer "+c.Token) - logger.Debug().Msg("authentication token configured") - } - - dialStart := time.Now() - conn, resp, err := dialer.DialContext(ctx, wsURL, headers) - dialDuration := time.Since(dialStart) - - if err != nil { - logger.Error().Err(err). - Str("websocket_url", wsURL). - Dur("dial_duration", dialDuration). - Msg("websocket connection failed") - if resp != nil { - logger.Error().Int("http_status", resp.StatusCode).Msg("websocket upgrade response status") - } - return err - } - - totalConnectTime := time.Since(connectionStart) - logger.Info(). - Dur("dial_time", dialDuration). - Dur("total_connect_time", totalConnectTime). - Msg("websocket connection established") - - logger.Debug(). - Int("http_status", resp.StatusCode). - Msg("websocket upgrade successful") - - c.Conn = conn - c.connMu.Lock() - c.lastPong = time.Now() - c.connMu.Unlock() - return nil -} diff --git a/client/go.mod b/client/go.mod deleted file mode 100644 index 698249f..0000000 --- a/client/go.mod +++ /dev/null @@ -1,18 +0,0 @@ -module github.com/karol-broda/funnel/client - -go 1.24.4 - -require ( - github.com/BurntSushi/toml v1.5.0 - github.com/gorilla/websocket v1.5.3 - github.com/karol-broda/funnel/shared v0.0.0-00010101000000-000000000000 -) - -require ( - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/rs/zerolog v1.34.0 // indirect - golang.org/x/sys v0.32.0 // indirect -) - -replace github.com/karol-broda/funnel/shared => ../shared diff --git a/client/go.sum b/client/go.sum deleted file mode 100644 index 772dea6..0000000 --- a/client/go.sum +++ /dev/null @@ -1,20 +0,0 @@ -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/client/request_handler.go b/client/request_handler.go deleted file mode 100644 index c1f1739..0000000 --- a/client/request_handler.go +++ /dev/null @@ -1,364 +0,0 @@ -package client - -import ( - "bytes" - "context" - "errors" - "io" - "net/http" - "strings" - "time" - - "github.com/gorilla/websocket" - "github.com/karol-broda/funnel/shared" -) - -func (c *Client) readPump() { - defer func() { - c.requestWg.Wait() - c.Close() - }() - logger := shared.GetTunnelLogger("client.handler", c.TunnelID) - - logger.Info().Msg("starting request handler loop") - - transport := &http.Transport{ - MaxIdleConns: 10, - MaxIdleConnsPerHost: 5, - IdleConnTimeout: 30 * time.Second, - MaxConnsPerHost: 10, - } - - httpClient := &http.Client{ - Timeout: 30 * time.Second, - Transport: transport, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } - - logger.Debug(). - Dur("http_timeout", httpClient.Timeout). - Int("max_conns_per_host", 10). - Int("max_idle_conns", 10). - Msg("http client configured") - - c.setupHeartbeat() - - requestCount := 0 - for { - select { - case <-c.ctx.Done(): - logger.Info().Msg("read pump shutting down due to context cancellation") - return - default: - var msg shared.Message - readStart := time.Now() - err := c.Conn.ReadJSON(&msg) - readDuration := time.Since(readStart) - - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { - logger.Error().Err(err).Dur("read_duration", readDuration).Msg("unexpected websocket read error") - } else { - logger.Info().Dur("read_duration", readDuration).Msg("websocket connection closed gracefully") - } - return - } - - logger.Debug().Dur("message_read_time", readDuration).Str("message_type", msg.Type).Msg("received message from server") - - switch msg.Type { - case "request": - requestCount++ - logger.Info(). - Int("request_count", requestCount). - Str("request_id", msg.RequestID). - Str("method", msg.Method). - Str("path", msg.Path). - Int("body_size", len(msg.Body)). - Msg("received request from server") - - c.requestWg.Add(1) - go func(m shared.Message) { - defer c.requestWg.Done() - c.processRequest(httpClient, m) - }(msg) - case "request_cancel": - logger.Info().Str("request_id", msg.RequestID).Msg("received request cancellation") - c.ongoingRequestsMu.Lock() - if cancel, ok := c.ongoingRequests[msg.RequestID]; ok { - cancel() - delete(c.ongoingRequests, msg.RequestID) - } - c.ongoingRequestsMu.Unlock() - default: - logger.Debug().Str("message_type", msg.Type).Msg("ignoring unhandled message type") - } - } - } -} - -func (c *Client) setupHeartbeat() { - logger := shared.GetTunnelLogger("client.handler", c.TunnelID) - - c.Conn.SetPongHandler(func(string) error { - c.updateLastPong() - logger.Debug().Msg("pong received from server") - return nil - }) - - go func() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - for { - select { - case <-c.ctx.Done(): - logger.Debug().Msg("context cancelled, stopping ping routine") - return - case <-ticker.C: - if c.Conn == nil { - logger.Debug().Msg("connection is nil, stopping ping routine") - return - } - - lastPong := c.getLastPong() - if time.Since(lastPong) > 90*time.Second { - logger.Warn(). - Dur("time_since_last_pong", time.Since(lastPong)). - Msg("no pong received recently, connection may be stale") - c.Close() - return - } - - select { - case c.outgoingMessages <- &shared.Message{Type: "ping"}: - logger.Debug().Msg("ping message queued") - case <-c.ctx.Done(): - logger.Debug().Msg("context cancelled while queuing ping, stopping ping routine") - return - default: - logger.Warn().Msg("outgoing message channel full, cannot queue ping") - } - } - } - }() - - logger.Debug().Msg("heartbeat mechanism setup completed") -} - -func (c *Client) writePump() { - defer func() { - c.Close() - }() - logger := shared.GetTunnelLogger("client.handler", c.TunnelID) - logger.Info().Msg("starting writer loop") - - for msg := range c.outgoingMessages { - if msg.Type == "ping" { - if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { - logger.Error().Err(err).Msg("failed to send ping") - return - } - logger.Debug().Msg("ping sent to server") - continue - } - - c.connMu.Lock() - err := c.Conn.WriteJSON(msg) - c.connMu.Unlock() - - if err != nil { - logger.Error().Err(err).Msg("failed to write json message") - return - } - - if msg.RequestID != "" { - logger.Debug(). - Str("request_id", msg.RequestID). - Str("type", msg.Type). - Msg("message sent to server") - } - } - logger.Info().Msg("writer loop finished") -} - -func (c *Client) processRequest(httpClient *http.Client, msg shared.Message) { - logger := shared.GetRequestLogger("client.handler", c.TunnelID, msg.RequestID) - - select { - case c.requestSemaphore <- struct{}{}: - defer func() { <-c.requestSemaphore }() - case <-c.ctx.Done(): - logger.Warn().Msg("shutting down, not processing new request") - return - } - - reqCtx, cancel := context.WithTimeout(c.ctx, 30*time.Second) - defer cancel() - - c.ongoingRequestsMu.Lock() - c.ongoingRequests[msg.RequestID] = cancel - c.ongoingRequestsMu.Unlock() - - defer func() { - c.ongoingRequestsMu.Lock() - delete(c.ongoingRequests, msg.RequestID) - c.ongoingRequestsMu.Unlock() - }() - - processStart := time.Now() - logger.Debug(). - Str("method", msg.Method). - Str("path", msg.Path). - Int("header_count", len(msg.Headers)). - Int("body_size", len(msg.Body)). - Int("concurrent_requests", len(c.requestSemaphore)). - Msg("processing request") - - req, err := http.NewRequestWithContext(reqCtx, msg.Method, "http://"+c.LocalAddr+msg.Path, bytes.NewReader(msg.Body)) - if err != nil { - logger.Error().Err(err).Msg("failed to create request") - if reqCtx.Err() == nil { - c.sendError(msg.RequestID, http.StatusInternalServerError, "failed to create request") - } - return - } - - c.setRequestHeaders(req, msg.Headers) - - logger.Debug(). - Int("concurrent_requests", len(c.requestSemaphore)). - Int("header_count", len(req.Header)). - Str("local_url", req.URL.String()). - Msg("forwarding request to local service") - - upstreamResponseTimeStart := time.Now() - resp, err := httpClient.Do(req) - - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - logger.Warn().Err(err).Msg("request to local service was canceled or timed out") - return - } - logger.Error().Err(err).Msg("failed to make request to local service") - c.sendError(msg.RequestID, http.StatusBadGateway, "local service connection failed") - return - } - defer resp.Body.Close() - - logger.Debug(). - Int("status_code", resp.StatusCode). - Dur("upstream_response_time", time.Since(upstreamResponseTimeStart)). - Int64("content_length", resp.ContentLength). - Msg("received response from local service") - - bodyReadStart := time.Now() - body, err := io.ReadAll(resp.Body) - bodyReadDuration := time.Since(bodyReadStart) - - if err != nil { - logger.Error().Err(err). - Dur("body_read_duration", bodyReadDuration). - Msg("failed to read response body") - c.sendError(msg.RequestID, http.StatusBadGateway, "failed to read response body") - return - } - - totalProcessTime := time.Since(processStart) - logger.Info(). - Int("status_code", resp.StatusCode). - Int("response_size", len(body)). - Dur("total_process_time", totalProcessTime). - Dur("upstream_time", time.Since(upstreamResponseTimeStart)). - Dur("body_read_time", bodyReadDuration). - Msg("request processed successfully") - - select { - case <-c.ctx.Done(): - logger.Info().Str("request_id", msg.RequestID).Msg("shutting down, not sending response") - return - default: - c.sendResponse(msg.RequestID, resp.StatusCode, resp.Header, body) - } -} - -func (c *Client) setRequestHeaders(req *http.Request, headers map[string][]string) { - req.Header = make(http.Header) - - for k, values := range headers { - if c.shouldSkipHeader(k) { - continue - } - if strings.ToLower(k) == "host" { - continue - } - for _, value := range values { - req.Header.Add(k, value) - } - } - req.Host = c.LocalAddr -} - -func (c *Client) shouldSkipHeader(headerName string) bool { - lower := strings.ToLower(headerName) - return lower == "connection" || - lower == "keep-alive" || - lower == "proxy-authenticate" || - lower == "proxy-authorization" || - lower == "te" || - lower == "trailer" || - lower == "transfer-encoding" || - lower == "upgrade" || - lower == "proxy-connection" -} - -func (c *Client) sendResponse(requestID string, status int, headers http.Header, body []byte) { - logger := shared.GetRequestLogger("client.handler", c.TunnelID, requestID) - - respHeaders := make(map[string][]string) - for k, v := range headers { - if !c.shouldSkipHeader(k) { - respHeaders[k] = v - } - } - - respMsg := &shared.Message{ - Type: "response", - RequestID: requestID, - Status: status, - Headers: respHeaders, - Body: body, - } - - select { - case c.outgoingMessages <- respMsg: - sendDuration := time.Since(time.Now()) - logger.Debug(). - Int("status_code", status). - Int("header_count", len(respHeaders)). - Int("response_size", len(body)). - Dur("send_duration", sendDuration). - Msg("response queued successfully") - default: - logger.Warn().Msg("outgoing message channel full, dropping response") - } -} - -func (c *Client) sendError(requestID string, statusCode int, error string) { - logger := shared.GetRequestLogger("client.handler", c.TunnelID, requestID) - logger.Error().Int("status_code", statusCode).Str("error", error).Msg("sending error response") - errMessage := &shared.Message{ - Type: "response", - RequestID: requestID, - Status: statusCode, - Headers: http.Header{"Content-Type": []string{"text/plain"}}, - Body: []byte(error), - } - select { - case c.outgoingMessages <- errMessage: - default: - logger.Warn().Int("status", statusCode).Msg("outgoing message channel full, dropping error response") - } -} diff --git a/client/request_handler_header_test.go b/client/request_handler_header_test.go deleted file mode 100644 index b58876d..0000000 --- a/client/request_handler_header_test.go +++ /dev/null @@ -1,294 +0,0 @@ -package client - -import ( - "net/http/httptest" - "testing" -) - -func TestClient_shouldSkipHeader(t *testing.T) { - client := &Client{ - TunnelID: "test-tunnel", - LocalAddr: "localhost:3000", - } - - tests := []struct { - name string - headerName string - shouldSkip bool - }{ - { - name: "skip Connection header", - headerName: "Connection", - shouldSkip: true, - }, - { - name: "skip connection header lowercase", - headerName: "connection", - shouldSkip: true, - }, - { - name: "skip Upgrade header", - headerName: "Upgrade", - shouldSkip: true, - }, - { - name: "skip upgrade header lowercase", - headerName: "upgrade", - shouldSkip: true, - }, - { - name: "skip Proxy-Connection header", - headerName: "Proxy-Connection", - shouldSkip: true, - }, - { - name: "skip Proxy-Authenticate header", - headerName: "Proxy-Authenticate", - shouldSkip: true, - }, - { - name: "skip Proxy-Authorization header", - headerName: "Proxy-Authorization", - shouldSkip: true, - }, - { - name: "skip TE header", - headerName: "TE", - shouldSkip: true, - }, - { - name: "skip Trailer header", - headerName: "Trailer", - shouldSkip: true, - }, - { - name: "skip Transfer-Encoding header", - headerName: "Transfer-Encoding", - shouldSkip: true, - }, - { - name: "forward User-Agent header", - headerName: "User-Agent", - shouldSkip: false, - }, - { - name: "forward X-Forwarded-For header", - headerName: "X-Forwarded-For", - shouldSkip: false, - }, - { - name: "forward X-Forwarded-Host header", - headerName: "X-Forwarded-Host", - shouldSkip: false, - }, - { - name: "forward X-Forwarded-Proto header", - headerName: "X-Forwarded-Proto", - shouldSkip: false, - }, - { - name: "forward X-Real-IP header", - headerName: "X-Real-IP", - shouldSkip: false, - }, - { - name: "forward Content-Type header", - headerName: "Content-Type", - shouldSkip: false, - }, - { - name: "forward Accept header", - headerName: "Accept", - shouldSkip: false, - }, - { - name: "forward Authorization header", - headerName: "Authorization", - shouldSkip: false, - }, - { - name: "forward custom header", - headerName: "X-Custom-Header", - shouldSkip: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := client.shouldSkipHeader(tt.headerName) - if result != tt.shouldSkip { - t.Errorf("shouldSkipHeader(%s) = %v, want %v", tt.headerName, result, tt.shouldSkip) - } - }) - } -} - -func TestClient_setRequestHeaders(t *testing.T) { - tests := []struct { - name string - inputHeaders map[string][]string - localAddr string - expectedSkipped []string - expectedForwarded []string - expectedHost string - }{ - { - name: "forward proxy headers", - inputHeaders: map[string][]string{ - "X-Forwarded-For": {"192.168.1.100"}, - "X-Forwarded-Host": {"api.example.com"}, - "X-Forwarded-Proto": {"https"}, - "X-Real-IP": {"203.0.113.45"}, - "User-Agent": {"TestClient/1.0"}, - "Accept": {"application/json"}, - }, - localAddr: "localhost:8080", - expectedSkipped: []string{}, - expectedForwarded: []string{"X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Proto", "X-Real-IP", "User-Agent", "Accept"}, - expectedHost: "localhost:8080", - }, - { - name: "skip connection headers", - inputHeaders: map[string][]string{ - "Connection": {"keep-alive"}, - "Upgrade": {"websocket"}, - "Transfer-Encoding": {"chunked"}, - "TE": {"trailers"}, - "User-Agent": {"TestClient/1.0"}, - "Content-Type": {"application/json"}, - }, - localAddr: "localhost:3000", - expectedSkipped: []string{"Connection", "Upgrade", "Transfer-Encoding", "TE"}, - expectedForwarded: []string{"User-Agent", "Content-Type"}, - expectedHost: "localhost:3000", - }, - { - name: "handle Host header specially", - inputHeaders: map[string][]string{ - "Host": {"tunnel.example.com"}, - "User-Agent": {"TestClient/1.0"}, - "Content-Type": {"application/json"}, - }, - localAddr: "localhost:9000", - expectedSkipped: []string{}, - expectedForwarded: []string{"User-Agent", "Content-Type"}, - expectedHost: "localhost:9000", - }, - { - name: "mixed headers with proxy and connection headers", - inputHeaders: map[string][]string{ - "X-Forwarded-For": {"192.168.1.100, 10.0.0.1"}, - "X-Forwarded-Host": {"api.example.com"}, - "Connection": {"close"}, - "Proxy-Connection": {"keep-alive"}, - "User-Agent": {"TestClient/1.0"}, - "Authorization": {"Bearer token123"}, - "Content-Type": {"application/json"}, - "X-Custom-Header": {"custom-value"}, - "Transfer-Encoding": {"gzip"}, - }, - localAddr: "localhost:5000", - expectedSkipped: []string{"Connection", "Proxy-Connection", "Transfer-Encoding"}, - expectedForwarded: []string{"X-Forwarded-For", "X-Forwarded-Host", "User-Agent", "Authorization", "Content-Type", "X-Custom-Header"}, - expectedHost: "localhost:5000", - }, - { - name: "no Host header provided", - inputHeaders: map[string][]string{ - "User-Agent": {"TestClient/1.0"}, - "Content-Type": {"application/json"}, - }, - localAddr: "localhost:4000", - expectedSkipped: []string{}, - expectedForwarded: []string{"User-Agent", "Content-Type"}, - expectedHost: "localhost:4000", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := &Client{ - TunnelID: "test-headers", - LocalAddr: tt.localAddr, - } - - req := httptest.NewRequest("GET", "http://"+tt.localAddr+"/test", nil) - - client.setRequestHeaders(req, tt.inputHeaders) - - for _, skippedHeader := range tt.expectedSkipped { - if values := req.Header.Values(skippedHeader); len(values) > 0 { - t.Errorf("Header %s should be skipped but found values: %v", skippedHeader, values) - } - } - - for _, forwardedHeader := range tt.expectedForwarded { - if values := req.Header.Values(forwardedHeader); len(values) == 0 { - t.Errorf("Header %s should be forwarded but not found", forwardedHeader) - } - } - - if req.Host != tt.expectedHost { - t.Errorf("Host header = %s, want %s", req.Host, tt.expectedHost) - } - - if tt.inputHeaders["X-Forwarded-For"] != nil { - if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != tt.inputHeaders["X-Forwarded-For"][0] { - t.Errorf("X-Forwarded-For = %s, want %s", forwardedFor, tt.inputHeaders["X-Forwarded-For"][0]) - } - } - }) - } -} - -func TestClient_setRequestHeaders_HeaderValues(t *testing.T) { - client := &Client{ - TunnelID: "test-values", - LocalAddr: "localhost:3000", - } - - tests := []struct { - name string - inputHeaders map[string][]string - headerToTest string - expectedValue string - }{ - { - name: "single value header", - inputHeaders: map[string][]string{ - "Authorization": {"Bearer token123"}, - }, - headerToTest: "Authorization", - expectedValue: "Bearer token123", - }, - { - name: "multiple value header - first value", - inputHeaders: map[string][]string{ - "Accept": {"application/json", "text/html"}, - }, - headerToTest: "Accept", - expectedValue: "application/json", - }, - { - name: "forwarded header preservation", - inputHeaders: map[string][]string{ - "X-Forwarded-Proto": {"https"}, - }, - headerToTest: "X-Forwarded-Proto", - expectedValue: "https", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest("GET", "http://localhost:3000/test", nil) - - client.setRequestHeaders(req, tt.inputHeaders) - - actualValue := req.Header.Get(tt.headerToTest) - if actualValue != tt.expectedValue { - t.Errorf("Header %s = %s, want %s", tt.headerToTest, actualValue, tt.expectedValue) - } - }) - } -} diff --git a/client/runner.go b/client/runner.go deleted file mode 100644 index a531af7..0000000 --- a/client/runner.go +++ /dev/null @@ -1,121 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math" - "net/url" - "strings" - "sync" - "time" - - "github.com/karol-broda/funnel/shared" -) - -const maxReconnectDelay = 30 * time.Second - -func Run(tunnelID, serverURL, localAddr, token string, shutdown <-chan struct{}) { - logger := shared.GetTunnelLogger("client.runner", tunnelID) - logger.Info().Str("local_addr", localAddr).Str("server_url", serverURL).Bool("has_token", token != "").Msg("starting tunnel client with reconnection logic") - - reconnectAttempts := 0 - - for { - select { - case <-shutdown: - logger.Info().Msg("shutdown signal received, stopping client runner.") - return - default: - // continue - } - - c := New(tunnelID, serverURL, localAddr, token) - - logger.Info().Int("attempt", reconnectAttempts+1).Msg("attempting to connect to server") - err := c.connect(c.ctx) - - if err != nil { - errorCategory := categorizeConnectionError(err) - logger.Error().Err(err).Str("error_category", errorCategory).Msg("connection failed") - - select { - case <-shutdown: - continue - case <-time.After(getReconnectDelay(reconnectAttempts)): - reconnectAttempts++ - continue - } - } - - logger.Info().Msg("connected successfully") - reconnectAttempts = 0 - - if u, err := url.Parse(c.ServerURL); err != nil { - logger.Error().Err(err).Msg("failed to parse server url for public url display") - } else { - publicURL := fmt.Sprintf("http://%s.%s", c.TunnelID, u.Host) - logger.Info().Str("public_url", publicURL).Msg("tunnel is available") - } - - var wg sync.WaitGroup - wg.Add(2) - - runCtx, cancelRun := context.WithCancel(context.Background()) - - go func() { - defer wg.Done() - c.readPump() - cancelRun() - }() - - go func() { - defer wg.Done() - c.writePump() - cancelRun() - }() - - select { - case <-shutdown: - logger.Info().Msg("shutdown during active connection, closing.") - c.Close() - case <-runCtx.Done(): - logger.Warn().Msg("connection lost, will attempt to reconnect.") - } - - wg.Wait() - c.Close() - } -} - -func getReconnectDelay(attempts int) time.Duration { - delay := time.Duration(math.Pow(2, float64(attempts))) * time.Second - if delay > maxReconnectDelay { - return maxReconnectDelay - } - return delay -} - -func categorizeConnectionError(err error) string { - if err == nil { - return "unknown" - } - - errStr := strings.ToLower(err.Error()) - - switch { - case strings.Contains(errStr, "timeout"): - return "timeout" - case strings.Contains(errStr, "refused"): - return "connection_refused" - case strings.Contains(errStr, "no such host"): - return "dns_resolution" - case strings.Contains(errStr, "network"): - return "network_error" - case strings.Contains(errStr, "tls"): - return "tls_error" - case strings.Contains(errStr, "websocket"): - return "websocket_error" - default: - return "other" - } -} diff --git a/cmd/funnel/go.mod b/cmd/funnel/go.mod deleted file mode 100644 index fb2c473..0000000 --- a/cmd/funnel/go.mod +++ /dev/null @@ -1,28 +0,0 @@ -module github.com/karol-broda/funnel/cmd/funnel - -go 1.24.4 - -require ( - github.com/karol-broda/funnel/client v0.0.0-00010101000000-000000000000 - github.com/karol-broda/funnel/shared v0.0.0-00010101000000-000000000000 - github.com/karol-broda/funnel/version v0.0.0-00010101000000-000000000000 -) - -require github.com/BurntSushi/toml v1.5.0 // indirect - -require ( - github.com/gorilla/websocket v1.5.3 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/rs/zerolog v1.34.0 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.6 // indirect - golang.org/x/sys v0.32.0 // indirect -) - -replace github.com/karol-broda/funnel/client => ../../client - -replace github.com/karol-broda/funnel/shared => ../../shared - -replace github.com/karol-broda/funnel/version => ../../version diff --git a/cmd/funnel/go.sum b/cmd/funnel/go.sum deleted file mode 100644 index 1d397bb..0000000 --- a/cmd/funnel/go.sum +++ /dev/null @@ -1,30 +0,0 @@ -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/funnel/main.go b/cmd/funnel/main.go deleted file mode 100644 index 4feb095..0000000 --- a/cmd/funnel/main.go +++ /dev/null @@ -1,253 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/signal" - "path/filepath" - "strings" - "syscall" - - "github.com/karol-broda/funnel/client" - "github.com/karol-broda/funnel/shared" - "github.com/karol-broda/funnel/version" - "github.com/rs/zerolog" - "github.com/spf13/cobra" -) - -var ( - server string - local string - id string - inlet string - token string -) - -var rootCmd = &cobra.Command{ - Use: filepath.Base(os.Args[0]), - Short: "a tunnel client for creating secure tunnels", - Long: `a tunnel client that creates secure tunnels to expose local services through a remote server`, -} - -var httpCmd = &cobra.Command{ - Use: "http [address:port | port]", - Short: "create an HTTP tunnel", - Long: `create an HTTP tunnel to expose a local HTTP service through a remote server`, - Args: cobra.ExactArgs(1), - Run: runHTTPClient, -} - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "print version information", - Run: func(cmd *cobra.Command, args []string) { - version.PrintVersionInfo("funnel-client") - }, -} - -var configCmd = &cobra.Command{ - Use: "config", - Short: "manage client configuration", -} - -var configSetTokenCmd = &cobra.Command{ - Use: "set-token ", - Short: "save authentication token to config file", - Args: cobra.ExactArgs(1), - Run: runConfigSetToken, -} - -var configSetServerCmd = &cobra.Command{ - Use: "set-server ", - Short: "save server URL to config file", - Args: cobra.ExactArgs(1), - Run: runConfigSetServer, -} - -var configShowCmd = &cobra.Command{ - Use: "show", - Short: "show current configuration", - Run: runConfigShow, -} - -var configPathCmd = &cobra.Command{ - Use: "path", - Short: "show config file path", - Run: runConfigPath, -} - -var configInlet string - -func init() { - httpCmd.Flags().StringVarP(&server, "server", "s", "", "tunnel server url (overrides config)") - httpCmd.Flags().StringVarP(&id, "id", "i", "", "tunnel id (subdomain)") - httpCmd.Flags().StringVarP(&inlet, "inlet", "", "default", "inlet configuration to use") - httpCmd.Flags().StringVarP(&token, "token", "t", "", "authentication token (overrides config)") - - configCmd.PersistentFlags().StringVar(&configInlet, "inlet", "default", "inlet to configure") - configCmd.AddCommand(configSetTokenCmd, configSetServerCmd, configShowCmd, configPathCmd) - - rootCmd.AddCommand(httpCmd) - rootCmd.AddCommand(versionCmd) - rootCmd.AddCommand(configCmd) -} - -func runHTTPClient(cmd *cobra.Command, args []string) { - shared.InitializeLogging(shared.DefaultLogConfig()) - - logger := shared.GetLogger("client") - - logger.Info(). - Str("version", version.GetVersion()). - Str("inlet", inlet). - Msg("tunnel client starting up") - - localArg := args[0] - if localArg == "" { - logger.Fatal().Msg("local address or port cannot be empty") - } - - if strings.Contains(localArg, ":") { - local = localArg - logger.Info().Str("local_address", local).Msg("using provided address:port") - } else { - local = "localhost:" + localArg - logger.Info().Str("port", localArg).Str("constructed_local", local).Msg("constructed local address from port") - } - - // resolve server URL and token from config or command line flags - finalServer, finalToken, err := resolveServerAndToken(logger) - if err != nil { - logger.Fatal().Err(err).Msg("failed to resolve server configuration") - } - - logger.Info().Str("server", finalServer).Bool("has_token", finalToken != "").Msg("using server configuration") - - if id == "" { - generatedID, err := shared.GenerateDomainSafeID() - if err != nil { - logger.Fatal().Err(err).Msg("failed to generate tunnel ID") - } - id = generatedID - logger.Info().Str("generated_id", id).Msg("generated tunnel ID") - } else { - logger.Info().Str("provided_id", id).Msg("using provided tunnel ID") - } - - logger.Info().Msg("client configuration validated, starting tunnel") - - shutdownChan := make(chan struct{}) - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - go func() { - <-sigChan - logger.Info().Msg("received shutdown signal, initiating shutdown") - close(shutdownChan) - }() - - client.Run(id, finalServer, local, finalToken, shutdownChan) - - logger.Info().Msg("client has shut down") -} - -func resolveServerAndToken(logger zerolog.Logger) (string, string, error) { - var finalServer string - var finalToken string - - // try to load configuration - configManager := client.NewConfigManager() - inletConfig, err := configManager.GetInlet(inlet) - - // resolve server: command line flag takes precedence over config - if server != "" { - finalServer = server - logger.Info().Str("server", server).Msg("using server from command line flag") - } else if err == nil && inletConfig != nil { - finalServer = inletConfig.Server - logger.Info(). - Str("inlet", inlet). - Str("server", inletConfig.Server). - Str("domain", inletConfig.Domain). - Msg("using server from configuration") - } else { - logger.Error().Err(err).Str("inlet", inlet).Msg("failed to load inlet configuration") - return "", "", fmt.Errorf("no configuration found. please specify --server flag or create a config file at ~/.config/funnel/config.toml") - } - - // resolve token: command line flag takes precedence over config - if token != "" { - finalToken = token - logger.Info().Msg("using token from command line flag") - } else if err == nil && inletConfig != nil && inletConfig.Token != "" { - finalToken = inletConfig.Token - logger.Info().Str("inlet", inlet).Msg("using token from configuration") - } - - return finalServer, finalToken, nil -} - -func runConfigSetToken(cmd *cobra.Command, args []string) { - tokenValue := args[0] - - configManager := client.NewConfigManager() - if err := configManager.SetToken(configInlet, tokenValue); err != nil { - fmt.Fprintf(os.Stderr, "error: failed to save token: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Token saved to inlet %q\n", configInlet) - fmt.Printf(" config file: %s\n", configManager.GetConfigPath()) - fmt.Printf("\n Warning: token is stored in plain text in the config file.\n") - fmt.Printf(" Ensure the file has appropriate permissions (chmod 600).\n") -} - -func runConfigSetServer(cmd *cobra.Command, args []string) { - serverValue := args[0] - - configManager := client.NewConfigManager() - if err := configManager.SetServer(configInlet, serverValue); err != nil { - fmt.Fprintf(os.Stderr, "error: failed to save server: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Server saved to inlet %q\n", configInlet) - fmt.Printf(" config file: %s\n", configManager.GetConfigPath()) -} - -func runConfigShow(cmd *cobra.Command, args []string) { - configManager := client.NewConfigManager() - config := configManager.GetConfig() - - fmt.Printf("Config file: %s\n\n", configManager.GetConfigPath()) - - if config == nil || len(config.Inlets) == 0 { - fmt.Println("No inlets configured.") - return - } - - for name, inlet := range config.Inlets { - fmt.Printf("[%s]\n", name) - if inlet.Server != "" { - fmt.Printf(" server: %s\n", inlet.Server) - } - if inlet.Domain != "" { - fmt.Printf(" domain: %s\n", inlet.Domain) - } - if inlet.Token != "" { - fmt.Printf(" token: %s...\n", inlet.Token[:min(10, len(inlet.Token))]) - } - fmt.Println() - } -} - -func runConfigPath(cmd *cobra.Command, args []string) { - configManager := client.NewConfigManager() - fmt.Println(configManager.GetConfigPath()) -} - -func main() { - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } -} diff --git a/cmd/server/go.mod b/cmd/server/go.mod deleted file mode 100644 index dd7531f..0000000 --- a/cmd/server/go.mod +++ /dev/null @@ -1,225 +0,0 @@ -module github.com/karol-broda/funnel/cmd/server - -go 1.24.4 - -require ( - github.com/karol-broda/funnel/server v0.0.0-00010101000000-000000000000 - github.com/karol-broda/funnel/shared v0.0.0-00010101000000-000000000000 - github.com/karol-broda/funnel/version v0.0.0-20250808200607-dc36be944b32 -) - -require ( - cloud.google.com/go/auth v0.15.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect - github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect - github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.30 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect - github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect - github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect - github.com/KyleBanks/depth v1.2.1 // indirect - github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect - github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect - github.com/aliyun/alibaba-cloud-sdk-go v1.63.100 // indirect - github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect - github.com/aws/aws-sdk-go-v2/config v1.29.9 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect - github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 // indirect - github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect - github.com/aws/smithy-go v1.22.2 // indirect - github.com/baidubce/bce-sdk-go v0.9.223 // indirect - github.com/benbjohnson/clock v1.3.0 // indirect - github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/civo/civogo v0.3.11 // indirect - github.com/cloudflare/cloudflare-go v0.115.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/dnsimple/dnsimple-go v1.7.0 // indirect - github.com/exoscale/egoscale/v3 v3.1.13 // indirect - github.com/fatih/structs v1.1.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-acme/lego/v4 v4.23.1 // indirect - github.com/go-errors/errors v1.0.1 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/spec v0.20.6 // indirect - github.com/go-openapi/swag v0.19.15 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.16.0 // indirect - github.com/go-resty/resty/v2 v2.16.5 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/gofrs/flock v0.12.1 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/gophercloud/gophercloud v1.14.1 // indirect - github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.141 // indirect - github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect - github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect - github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect - github.com/labbsr0x/goh v1.0.1 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/linode/linodego v1.48.1 // indirect - github.com/liquidweb/liquidweb-cli v0.6.9 // indirect - github.com/liquidweb/liquidweb-go v1.6.4 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.6 // indirect - github.com/miekg/dns v1.1.64 // indirect - github.com/mimuret/golang-iij-dpf v0.9.1 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect - github.com/nrdcg/auroradns v1.1.0 // indirect - github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect - github.com/nrdcg/desec v0.10.0 // indirect - github.com/nrdcg/dnspod-go v0.4.0 // indirect - github.com/nrdcg/freemyip v0.3.0 // indirect - github.com/nrdcg/goacmedns v0.2.0 // indirect - github.com/nrdcg/goinwx v0.10.0 // indirect - github.com/nrdcg/mailinabox v0.2.0 // indirect - github.com/nrdcg/namesilo v0.2.1 // indirect - github.com/nrdcg/nodion v0.1.0 // indirect - github.com/nrdcg/porkbun v0.4.0 // indirect - github.com/nzdjb/go-metaname v1.0.0 // indirect - github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect - github.com/oracle/oci-go-sdk/v65 v65.87.0 // indirect - github.com/ovh/go-ovh v1.7.0 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/peterhellberg/link v1.2.0 // indirect - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/pquerna/otp v1.4.0 // indirect - github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect - github.com/sacloud/api-client-go v0.2.10 // indirect - github.com/sacloud/go-http v0.1.8 // indirect - github.com/sacloud/iaas-api-go v1.14.0 // indirect - github.com/sacloud/packages-go v0.0.10 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 // indirect - github.com/selectel/domains-go v1.1.0 // indirect - github.com/selectel/go-selvpcclient/v3 v3.2.1 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect - github.com/softlayer/softlayer-go v1.1.7 // indirect - github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect - github.com/sony/gobreaker v0.5.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/viper v1.18.2 // indirect - github.com/stretchr/testify v1.10.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/swaggo/files/v2 v2.0.0 // indirect - github.com/swaggo/http-swagger/v2 v2.0.2 // indirect - github.com/swaggo/swag v1.16.6 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 // indirect - github.com/tjfoc/gmsm v1.4.1 // indirect - github.com/transip/gotransip/v6 v6.26.0 // indirect - github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect - github.com/vinyldns/go-vinyldns v0.9.16 // indirect - github.com/volcengine/volc-sdk-golang v1.0.199 // indirect - github.com/vultr/govultr/v3 v3.17.0 // indirect - github.com/yandex-cloud/go-genproto v0.0.0-20250319153614-fb9d3e5eb01a // indirect - github.com/yandex-cloud/go-sdk v0.0.0-20250320143332-9cbcfc5de4ae // indirect - go.mongodb.org/mongo-driver v1.13.1 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - go.uber.org/ratelimit v0.3.0 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.32.0 // indirect - google.golang.org/api v0.227.0 // indirect - google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect - google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -require ( - github.com/gorilla/websocket v1.5.3 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/rs/zerolog v1.34.0 // indirect - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.6 // indirect - golang.org/x/sys v0.32.0 // indirect -) - -replace github.com/karol-broda/funnel/server => ../../server - -replace github.com/karol-broda/funnel/shared => ../../shared - -replace github.com/karol-broda/funnel/version => ../../version diff --git a/cmd/server/go.sum b/cmd/server/go.sum deleted file mode 100644 index c6eee0e..0000000 --- a/cmd/server/go.sum +++ /dev/null @@ -1,1430 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE= -github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= -github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= -github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= -github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= -github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= -github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= -github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY= -github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/aliyun/alibaba-cloud-sdk-go v1.63.100 h1:yUkCbrSM1cWtgBfRVKMQtdt22KhDvKY7g4V+92eG9wA= -github.com/aliyun/alibaba-cloud-sdk-go v1.63.100/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= -github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= -github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 h1:0j58UseBtLuBcP6nY2z4SM1qZEvLF0ylyH6+ggnphLg= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1/go.mod h1:Qy22QnQSdHbZwMZrarsWZBIuK51isPlkD+Z4sztxX0o= -github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 h1:/nkJHXtJXJeelXHqG0898+fWKgvfaXBhGzbCsSmn9j8= -github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0/go.mod h1:kGYOjvTa0Vw0qxrqrOLut1vMnui6qLxqv/SX3vYeM8Y= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= -github.com/baidubce/bce-sdk-go v0.9.223 h1:vvDeIemf7ePPP59nLHCntQ/vS++ok2HKbRPgmz1VZKU= -github.com/baidubce/bce-sdk-go v0.9.223/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= -github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= -github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= -github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= -github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= -github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY= -github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/exoscale/egoscale/v3 v3.1.13 h1:CAGC7QRjp2AiGj01agsSD0VKCp4OZmW5f51vV2IguNQ= -github.com/exoscale/egoscale/v3 v3.1.13/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-acme/lego/v4 v4.23.1 h1:lZ5fGtGESA2L9FB8dNTvrQUq3/X4QOb8ExkKyY7LSV4= -github.com/go-acme/lego/v4 v4.23.1/go.mod h1:7UMVR7oQbIYw6V7mTgGwi4Er7B6Ww0c+c8feiBM0EgI= -github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= -github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= -github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= -github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= -github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= -github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 h1:sH7xkTfYzxIEgzq1tDHIMKRh1vThOEOGNsettdEeLbE= -github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56/go.mod h1:VSalo4adEk+3sNkmVJLnhHoOyOYYS8sTWLG4mv5BKto= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= -github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.141 h1:8i57QAi5u+iPAYze92bkIvZoHiS0J45ndul5glr/NE8= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.141/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY= -github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E= -github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 h1:wS8kTlQVeVbrepeY83s9X+XdSa6Qah5KO+tdW+zRQXU= -github.com/infobloxopen/infoblox-go-client/v2 v2.9.0/go.mod h1:NeNJpz09efw/edzqkVivGv1bWqBXTomqYBRFbP+XBqg= -github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= -github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= -github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labbsr0x/bindman-dns-webhook v1.0.2 h1:I7ITbmQPAVwrDdhd6dHKi+MYJTJqPCK0jE6YNBAevnk= -github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= -github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk= -github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/linode/linodego v1.48.1 h1:Ojw1S+K5jJr1dggO8/H6r4FINxXnJbOU5GkbpaTfmhU= -github.com/linode/linodego v1.48.1/go.mod h1:fc3t60If8X+yZTFAebhCnNDFrhwQhq9HDU92WnBousQ= -github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= -github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= -github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= -github.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9fgIbjMc= -github.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= -github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= -github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= -github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= -github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M= -github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= -github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= -github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= -github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= -github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= -github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= -github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo= -github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= -github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 h1:ouZ2JWDl8IW5k1qugYbmpbmW8hn85Ig6buSMBRlz3KI= -github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3/go.mod h1:ZwadWt7mVhMHMbAQ1w8IhDqtWO3eWqWq72W7trnaiE8= -github.com/nrdcg/desec v0.10.0 h1:qrEDiqnsvNU9QE7lXIXi/tIHAfyaFXKxF2/8/52O8uM= -github.com/nrdcg/desec v0.10.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs= -github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= -github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= -github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc= -github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM= -github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0= -github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= -github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM= -github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4= -github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk= -github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc= -github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= -github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= -github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= -github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= -github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9Hg= -github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= -github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= -github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= -github.com/oracle/oci-go-sdk/v65 v65.87.0 h1:CeVuK8t0dYODGT3P9IDhz4vyXF8poYE1ijoiO5vrKl0= -github.com/oracle/oci-go-sdk/v65 v65.87.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= -github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw= -github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= -github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= -github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= -github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= -github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE= -github.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sacloud/api-client-go v0.2.10 h1:+rv3jDohD+pkdYwOTBiB+jZsM0xK3AxadXRzhp3q66c= -github.com/sacloud/api-client-go v0.2.10/go.mod h1:Jj3CTy2+O4bcMedVDXlbHuqqche85HEPuVXoQFhLaRc= -github.com/sacloud/go-http v0.1.8 h1:ynreWA/vnM8G2ksbMlmefBHsXURKPz49qlPRqQ9IQdw= -github.com/sacloud/go-http v0.1.8/go.mod h1:7TL7TN1fnPKHsMifIqURDkGujnKViCgEz5Ei/LQdFK8= -github.com/sacloud/iaas-api-go v1.14.0 h1:xjkFWqdo4ilTrKPNNYBNWR/CZ/kVRsJrdAHAad6J/AQ= -github.com/sacloud/iaas-api-go v1.14.0/go.mod h1:C8os2Mnj0TOmMdSllwhaDWKMVG2ysFnpe69kyA4M3V0= -github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D+AOck= -github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 h1:4+LP7qmsLSGbmc66m1s5dKRMBwztRppfxFKlYqYte/c= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32/go.mod h1:kzh+BSAvpoyHHdHBCDhmSWtBc1NbLMZ2lWHqnBoxFks= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5gSHo= -github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= -github.com/selectel/go-selvpcclient/v3 v3.2.1 h1:ny6WIAMiHzKxOgOEnwcWE79wIQij1AHHylzPA41MXCw= -github.com/selectel/go-selvpcclient/v3 v3.2.1/go.mod h1:3EfSf8aEWyhspOGbvZ6mvnFg7JN5uckxNyBFPGWsXNQ= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= -github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= -github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= -github.com/softlayer/softlayer-go v1.1.7 h1:SgTL+pQZt1h+5QkAhVmHORM/7N9c1X0sljJhuOIHxWE= -github.com/softlayer/softlayer-go v1.1.7/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw= -github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= -github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= -github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= -github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= -github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= -github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= -github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= -github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128 h1:NGnqDc8FQL0YdiCHgTO4Wkso6ToD8rE3JW9VOzoPBNA= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 h1:mrJ5Fbkd7sZIJ5F6oRfh5zebPQaudPH9Y0+GUmFytYU= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128/go.mod h1:zbsYIBT+VTX4z4ocjTAdLBIWyNYj3z0BRqd0iPdnjsk= -github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= -github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550= -github.com/transip/gotransip/v6 v6.26.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= -github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= -github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI= -github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= -github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= -github.com/volcengine/volc-sdk-golang v1.0.199 h1:zv9QOqTl/IsLwtfC37GlJtcz6vMAHi+pjq8ILWjLYUc= -github.com/volcengine/volc-sdk-golang v1.0.199/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ= -github.com/vultr/govultr/v3 v3.17.0 h1:His5Jh5N8KKqaJxfy3uG6jQbLXy0TmQhNxOiRvkKk00= -github.com/vultr/govultr/v3 v3.17.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yandex-cloud/go-genproto v0.0.0-20250319153614-fb9d3e5eb01a h1:YO8gGyAV4N5SR3NzloZ1128IahSpXWr78oU7aEe7f04= -github.com/yandex-cloud/go-genproto v0.0.0-20250319153614-fb9d3e5eb01a/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= -github.com/yandex-cloud/go-sdk v0.0.0-20250320143332-9cbcfc5de4ae h1:x+uGuST05LVlgCxF5TsP8kQCCTW7uIeAQJ1dKtSmWqE= -github.com/yandex-cloud/go-sdk v0.0.0-20250320143332-9cbcfc5de4ae/go.mod h1:V71iJlJnS/NtNNdg/B7SwccBS19aXxwY3fv/wut9D74= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= -go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -go.uber.org/ratelimit v0.3.0 h1:IdZd9wqvFXnvLvSEBo0KPcGfkoBGNkpTHlrE3Rcjkjw= -go.uber.org/ratelimit v0.3.0/go.mod h1:So5LG7CV1zWpY1sHe+DXTJqQvOx+FFPFaAs2SnoyBaI= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc= -google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= -google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= -gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.13.0 h1:I5NNqI9Bi1SGK92TVkOvLTwux5LNrix/99H2datVh48= -gopkg.in/ns1/ns1-go.v2 v2.13.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index 4e0ccab..0000000 --- a/cmd/server/main.go +++ /dev/null @@ -1,310 +0,0 @@ -// Package main provides the main entry point for the funnel server. -// -// @title Funnel Server API -// @version 1.0 -// @description REST API for tunnel management and monitoring in the funnel tunneling solution -// @description -// @description This API provides endpoints for: -// @description - Health checking -// @description - Server statistics and monitoring -// @description - Tunnel management (list, view, delete) -// @description - Individual tunnel statistics -// -// @contact.name Funnel Support -// @contact.url https://github.com/karol-broda/funnel -// -// @license.name MIT -// @license.url https://github.com/karol-broda/funnel/blob/master/LICENSE.md -// -// @host localhost:8080 -// @BasePath /api -// -// @schemes http https -// -// @tag.name Server -// @tag.description Server health and statistics endpoints -// -// @tag.name Tunnels -// @tag.description Tunnel management and monitoring endpoints -package main - -import ( - "crypto/tls" - "fmt" - "net/http" - "os" - "runtime" - "time" - - "github.com/karol-broda/funnel/server" - "github.com/karol-broda/funnel/shared" - "github.com/karol-broda/funnel/version" - "github.com/spf13/cobra" -) - -var ( - port int - tlsPort int - host string - enableTls bool - certDir string - letsEncryptEmail string - dnsProvidersConfig string - tokenStorePath string - requireAuth bool -) - -func getDefaultCertDir() string { - if runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" { - return "/var/lib/funnel/certs" - } - return "./certs" -} - -func getDefaultTokenStorePath() string { - if runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" { - return "/var/lib/funnel/tokens.json" - } - return "./tokens.json" -} - -func main() { - rootCmd := &cobra.Command{ - Use: "server", - Short: "funnel server", - Run: runServer, - } - versionCmd := &cobra.Command{ - Use: "version", - Short: "print version information", - Run: func(cmd *cobra.Command, args []string) { - version.PrintVersionInfo("tunnel-server") - }, - } - - tokenCmd := &cobra.Command{ - Use: "token", - Short: "manage authentication tokens", - } - - tokenCreateCmd := &cobra.Command{ - Use: "create", - Short: "create a new authentication token", - Run: runTokenCreate, - } - tokenCreateCmd.Flags().StringVar(&tokenName, "name", "", "name for the token (required)") - tokenCreateCmd.MarkFlagRequired("name") - - tokenListCmd := &cobra.Command{ - Use: "list", - Short: "list all active tokens", - Run: runTokenList, - } - - tokenRevokeCmd := &cobra.Command{ - Use: "revoke", - Short: "revoke a token", - Run: runTokenRevoke, - } - tokenRevokeCmd.Flags().StringVar(&tokenName, "name", "", "name of the token to revoke (required)") - tokenRevokeCmd.MarkFlagRequired("name") - - tokenCmd.AddCommand(tokenCreateCmd, tokenListCmd, tokenRevokeCmd) - tokenCmd.PersistentFlags().StringVar(&tokenStorePath, "token-store", getDefaultTokenStorePath(), "path to token store file") - - rootCmd.PersistentFlags().IntVarP(&port, "port", "p", 8080, "port to listen on for http") - rootCmd.PersistentFlags().IntVar(&tlsPort, "tls-port", 8443, "port to listen on for https") - rootCmd.PersistentFlags().StringVar(&host, "host", "0.0.0.0", "host to listen on") - rootCmd.PersistentFlags().BoolVar(&enableTls, "enable-tls", false, "enable tls with let's encrypt") - rootCmd.PersistentFlags().StringVar(&certDir, "cert-dir", getDefaultCertDir(), "directory to store tls certificates") - rootCmd.PersistentFlags().StringVar(&letsEncryptEmail, "letsencrypt-email", "", "email address for let's encrypt") - rootCmd.PersistentFlags().StringVar(&dnsProvidersConfig, "dns-providers-config", "", "path to dns providers config file") - rootCmd.PersistentFlags().StringVar(&tokenStorePath, "token-store", getDefaultTokenStorePath(), "path to token store file") - rootCmd.PersistentFlags().BoolVar(&requireAuth, "require-auth", false, "require authentication for tunnel connections") - rootCmd.AddCommand(versionCmd, tokenCmd) - - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -var tokenName string - -func runTokenCreate(cmd *cobra.Command, args []string) { - shared.InitializeLogging(shared.DefaultLogConfig()) - - storePath := cmd.Flag("token-store").Value.String() - tokenStore, err := server.NewTokenStore(storePath) - if err != nil { - fmt.Fprintf(os.Stderr, "error: failed to load token store: %v\n", err) - os.Exit(1) - } - - plainToken, err := tokenStore.Create(tokenName) - if err != nil { - fmt.Fprintf(os.Stderr, "error: failed to create token: %v\n", err) - os.Exit(1) - } - - fmt.Printf("\nToken created for %q\n\n", tokenName) - fmt.Printf(" %s\n\n", plainToken) - fmt.Printf(" Save this token now - it will NOT be shown again.\n") - fmt.Printf(" Token is persisted to disk and survives server restarts.\n\n") -} - -func runTokenList(cmd *cobra.Command, args []string) { - shared.InitializeLogging(shared.DefaultLogConfig()) - - storePath := cmd.Flag("token-store").Value.String() - tokenStore, err := server.NewTokenStore(storePath) - if err != nil { - fmt.Fprintf(os.Stderr, "error: failed to load token store: %v\n", err) - os.Exit(1) - } - - tokens := tokenStore.List() - if len(tokens) == 0 { - fmt.Println("No active tokens found.") - return - } - - fmt.Printf("\n%-20s %-12s %s\n", "NAME", "PREFIX", "CREATED") - fmt.Printf("%-20s %-12s %s\n", "----", "------", "-------") - for _, t := range tokens { - age := formatAge(t.CreatedAt) - fmt.Printf("%-20s %-12s %s\n", t.Name, t.Prefix+"...", age) - } - fmt.Println() -} - -func runTokenRevoke(cmd *cobra.Command, args []string) { - shared.InitializeLogging(shared.DefaultLogConfig()) - - storePath := cmd.Flag("token-store").Value.String() - tokenStore, err := server.NewTokenStore(storePath) - if err != nil { - fmt.Fprintf(os.Stderr, "error: failed to load token store: %v\n", err) - os.Exit(1) - } - - if err := tokenStore.Revoke(tokenName); err != nil { - fmt.Fprintf(os.Stderr, "error: failed to revoke token: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Token %q revoked.\n", tokenName) -} - -func formatAge(t time.Time) string { - d := time.Since(t) - switch { - case d < time.Minute: - return "just now" - case d < time.Hour: - return fmt.Sprintf("%d minutes ago", int(d.Minutes())) - case d < 24*time.Hour: - return fmt.Sprintf("%d hours ago", int(d.Hours())) - case d < 7*24*time.Hour: - return fmt.Sprintf("%d days ago", int(d.Hours()/24)) - default: - return t.Format("2006-01-02") - } -} - -func runServer(cmd *cobra.Command, args []string) { - logger := shared.GetLogger("server.main") - - s := server.NewServer() - tunnelRouter := server.NewTunnelRouter(s) - s.SetRouter(tunnelRouter) - - // Initialize API handler - apiHandler := server.NewAPIHandler(s, tunnelRouter) - tunnelRouter.SetAPIHandler(apiHandler) - - // Initialize token store if authentication is enabled - if requireAuth { - tokenStore, err := server.NewTokenStore(tokenStorePath) - if err != nil { - logger.Fatal().Err(err).Str("path", tokenStorePath).Msg("failed to initialize token store") - } - s.SetTokenStore(tokenStore) - - if tokenStore.Count() == 0 { - logger.Warn().Msg("authentication enabled but no tokens exist - run 'server token create --name ' to create one") - } - } else { - logger.Warn().Msg("authentication disabled - anyone can create tunnels. use --require-auth to enable") - } - - if enableTls { - if letsEncryptEmail == "" || dnsProvidersConfig == "" { - logger.Fatal().Msg("--letsencrypt-email and --dns-providers-config must be set when --enable-tls is true") - } - - logger.Info(). - Str("email", letsEncryptEmail). - Str("cert_dir", certDir). - Str("dns_config", dnsProvidersConfig). - Msg("tls is enabled, initializing certificate manager") - - certManager, err := server.NewCertificateManager(letsEncryptEmail, certDir, dnsProvidersConfig) - if err != nil { - logger.Fatal().Err(err).Msg("failed to initialize certificate manager") - } - - if err := certManager.PreloadCertificates(); err != nil { - logger.Fatal().Err(err).Msg("failed to preload certificates") - } - - tlsConfig := &tls.Config{ - GetCertificate: certManager.GetCertificate, - MinVersion: tls.VersionTLS12, - } - - httpsAddr := fmt.Sprintf("%s:%d", host, tlsPort) - httpsServer := &http.Server{ - Addr: httpsAddr, - Handler: tunnelRouter, - TLSConfig: tlsConfig, - } - - logger.Info().Str("address", httpsAddr).Msg("starting https server") - go func() { - if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { - logger.Fatal().Err(err).Msg("https server failed") - } - }() - - httpAddr := fmt.Sprintf("%s:%d", host, port) - httpServer := &http.Server{ - Addr: httpAddr, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently) - }), - } - logger.Info().Str("address", httpAddr).Msg("starting http to https redirect server") - go func() { - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.Fatal().Err(err).Msg("http redirect server failed") - } - }() - - } else { - httpAddr := fmt.Sprintf("%s:%d", host, port) - httpServer := &http.Server{ - Addr: httpAddr, - Handler: tunnelRouter, - } - logger.Info().Str("address", httpAddr).Msg("starting http server") - go func() { - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.Fatal().Err(err).Msg("http server failed") - } - }() - } - - // wait indefinitely - select {} -} diff --git a/server/api.go b/server/api.go deleted file mode 100644 index b6eea63..0000000 --- a/server/api.go +++ /dev/null @@ -1,700 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - "time" - - "github.com/karol-broda/funnel/server/docs" - "github.com/karol-broda/funnel/shared" - "github.com/swaggo/swag" -) - -// APIResponse represents a standard API response structure -type APIResponse struct { - Success bool `json:"success" example:"true"` // Indicates if the request was successful - Data interface{} `json:"data,omitempty"` // Response data (present on success) - Error string `json:"error,omitempty" example:"error message"` // Error message (present on failure) -} - -// TunnelInfo represents tunnel information for API responses -type TunnelInfo struct { - ID string `json:"id" example:"my-tunnel"` // Unique tunnel identifier - CreatedAt time.Time `json:"created_at" example:"2025-08-08T18:30:00Z"` // Tunnel creation timestamp - MessagesReceived int64 `json:"messages_received" example:"45"` // Number of messages received from client - MessagesSent int64 `json:"messages_sent" example:"45"` // Number of messages sent to client - BytesReceived int64 `json:"bytes_received" example:"12543"` // Total bytes received from client - BytesSent int64 `json:"bytes_sent" example:"98765"` // Total bytes sent to client - Uptime string `json:"uptime" example:"1h15m30s"` // Tunnel connection uptime - Status string `json:"status" example:"connected" enums:"connected,disconnected"` // Current tunnel status -} - -// ServerStats represents server-wide statistics -type ServerStats struct { - ActiveTunnels int `json:"active_tunnels" example:"2"` // Number of currently active tunnel connections - TotalRequests int64 `json:"total_requests" example:"1542"` // Total number of HTTP requests processed - CacheHits int64 `json:"cache_hits" example:"1401"` // Number of cache hits for hostname resolution - CacheMisses int64 `json:"cache_misses" example:"141"` // Number of cache misses for hostname resolution - CacheHitRate string `json:"cache_hit_rate" example:"90.9%"` // Cache hit rate percentage - Uptime string `json:"uptime" example:"2h15m30s"` // Server uptime duration - RouterStats map[string]interface{} `json:"router_stats"` // Detailed router statistics -} - -// HealthData represents health check response data -type HealthData struct { - Status string `json:"status" example:"healthy"` // Service status - Timestamp string `json:"timestamp" example:"2025-08-08T20:11:00Z"` // Current timestamp - Uptime string `json:"uptime" example:"6.4281425s"` // Server uptime duration -} - -// TunnelStats represents detailed tunnel statistics -type TunnelStats struct { - TunnelID string `json:"tunnel_id" example:"my-tunnel"` // Tunnel identifier - CreatedAt time.Time `json:"created_at" example:"2025-08-08T18:30:00Z"` // Creation timestamp - Uptime string `json:"uptime" example:"1h15m30s"` // Connection uptime - MessagesReceived int64 `json:"messages_received" example:"45"` // Messages received count - MessagesSent int64 `json:"messages_sent" example:"45"` // Messages sent count - BytesReceived int64 `json:"bytes_received" example:"12543"` // Bytes received total - BytesSent int64 `json:"bytes_sent" example:"98765"` // Bytes sent total - Status string `json:"status" example:"connected"` // Connection status - ConnectionInfo map[string]interface{} `json:"connection_info"` // Connection details -} - -// TunnelDeleteData represents tunnel deletion response data -type TunnelDeleteData struct { - Message string `json:"message" example:"tunnel deleted successfully"` // Success message - TunnelID string `json:"tunnel_id" example:"my-tunnel"` // Deleted tunnel ID -} - -// DetailedTunnelMetrics represents comprehensive tunnel metrics -type DetailedTunnelMetrics struct { - TunnelID string `json:"tunnel_id" example:"my-tunnel"` // Tunnel identifier - TotalRequests int64 `json:"total_requests" example:"1542"` // Total requests processed - TotalRequestsSuccess int64 `json:"total_requests_success" example:"1498"` // Successful requests - TotalRequestsError int64 `json:"total_requests_error" example:"44"` // Failed requests - TotalBytesIn int64 `json:"total_bytes_in" example:"125430"` // Total bytes received - TotalBytesOut int64 `json:"total_bytes_out" example:"987650"` // Total bytes sent - AverageResponseTime float64 `json:"average_response_time_ms" example:"25.5"` // Average response time in ms - MinResponseTime float64 `json:"min_response_time_ms" example:"5.2"` // Minimum response time in ms - MaxResponseTime float64 `json:"max_response_time_ms" example:"150.8"` // Maximum response time in ms - RequestsPerSecond float64 `json:"requests_per_second" example:"12.4"` // Requests per second - ConnectionsActive int `json:"connections_active" example:"1"` // Active connections - ConnectionsTotal int64 `json:"connections_total" example:"3"` // Total connections made - ReconnectCount int64 `json:"reconnect_count" example:"2"` // Number of reconnections - TimeoutCount int64 `json:"timeout_count" example:"5"` // Number of timeouts - ConnectionErrors int64 `json:"connection_errors" example:"2"` // Connection errors - DataErrors int64 `json:"data_errors" example:"1"` // Data errors - ClientIP string `json:"client_ip" example:"192.168.1.100:52341"` // Client IP address - UserAgent string `json:"user_agent" example:"FunnelClient/1.0"` // Client user agent - LastActivity string `json:"last_activity" example:"2025-08-08T20:30:00Z"` // Last activity time - CreatedAt string `json:"created_at" example:"2025-08-08T18:30:00Z"` // Creation time -} - -// HistoricalMetricsResponse represents historical tunnel metrics -type HistoricalMetricsResponse struct { - TunnelID string `json:"tunnel_id" example:"my-tunnel"` // Tunnel identifier - TimeRange string `json:"time_range" example:"24h"` // Time range of data - Interval string `json:"interval" example:"1m"` // Data point interval - DataPoints []HistoricalDataPoint `json:"data_points"` // Historical data points -} - -// ServerMetricsResponse represents comprehensive server metrics -type ServerMetricsResponse struct { - TotalTunnels int64 `json:"total_tunnels" example:"15"` // Total tunnels created - ActiveTunnels int `json:"active_tunnels" example:"8"` // Currently active tunnels - TotalRequests int64 `json:"total_requests" example:"125430"` // Total requests processed - TotalBytesTransferred int64 `json:"total_bytes_transferred" example:"5670000"` // Total bytes transferred - AverageResponseTime float64 `json:"average_response_time_ms" example:"28.7"` // Server average response time - ServerRequestsPerSec float64 `json:"server_requests_per_second" example:"45.2"` // Server-wide RPS - MemoryUsage int64 `json:"memory_usage_bytes" example:"67108864"` // Memory usage in bytes - CPUUsage float64 `json:"cpu_usage_percent" example:"15.5"` // CPU usage percentage - NetworkConnections int `json:"network_connections" example:"12"` // Active network connections - TotalErrors int64 `json:"total_errors" example:"234"` // Total errors - ErrorRate float64 `json:"error_rate_percent" example:"0.18"` // Error rate percentage - ServerUptime string `json:"server_uptime" example:"5h23m15s"` // Server uptime -} - -// APIHandler handles REST API requests -type APIHandler struct { - server *Server - router *TunnelRouter - startTime time.Time -} - -// NewAPIHandler creates a new API handler -func NewAPIHandler(server *Server, router *TunnelRouter) *APIHandler { - return &APIHandler{ - server: server, - router: router, - startTime: time.Now(), - } -} - -// HandleAPIRequest routes API requests to appropriate handlers -func (api *APIHandler) HandleAPIRequest(w http.ResponseWriter, r *http.Request) { - logger := shared.GetLogger("server.api") - - logger.Info(). - Str("method", r.Method). - Str("path", r.URL.Path). - Str("remote_addr", r.RemoteAddr). - Msg("API request received") - - // Set common headers - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - - // Handle CORS preflight - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - // Route based on path - path := strings.TrimPrefix(r.URL.Path, "/api") - - switch { - case path == "/health": - api.handleHealth(w, r) - case path == "/server/stats": - api.handleServerStats(w, r) - case path == "/tunnels": - api.handleTunnels(w, r) - case strings.HasPrefix(path, "/tunnels/"): - api.handleTunnelSpecific(w, r, path) - case path == "/metrics": - api.handleMetrics(w, r) - case path == "/metrics/historical": - api.handleHistoricalMetrics(w, r) - - case path == "/swagger/doc.json": - api.ServeSwaggerJSON(w, r) - default: - api.writeErrorResponse(w, "endpoint not found", http.StatusNotFound) - } -} - -// handleHealth returns a health check response -// -// @Summary Health check -// @Description Returns the health status of the server -// @Tags Server -// @Accept json -// @Produce json -// @Success 200 {object} APIResponse{data=HealthData} "Server is healthy" -// @Router /health [get] -func (api *APIHandler) handleHealth(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - health := map[string]interface{}{ - "status": "healthy", - "timestamp": time.Now().UTC(), - "uptime": time.Since(api.startTime).String(), - } - - api.writeSuccessResponse(w, health) -} - -// handleServerStats returns server-wide statistics -// -// @Summary Get server statistics -// @Description Returns comprehensive server-wide statistics including tunnel count, request metrics, and cache performance -// @Tags Server -// @Accept json -// @Produce json -// @Success 200 {object} APIResponse{data=ServerStats} "Server statistics retrieved successfully" -// @Router /server/stats [get] -func (api *APIHandler) handleServerStats(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - api.server.TunnelsMu.RLock() - activeTunnels := len(api.server.Tunnels) - api.server.TunnelsMu.RUnlock() - - routerStats := api.router.GetStats() - - stats := ServerStats{ - ActiveTunnels: activeTunnels, - TotalRequests: routerStats["total_requests"].(int64), - CacheHits: routerStats["cache_hits"].(int64), - CacheMisses: routerStats["cache_misses"].(int64), - CacheHitRate: routerStats["cache_hit_rate"].(string), - Uptime: time.Since(api.startTime).String(), - RouterStats: routerStats, - } - - api.writeSuccessResponse(w, stats) -} - -// handleTunnels handles requests to /api/tunnels -func (api *APIHandler) handleTunnels(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - api.handleListTunnels(w, r) - default: - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - -// handleListTunnels returns a list of all active tunnels -// -// @Summary List active tunnels -// @Description Returns a list of all currently active tunnel connections -// @Tags Tunnels -// @Accept json -// @Produce json -// @Success 200 {object} APIResponse{data=[]TunnelInfo} "List of active tunnels" -// @Router /tunnels [get] -func (api *APIHandler) handleListTunnels(w http.ResponseWriter, r *http.Request) { - api.server.TunnelsMu.RLock() - defer api.server.TunnelsMu.RUnlock() - - tunnels := make([]TunnelInfo, 0, len(api.server.Tunnels)) - for _, tunnel := range api.server.Tunnels { - tunnelInfo := api.getTunnelInfo(tunnel) - tunnels = append(tunnels, tunnelInfo) - } - - api.writeSuccessResponse(w, tunnels) -} - -// handleTunnelSpecific handles requests to specific tunnel endpoints -func (api *APIHandler) handleTunnelSpecific(w http.ResponseWriter, r *http.Request, path string) { - // Extract tunnel ID from path: /tunnels/{id} or /tunnels/{id}/stats - pathParts := strings.Split(strings.TrimPrefix(path, "/tunnels/"), "/") - if len(pathParts) == 0 || pathParts[0] == "" { - api.writeErrorResponse(w, "tunnel ID required", http.StatusBadRequest) - return - } - - tunnelID := pathParts[0] - - // Validate tunnel ID format - if err := shared.ValidateTunnelID(tunnelID); err != nil { - api.writeErrorResponse(w, fmt.Sprintf("invalid tunnel ID format: %s", err.Error()), http.StatusBadRequest) - return - } - - tunnel, exists := api.server.GetTunnel(tunnelID) - if !exists { - api.writeErrorResponse(w, "tunnel not found", http.StatusNotFound) - return - } - - // Handle different endpoints - if len(pathParts) == 1 { - // /tunnels/{id} - switch r.Method { - case "GET": - api.handleGetTunnel(w, r, tunnel) - case "DELETE": - api.handleDeleteTunnel(w, r, tunnel) - default: - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - } - } else if len(pathParts) == 2 && pathParts[1] == "stats" { - // /tunnels/{id}/stats - switch r.Method { - case "GET": - api.handleGetTunnelStats(w, r, tunnel) - default: - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - } - } else if len(pathParts) == 2 && pathParts[1] == "metrics" { - // /tunnels/{id}/metrics - switch r.Method { - case "GET": - api.handleGetTunnelMetrics(w, r, tunnel) - default: - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - } - } else if len(pathParts) == 2 && pathParts[1] == "historical" { - // /tunnels/{id}/historical - switch r.Method { - case "GET": - api.handleGetTunnelHistorical(w, r, tunnel) - default: - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - } - } else { - api.writeErrorResponse(w, "endpoint not found", http.StatusNotFound) - } -} - -// handleGetTunnel returns information about a specific tunnel -// -// @Summary Get tunnel details -// @Description Returns detailed information about a specific tunnel -// @Tags Tunnels -// @Accept json -// @Produce json -// @Param tunnelId path string true "Tunnel ID (3-63 characters, lowercase letters, numbers, and hyphens only)" minlength(3) maxlength(63) -// @Success 200 {object} APIResponse{data=TunnelInfo} "Tunnel details retrieved successfully" -// @Failure 400 {object} APIResponse "Invalid tunnel ID format" -// @Failure 404 {object} APIResponse "Tunnel not found" -// @Router /tunnels/{tunnelId} [get] -func (api *APIHandler) handleGetTunnel(w http.ResponseWriter, r *http.Request, tunnel *Tunnel) { - tunnelInfo := api.getTunnelInfo(tunnel) - api.writeSuccessResponse(w, tunnelInfo) -} - -// handleDeleteTunnel forcibly closes a tunnel -// -// @Summary Delete tunnel -// @Description Forcibly closes a tunnel connection -// @Tags Tunnels -// @Accept json -// @Produce json -// @Param tunnelId path string true "Tunnel ID to delete" minlength(3) maxlength(63) -// @Success 200 {object} APIResponse{data=TunnelDeleteData} "Tunnel deleted successfully" -// @Failure 400 {object} APIResponse "Invalid tunnel ID format" -// @Failure 404 {object} APIResponse "Tunnel not found" -// @Router /tunnels/{tunnelId} [delete] -func (api *APIHandler) handleDeleteTunnel(w http.ResponseWriter, r *http.Request, tunnel *Tunnel) { - logger := shared.GetTunnelLogger("server.api", tunnel.ID) - - logger.Info(). - Str("remote_addr", r.RemoteAddr). - Msg("tunnel deletion requested via API") - - // Remove the tunnel - api.server.RemoveTunnel(tunnel.ID) - - response := map[string]interface{}{ - "message": "tunnel deleted successfully", - "tunnel_id": tunnel.ID, - } - - api.writeSuccessResponse(w, response) -} - -// handleGetTunnelStats returns detailed statistics for a specific tunnel -// -// @Summary Get tunnel statistics -// @Description Returns detailed statistics and metrics for a specific tunnel -// @Tags Tunnels -// @Accept json -// @Produce json -// @Param tunnelId path string true "Tunnel ID" minlength(3) maxlength(63) -// @Success 200 {object} APIResponse{data=TunnelStats} "Tunnel statistics retrieved successfully" -// @Failure 400 {object} APIResponse "Invalid tunnel ID format" -// @Failure 404 {object} APIResponse "Tunnel not found" -// @Router /tunnels/{tunnelId}/stats [get] -func (api *APIHandler) handleGetTunnelStats(w http.ResponseWriter, r *http.Request, tunnel *Tunnel) { - stats := map[string]interface{}{ - "tunnel_id": tunnel.ID, - "created_at": tunnel.createdAt, - "uptime": time.Since(tunnel.createdAt).String(), - "messages_received": tunnel.messagesReceived, - "messages_sent": tunnel.messagesSent, - "bytes_received": tunnel.bytesReceived, - "bytes_sent": tunnel.bytesSent, - "status": "connected", - "connection_info": map[string]interface{}{ - "remote_addr": tunnel.getRemoteAddr(), - }, - } - - api.writeSuccessResponse(w, stats) -} - -// getTunnelInfo converts a Tunnel to TunnelInfo for API responses -func (api *APIHandler) getTunnelInfo(tunnel *Tunnel) TunnelInfo { - return TunnelInfo{ - ID: tunnel.ID, - CreatedAt: tunnel.createdAt, - MessagesReceived: tunnel.messagesReceived, - MessagesSent: tunnel.messagesSent, - BytesReceived: tunnel.bytesReceived, - BytesSent: tunnel.bytesSent, - Uptime: time.Since(tunnel.createdAt).String(), - Status: "connected", - } -} - -// writeSuccessResponse writes a successful API response -func (api *APIHandler) writeSuccessResponse(w http.ResponseWriter, data interface{}) { - response := APIResponse{ - Success: true, - Data: data, - } - - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(response); err != nil { - logger := shared.GetLogger("server.api") - logger.Error().Err(err).Msg("failed to encode success response") - } -} - -// writeErrorResponse writes an error API response -func (api *APIHandler) writeErrorResponse(w http.ResponseWriter, message string, statusCode int) { - response := APIResponse{ - Success: false, - Error: message, - } - - w.WriteHeader(statusCode) - if err := json.NewEncoder(w).Encode(response); err != nil { - logger := shared.GetLogger("server.api") - logger.Error().Err(err).Msg("failed to encode error response") - } -} - -// ServeSwaggerJSON serves the swagger.json file for the Swagger UI -func (api *APIHandler) ServeSwaggerJSON(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - logger := shared.GetLogger("server.api") - logger.Info(). - Str("remote_addr", r.RemoteAddr). - Msg("Swagger JSON requested") - - // Update swagger info with current request details - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - - docs.SwaggerInfo.Host = r.Host - docs.SwaggerInfo.Schemes = []string{scheme} - docs.SwaggerInfo.BasePath = "/api" - - // Get the swagger specification - doc, err := swag.ReadDoc("swagger") - if err != nil { - logger.Error().Err(err).Msg("failed to read swagger documentation") - api.writeErrorResponse(w, "failed to generate swagger documentation", http.StatusInternalServerError) - return - } - - // Set headers for JSON response - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - w.WriteHeader(http.StatusOK) - - // Write the swagger JSON - if _, err := w.Write([]byte(doc)); err != nil { - logger.Error().Err(err).Msg("failed to write swagger documentation") - } -} - -// handleMetrics returns comprehensive server-wide metrics -// -// @Summary Get comprehensive server metrics -// @Description Returns detailed server-wide metrics including performance, resource usage, and aggregated statistics -// @Tags Server -// @Accept json -// @Produce json -// @Success 200 {object} APIResponse{data=ServerMetricsResponse} "Comprehensive server metrics" -// @Router /metrics [get] -func (api *APIHandler) handleMetrics(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - api.server.TunnelsMu.RLock() - activeTunnels := len(api.server.Tunnels) - totalTunnels := int64(0) - totalRequests := int64(0) - totalBytes := int64(0) - totalErrors := int64(0) - - // Aggregate statistics from all tunnels - for _, tunnel := range api.server.Tunnels { - if tunnel.statistics != nil { - stats := tunnel.statistics.GetSnapshot() - totalRequests += stats.TotalRequests - totalBytes += stats.TotalBytesIn + stats.TotalBytesOut - totalErrors += stats.TotalRequestsError - } - totalTunnels++ - } - api.server.TunnelsMu.RUnlock() - - // Calculate server-wide metrics - uptime := time.Since(api.startTime) - serverRequestsPerSec := float64(0) - if uptime.Seconds() > 0 { - serverRequestsPerSec = float64(totalRequests) / uptime.Seconds() - } - - errorRate := float64(0) - if totalRequests > 0 { - errorRate = (float64(totalErrors) / float64(totalRequests)) * 100 - } - - metrics := ServerMetricsResponse{ - TotalTunnels: totalTunnels, - ActiveTunnels: activeTunnels, - TotalRequests: totalRequests, - TotalBytesTransferred: totalBytes, - AverageResponseTime: 0, // Could be calculated from tunnel averages - ServerRequestsPerSec: serverRequestsPerSec, - MemoryUsage: 0, // Would need runtime.ReadMemStats() - CPUUsage: 0, // Would need system monitoring - NetworkConnections: activeTunnels, - TotalErrors: totalErrors, - ErrorRate: errorRate, - ServerUptime: uptime.String(), - } - - api.writeSuccessResponse(w, metrics) -} - -// handleHistoricalMetrics returns historical server metrics -// -// @Summary Get historical server metrics -// @Description Returns time-series data for server-wide metrics over a specified time range -// @Tags Server -// @Accept json -// @Produce json -// @Param range query string false "Time range (1h, 24h, 7d)" default(24h) -// @Param interval query string false "Data interval (1m, 5m, 1h)" default(5m) -// @Success 200 {object} APIResponse{data=HistoricalMetricsResponse} "Historical server metrics" -// @Router /metrics/historical [get] -func (api *APIHandler) handleHistoricalMetrics(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - api.writeErrorResponse(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - // Parse query parameters - timeRange := r.URL.Query().Get("range") - if timeRange == "" { - timeRange = "24h" - } - - interval := r.URL.Query().Get("interval") - if interval == "" { - interval = "5m" - } - - // For now, return aggregated data from all tunnels - // In a production system, this would come from a time-series database - response := HistoricalMetricsResponse{ - TunnelID: "server", - TimeRange: timeRange, - Interval: interval, - DataPoints: []HistoricalDataPoint{}, // Would be populated from historical data - } - - api.writeSuccessResponse(w, response) -} - -// handleGetTunnelMetrics returns comprehensive metrics for a specific tunnel -// -// @Summary Get comprehensive tunnel metrics -// @Description Returns detailed performance and usage metrics for a specific tunnel -// @Tags Tunnels -// @Accept json -// @Produce json -// @Param tunnelId path string true "Tunnel ID" minlength(3) maxlength(63) -// @Success 200 {object} APIResponse{data=DetailedTunnelMetrics} "Comprehensive tunnel metrics" -// @Failure 400 {object} APIResponse "Invalid tunnel ID format" -// @Failure 404 {object} APIResponse "Tunnel not found" -// @Router /tunnels/{tunnelId}/metrics [get] -func (api *APIHandler) handleGetTunnelMetrics(w http.ResponseWriter, r *http.Request, tunnel *Tunnel) { - if tunnel.statistics == nil { - api.writeErrorResponse(w, "tunnel statistics not available", http.StatusInternalServerError) - return - } - - stats := tunnel.statistics.GetSnapshot() - - metrics := DetailedTunnelMetrics{ - TunnelID: tunnel.ID, - TotalRequests: stats.TotalRequests, - TotalRequestsSuccess: stats.TotalRequestsSuccess, - TotalRequestsError: stats.TotalRequestsError, - TotalBytesIn: stats.TotalBytesIn, - TotalBytesOut: stats.TotalBytesOut, - AverageResponseTime: stats.AverageResponseTime, - MinResponseTime: stats.MinResponseTime, - MaxResponseTime: stats.MaxResponseTime, - RequestsPerSecond: stats.RequestsPerSecond, - ConnectionsActive: stats.ConnectionsActive, - ConnectionsTotal: stats.ConnectionsTotal, - ReconnectCount: stats.ReconnectCount, - TimeoutCount: stats.TimeoutCount, - ConnectionErrors: stats.ConnectionErrors, - DataErrors: stats.DataErrors, - ClientIP: stats.ClientIP, - UserAgent: stats.UserAgent, - LastActivity: stats.LastActivity.Format(time.RFC3339), - CreatedAt: stats.CreatedAt.Format(time.RFC3339), - } - - api.writeSuccessResponse(w, metrics) -} - -// handleGetTunnelHistorical returns historical data for a specific tunnel -// -// @Summary Get tunnel historical data -// @Description Returns time-series data for a specific tunnel over a specified time range -// @Tags Tunnels -// @Accept json -// @Produce json -// @Param tunnelId path string true "Tunnel ID" minlength(3) maxlength(63) -// @Param range query string false "Time range (1h, 24h, 7d)" default(24h) -// @Param start query string false "Start time (RFC3339 format)" -// @Param end query string false "End time (RFC3339 format)" -// @Success 200 {object} APIResponse{data=HistoricalMetricsResponse} "Tunnel historical data" -// @Failure 400 {object} APIResponse "Invalid parameters" -// @Failure 404 {object} APIResponse "Tunnel not found" -// @Router /tunnels/{tunnelId}/historical [get] -func (api *APIHandler) handleGetTunnelHistorical(w http.ResponseWriter, r *http.Request, tunnel *Tunnel) { - if tunnel.historicalData == nil { - api.writeErrorResponse(w, "tunnel historical data not available", http.StatusInternalServerError) - return - } - - // Parse query parameters - timeRange := r.URL.Query().Get("range") - if timeRange == "" { - timeRange = "24h" - } - - startTimeStr := r.URL.Query().Get("start") - endTimeStr := r.URL.Query().Get("end") - - var dataPoints []HistoricalDataPoint - - if startTimeStr != "" && endTimeStr != "" { - // Use specific time range - startTime, err1 := time.Parse(time.RFC3339, startTimeStr) - endTime, err2 := time.Parse(time.RFC3339, endTimeStr) - - if err1 != nil || err2 != nil { - api.writeErrorResponse(w, "invalid time format, use RFC3339", http.StatusBadRequest) - return - } - - dataPoints = tunnel.historicalData.GetDataPointsInRange(startTime, endTime) - } else { - // Use all available data - dataPoints = tunnel.historicalData.GetDataPoints() - } - - response := HistoricalMetricsResponse{ - TunnelID: tunnel.ID, - TimeRange: timeRange, - Interval: tunnel.historicalData.Interval.String(), - DataPoints: dataPoints, - } - - api.writeSuccessResponse(w, response) -} diff --git a/server/auth.go b/server/auth.go deleted file mode 100644 index d3cb185..0000000 --- a/server/auth.go +++ /dev/null @@ -1,240 +0,0 @@ -package server - -import ( - "crypto/rand" - "crypto/sha256" - "crypto/subtle" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" - "sync" - "time" - - "github.com/karol-broda/funnel/shared" -) - -type TokenRecord struct { - Name string `json:"name"` - Hash string `json:"hash"` - Prefix string `json:"prefix"` - CreatedAt time.Time `json:"created_at"` - Revoked bool `json:"revoked,omitempty"` -} - -type TokenStore struct { - path string - records []TokenRecord - mu sync.RWMutex - enabled bool -} - -func NewTokenStore(path string) (*TokenStore, error) { - logger := shared.GetLogger("server.auth") - - ts := &TokenStore{ - path: path, - records: []TokenRecord{}, - enabled: true, - } - - if path == "" { - ts.enabled = false - logger.Info().Msg("token store disabled - no path configured") - return ts, nil - } - - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0700); err != nil { - return nil, fmt.Errorf("failed to create token store directory: %w", err) - } - - data, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - logger.Info().Str("path", path).Msg("token store file not found, starting with empty store") - return ts, nil - } - return nil, fmt.Errorf("failed to read token store: %w", err) - } - - if err := json.Unmarshal(data, &ts.records); err != nil { - return nil, fmt.Errorf("failed to parse token store: %w", err) - } - - activeCount := 0 - for _, r := range ts.records { - if !r.Revoked { - activeCount++ - } - } - - logger.Info(). - Str("path", path). - Int("total_tokens", len(ts.records)). - Int("active_tokens", activeCount). - Msg("token store loaded") - - return ts, nil -} - -func (ts *TokenStore) IsEnabled() bool { - return ts.enabled -} - -func (ts *TokenStore) save() error { - if ts.path == "" { - return nil - } - - data, err := json.MarshalIndent(ts.records, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal tokens: %w", err) - } - - tmpPath := ts.path + ".tmp" - if err := os.WriteFile(tmpPath, data, 0600); err != nil { - return fmt.Errorf("failed to write token store: %w", err) - } - - if err := os.Rename(tmpPath, ts.path); err != nil { - os.Remove(tmpPath) - return fmt.Errorf("failed to rename token store: %w", err) - } - - return nil -} - -func (ts *TokenStore) Create(name string) (string, error) { - logger := shared.GetLogger("server.auth") - - ts.mu.Lock() - defer ts.mu.Unlock() - - for _, r := range ts.records { - if r.Name == name && !r.Revoked { - return "", fmt.Errorf("token with name %q already exists", name) - } - } - - plainToken, err := generateSecureToken() - if err != nil { - return "", fmt.Errorf("failed to generate token: %w", err) - } - - record := TokenRecord{ - Name: name, - Hash: hashToken(plainToken), - Prefix: plainToken[:10], - CreatedAt: time.Now(), - Revoked: false, - } - - ts.records = append(ts.records, record) - - if err := ts.save(); err != nil { - ts.records = ts.records[:len(ts.records)-1] - return "", fmt.Errorf("failed to save token: %w", err) - } - - logger.Info(). - Str("name", name). - Str("prefix", record.Prefix). - Msg("token created") - - return plainToken, nil -} - -func (ts *TokenStore) Validate(plainToken string) (*TokenRecord, bool) { - if !ts.enabled { - return nil, true - } - - if plainToken == "" { - return nil, false - } - - hash := hashToken(plainToken) - - ts.mu.RLock() - defer ts.mu.RUnlock() - - for i := range ts.records { - if ts.records[i].Revoked { - continue - } - if subtle.ConstantTimeCompare([]byte(ts.records[i].Hash), []byte(hash)) == 1 { - return &ts.records[i], true - } - } - - return nil, false -} - -func (ts *TokenStore) Revoke(name string) error { - logger := shared.GetLogger("server.auth") - - ts.mu.Lock() - defer ts.mu.Unlock() - - found := false - for i := range ts.records { - if ts.records[i].Name == name && !ts.records[i].Revoked { - ts.records[i].Revoked = true - found = true - break - } - } - - if !found { - return fmt.Errorf("token %q not found or already revoked", name) - } - - if err := ts.save(); err != nil { - return fmt.Errorf("failed to save token store: %w", err) - } - - logger.Info().Str("name", name).Msg("token revoked") - return nil -} - -func (ts *TokenStore) List() []TokenRecord { - ts.mu.RLock() - defer ts.mu.RUnlock() - - result := make([]TokenRecord, 0) - for _, r := range ts.records { - if !r.Revoked { - result = append(result, r) - } - } - return result -} - -func (ts *TokenStore) Count() int { - ts.mu.RLock() - defer ts.mu.RUnlock() - - count := 0 - for _, r := range ts.records { - if !r.Revoked { - count++ - } - } - return count -} - -func generateSecureToken() (string, error) { - bytes := make([]byte, 32) - if _, err := rand.Read(bytes); err != nil { - return "", err - } - return "sk_" + base64.RawURLEncoding.EncodeToString(bytes), nil -} - -func hashToken(token string) string { - h := sha256.Sum256([]byte(token)) - return hex.EncodeToString(h[:]) -} diff --git a/server/auth_test.go b/server/auth_test.go deleted file mode 100644 index 2d5932d..0000000 --- a/server/auth_test.go +++ /dev/null @@ -1,288 +0,0 @@ -package server - -import ( - "os" - "path/filepath" - "testing" -) - -func TestTokenStore_CreateAndValidate(t *testing.T) { - tmpDir := t.TempDir() - storePath := filepath.Join(tmpDir, "tokens.json") - - store, err := NewTokenStore(storePath) - if err != nil { - t.Fatalf("failed to create token store: %v", err) - } - - plainToken, err := store.Create("test-token") - if err != nil { - t.Fatalf("failed to create token: %v", err) - } - - if plainToken == "" { - t.Fatal("expected non-empty token") - } - - if len(plainToken) < 10 { - t.Fatalf("token too short: %s", plainToken) - } - - if plainToken[:3] != "sk_" { - t.Fatalf("token should start with 'sk_', got: %s", plainToken[:3]) - } - - record, valid := store.Validate(plainToken) - if !valid { - t.Fatal("expected token to be valid") - } - - if record == nil { - t.Fatal("expected record to be non-nil") - } - - if record.Name != "test-token" { - t.Fatalf("expected name 'test-token', got: %s", record.Name) - } -} - -func TestTokenStore_InvalidToken(t *testing.T) { - tmpDir := t.TempDir() - storePath := filepath.Join(tmpDir, "tokens.json") - - store, err := NewTokenStore(storePath) - if err != nil { - t.Fatalf("failed to create token store: %v", err) - } - - _, err = store.Create("test-token") - if err != nil { - t.Fatalf("failed to create token: %v", err) - } - - record, valid := store.Validate("sk_invalid_token_that_does_not_exist") - if valid { - t.Fatal("expected invalid token to fail validation") - } - - if record != nil { - t.Fatal("expected record to be nil for invalid token") - } - - record, valid = store.Validate("") - if valid { - t.Fatal("expected empty token to fail validation") - } -} - -func TestTokenStore_Revoke(t *testing.T) { - tmpDir := t.TempDir() - storePath := filepath.Join(tmpDir, "tokens.json") - - store, err := NewTokenStore(storePath) - if err != nil { - t.Fatalf("failed to create token store: %v", err) - } - - plainToken, err := store.Create("test-token") - if err != nil { - t.Fatalf("failed to create token: %v", err) - } - - _, valid := store.Validate(plainToken) - if !valid { - t.Fatal("expected token to be valid before revoke") - } - - err = store.Revoke("test-token") - if err != nil { - t.Fatalf("failed to revoke token: %v", err) - } - - _, valid = store.Validate(plainToken) - if valid { - t.Fatal("expected token to be invalid after revoke") - } -} - -func TestTokenStore_DuplicateName(t *testing.T) { - tmpDir := t.TempDir() - storePath := filepath.Join(tmpDir, "tokens.json") - - store, err := NewTokenStore(storePath) - if err != nil { - t.Fatalf("failed to create token store: %v", err) - } - - _, err = store.Create("test-token") - if err != nil { - t.Fatalf("failed to create first token: %v", err) - } - - _, err = store.Create("test-token") - if err == nil { - t.Fatal("expected error when creating duplicate token name") - } -} - -func TestTokenStore_Persistence(t *testing.T) { - tmpDir := t.TempDir() - storePath := filepath.Join(tmpDir, "tokens.json") - - store1, err := NewTokenStore(storePath) - if err != nil { - t.Fatalf("failed to create token store: %v", err) - } - - plainToken, err := store1.Create("persistent-token") - if err != nil { - t.Fatalf("failed to create token: %v", err) - } - - store2, err := NewTokenStore(storePath) - if err != nil { - t.Fatalf("failed to reload token store: %v", err) - } - - record, valid := store2.Validate(plainToken) - if !valid { - t.Fatal("expected token to be valid after reload") - } - - if record.Name != "persistent-token" { - t.Fatalf("expected name 'persistent-token', got: %s", record.Name) - } -} - -func TestTokenStore_DisabledWithEmptyPath(t *testing.T) { - store, err := NewTokenStore("") - if err != nil { - t.Fatalf("failed to create disabled token store: %v", err) - } - - if store.IsEnabled() { - t.Fatal("expected store to be disabled with empty path") - } - - _, valid := store.Validate("any-token") - if !valid { - t.Fatal("expected validation to pass when store is disabled") - } - - _, valid = store.Validate("") - if !valid { - t.Fatal("expected validation to pass for empty token when store is disabled") - } -} - -func TestTokenStore_List(t *testing.T) { - tmpDir := t.TempDir() - storePath := filepath.Join(tmpDir, "tokens.json") - - store, err := NewTokenStore(storePath) - if err != nil { - t.Fatalf("failed to create token store: %v", err) - } - - tokens := store.List() - if len(tokens) != 0 { - t.Fatalf("expected 0 tokens, got: %d", len(tokens)) - } - - _, err = store.Create("token-1") - if err != nil { - t.Fatalf("failed to create token-1: %v", err) - } - - _, err = store.Create("token-2") - if err != nil { - t.Fatalf("failed to create token-2: %v", err) - } - - tokens = store.List() - if len(tokens) != 2 { - t.Fatalf("expected 2 tokens, got: %d", len(tokens)) - } - - err = store.Revoke("token-1") - if err != nil { - t.Fatalf("failed to revoke token-1: %v", err) - } - - tokens = store.List() - if len(tokens) != 1 { - t.Fatalf("expected 1 token after revoke, got: %d", len(tokens)) - } - - if tokens[0].Name != "token-2" { - t.Fatalf("expected remaining token to be 'token-2', got: %s", tokens[0].Name) - } -} - -func TestHashToken(t *testing.T) { - token := "sk_test_token_12345" - hash1 := hashToken(token) - hash2 := hashToken(token) - - if hash1 != hash2 { - t.Fatal("expected same hash for same token") - } - - if len(hash1) != 64 { - t.Fatalf("expected 64 character hex hash, got: %d", len(hash1)) - } - - differentHash := hashToken("sk_different_token") - if hash1 == differentHash { - t.Fatal("expected different hash for different token") - } -} - -func TestGenerateSecureToken(t *testing.T) { - token1, err := generateSecureToken() - if err != nil { - t.Fatalf("failed to generate token: %v", err) - } - - token2, err := generateSecureToken() - if err != nil { - t.Fatalf("failed to generate second token: %v", err) - } - - if token1 == token2 { - t.Fatal("expected unique tokens") - } - - if token1[:3] != "sk_" { - t.Fatalf("token should start with 'sk_', got: %s", token1[:3]) - } - - if len(token1) < 40 { - t.Fatalf("token too short: %d characters", len(token1)) - } -} - -func TestTokenStore_FilePermissions(t *testing.T) { - tmpDir := t.TempDir() - storePath := filepath.Join(tmpDir, "tokens.json") - - store, err := NewTokenStore(storePath) - if err != nil { - t.Fatalf("failed to create token store: %v", err) - } - - _, err = store.Create("test-token") - if err != nil { - t.Fatalf("failed to create token: %v", err) - } - - info, err := os.Stat(storePath) - if err != nil { - t.Fatalf("failed to stat token file: %v", err) - } - - perm := info.Mode().Perm() - if perm != 0600 { - t.Fatalf("expected file permissions 0600, got: %o", perm) - } -} diff --git a/server/certs.go b/server/certs.go deleted file mode 100644 index 9311e8d..0000000 --- a/server/certs.go +++ /dev/null @@ -1,569 +0,0 @@ -package server - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/go-acme/lego/v4/certcrypto" - "github.com/go-acme/lego/v4/certificate" - "github.com/go-acme/lego/v4/challenge" - "github.com/go-acme/lego/v4/lego" - "github.com/go-acme/lego/v4/providers/dns" - "github.com/go-acme/lego/v4/registration" - lru "github.com/hashicorp/golang-lru/v2" - "github.com/karol-broda/funnel/shared" - "github.com/rs/zerolog" -) - -type ProviderConfig struct { - Providers []DNSProviderConfig `json:"providers"` -} - -type DNSProviderConfig struct { - Name string `json:"name"` - Provider string `json:"provider"` - Domains []string `json:"domains"` - Config map[string]string `json:"config"` -} - -type User struct { - Email string - Registration *registration.Resource - key crypto.PrivateKey -} - -func (u *User) GetEmail() string { - return u.Email -} - -func (u *User) GetRegistration() *registration.Resource { - return u.Registration -} - -func (u *User) GetPrivateKey() crypto.PrivateKey { - return u.key -} - -type ProviderMux struct { - providers map[string]challenge.Provider - logger zerolog.Logger -} - -type providerCreator func(config DNSProviderConfig) (challenge.Provider, error) - -var providerCreateMutex sync.Mutex - -func NewProviderMux(config *ProviderConfig) (*ProviderMux, error) { - creator := func(pconf DNSProviderConfig) (challenge.Provider, error) { - providerCreateMutex.Lock() - defer providerCreateMutex.Unlock() - - for key, value := range pconf.Config { - os.Setenv(key, value) - } - - defer func() { - for key := range pconf.Config { - os.Unsetenv(key) - } - }() - - return dns.NewDNSChallengeProviderByName(pconf.Provider) - } - return newProviderMuxWithCreator(config, creator) -} - -func newProviderMuxWithCreator(config *ProviderConfig, create providerCreator) (*ProviderMux, error) { - logger := shared.GetLogger("certs.provider-mux") - - mux := &ProviderMux{ - providers: make(map[string]challenge.Provider), - logger: logger, - } - - for _, pconf := range config.Providers { - logger.Info(). - Str("provider_name", pconf.Name). - Str("provider_type", pconf.Provider). - Strs("domains", pconf.Domains). - Msg("configuring DNS provider") - - provider, err := create(pconf) - if err != nil { - return nil, fmt.Errorf("failed to create provider %s: %w", pconf.Name, err) - } - - for _, domain := range pconf.Domains { - normDomain := strings.ToLower(strings.TrimSpace(domain)) - if strings.HasPrefix(normDomain, "*.") { - baseDomain := normDomain[2:] - mux.providers[baseDomain] = provider - mux.providers[normDomain] = provider - } else { - mux.providers[normDomain] = provider - } - - logger.Debug(). - Str("domain", normDomain). - Str("provider", pconf.Name). - Msg("mapped domain to provider") - } - } - - logger.Info(). - Int("total_providers", len(config.Providers)). - Int("total_domain_mappings", len(mux.providers)). - Msg("provider multiplexer initialized") - - return mux, nil -} - -func (m *ProviderMux) FindManagedDomain(domain string) (string, bool) { - if _, ok := m.providers[domain]; ok { - return domain, true - } - - parts := strings.Split(domain, ".") - for i := 1; i < len(parts)-1; i++ { - parentDomain := strings.Join(parts[i:], ".") - if _, ok := m.providers[parentDomain]; ok { - return parentDomain, true - } - wildcardDomain := "*." + parentDomain - if _, ok := m.providers[wildcardDomain]; ok { - return wildcardDomain, true - } - } - - return "", false -} - -func (m *ProviderMux) Present(domain, token, keyAuth string) error { - provider, err := m.findProvider(domain) - if err != nil { - m.logger.Error().Err(err).Str("domain", domain).Msg("failed to find provider for domain") - return err - } - - m.logger.Debug(). - Str("domain", domain). - Str("token", token[:8]+"..."). - Msg("presenting DNS challenge") - - return provider.Present(domain, token, keyAuth) -} - -func (m *ProviderMux) CleanUp(domain, token, keyAuth string) error { - provider, err := m.findProvider(domain) - if err != nil { - m.logger.Error().Err(err).Str("domain", domain).Msg("failed to find provider for domain") - return err - } - - m.logger.Debug(). - Str("domain", domain). - Str("token", token[:8]+"..."). - Msg("cleaning up DNS challenge") - - return provider.CleanUp(domain, token, keyAuth) -} - -func (m *ProviderMux) findProvider(domain string) (challenge.Provider, error) { - challengeDomain := domain - prefix := "_acme-challenge." - if strings.HasPrefix(domain, prefix) { - challengeDomain = domain[len(prefix):] - } - - if provider, ok := m.providers[challengeDomain]; ok { - return provider, nil - } - - wildcardDomain := "*." + challengeDomain - if provider, ok := m.providers[wildcardDomain]; ok { - return provider, nil - } - - parts := strings.Split(challengeDomain, ".") - for i := 1; i < len(parts); i++ { - parentDomain := strings.Join(parts[i:], ".") - if provider, ok := m.providers[parentDomain]; ok { - return provider, nil - } - - wildcardParent := "*." + parentDomain - if provider, ok := m.providers[wildcardParent]; ok { - return provider, nil - } - } - - return nil, fmt.Errorf("no DNS provider found for domain: %s", domain) -} - -type CertificateManager struct { - user *User - client *lego.Client - providerMux *ProviderMux - certificates *lru.Cache[string, *tls.Certificate] - certMutex sync.Mutex - certDir string - logger zerolog.Logger - providerConfig *ProviderConfig -} - -func NewCertificateManager(email, certDir, configPath string) (*CertificateManager, error) { - const lruCacheSize = 512 - logger := shared.GetLogger("certs.manager") - - config, err := loadProviderConfig(configPath) - if err != nil { - return nil, fmt.Errorf("failed to load provider config: %w", err) - } - - providerMux, err := NewProviderMux(config) - if err != nil { - return nil, fmt.Errorf("failed to create provider multiplexer: %w", err) - } - - if err := os.MkdirAll(certDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create certificate directory: %w", err) - } - - certCache, err := lru.New[string, *tls.Certificate](lruCacheSize) - if err != nil { - return nil, fmt.Errorf("failed to create lru cache: %w", err) - } - - keyPath := filepath.Join(certDir, "account.key") - var privateKey crypto.PrivateKey - keyBytes, err := os.ReadFile(keyPath) - if err != nil { - if os.IsNotExist(err) { - logger.Info().Str("path", keyPath).Msg("no user private key found, generating a new one") - newPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, fmt.Errorf("failed to generate private key: %w", err) - } - privateKey = newPrivateKey - - pemKeyBytes, err := x509.MarshalECPrivateKey(newPrivateKey) - if err != nil { - return nil, fmt.Errorf("failed to marshal private key: %w", err) - } - pemKey := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: pemKeyBytes}) - if err := os.WriteFile(keyPath, pemKey, 0600); err != nil { - return nil, fmt.Errorf("failed to save private key: %w", err) - } - } else { - return nil, fmt.Errorf("failed to read private key: %w", err) - } - } else { - logger.Info().Str("path", keyPath).Msg("loading existing user private key") - block, _ := pem.Decode(keyBytes) - if block == nil { - return nil, fmt.Errorf("failed to decode pem block from private key") - } - p, err := x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse private key: %w", err) - } - privateKey = p - } - - acmeUser := &User{Email: email, key: privateKey} - - regPath := filepath.Join(certDir, "account.json") - regBytes, err := os.ReadFile(regPath) - if err != nil { - if os.IsNotExist(err) { - logger.Info().Msg("no registration found, creating new one") - tempConfig := lego.NewConfig(acmeUser) - tempConfig.CADirURL = lego.LEDirectoryProduction - tempConfig.Certificate.KeyType = certcrypto.EC256 - tempClient, err := lego.NewClient(tempConfig) - if err != nil { - return nil, fmt.Errorf("failed to create temp lego client: %w", err) - } - - reg, err := tempClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - if err != nil { - return nil, fmt.Errorf("failed to register user: %w", err) - } - acmeUser.Registration = reg - - regJSON, err := json.MarshalIndent(reg, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal registration: %w", err) - } - if err := os.WriteFile(regPath, regJSON, 0600); err != nil { - return nil, fmt.Errorf("failed to save registration: %w", err) - } - logger.Info().Str("uri", reg.URI).Msg("new user registered") - } else { - return nil, fmt.Errorf("failed to read registration file: %w", err) - } - } else { - var reg registration.Resource - if err := json.Unmarshal(regBytes, ®); err != nil { - return nil, fmt.Errorf("failed to unmarshal registration file: %w", err) - } - acmeUser.Registration = ® - logger.Info().Str("uri", reg.URI).Msg("user registration loaded from disk") - } - - finalConfig := lego.NewConfig(acmeUser) - finalConfig.Certificate.KeyType = certcrypto.EC256 - finalConfig.CADirURL = lego.LEDirectoryProduction - - client, err := lego.NewClient(finalConfig) - if err != nil { - return nil, fmt.Errorf("failed to create lego client: %w", err) - } - - err = client.Challenge.SetDNS01Provider(providerMux) - if err != nil { - return nil, fmt.Errorf("failed to set dns-01 provider: %w", err) - } - - cm := &CertificateManager{ - user: acmeUser, - client: client, - providerMux: providerMux, - certificates: certCache, - certDir: certDir, - logger: logger, - providerConfig: config, - } - - logger.Info(). - Str("email", email). - Str("cert_dir", certDir). - Str("ca_url", finalConfig.CADirURL). - Msg("certificate manager initialized") - - return cm, nil -} - -func (cm *CertificateManager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { - domain := strings.ToLower(hello.ServerName) - if domain == "" { - cm.logger.Warn().Msg("missing server name in tls handshake (sni), aborting handshake") - return nil, nil - } - - baseDomain, ok := cm.providerMux.FindManagedDomain(domain) - if !ok { - return nil, fmt.Errorf("domain %q is not configured for management", domain) - } - cm.logger.Debug().Str("domain", domain).Str("base_domain", baseDomain).Msg("certificate requested") - - cert, ok := cm.certificates.Get(baseDomain) - if ok && cm.isCertificateValid(cert) { - cm.logger.Debug().Str("domain", domain).Msg("using cached certificate (in-memory)") - return cert, nil - } - - cert, err := cm.loadCertificateFromDisk(baseDomain) - if err == nil && cm.isCertificateValid(cert) { - cm.logger.Info().Str("base_domain", baseDomain).Msg("loaded valid certificate from disk") - cm.certificates.Add(baseDomain, cert) - return cert, nil - } - - cm.logger.Info().Str("domain", domain).Msg("no valid certificate found in cache or on disk, obtaining new one") - return cm.obtainCertificate(baseDomain) -} - -func (cm *CertificateManager) obtainCertificate(baseDomain string) (*tls.Certificate, error) { - cm.certMutex.Lock() - defer cm.certMutex.Unlock() - - if cert, ok := cm.certificates.Get(baseDomain); ok && cm.isCertificateValid(cert) { - cm.logger.Debug().Str("domain", baseDomain).Msg("certificate was obtained by another goroutine while waiting for lock") - return cert, nil - } - - var domains []string - if strings.HasPrefix(baseDomain, "*.") { - domains = []string{baseDomain, strings.TrimPrefix(baseDomain, "*.")} - } else { - domains = []string{baseDomain} - } - - cm.logger.Info().Strs("domains", domains).Msg("requesting new certificate from let's encrypt") - - request := certificate.ObtainRequest{ - Domains: domains, - Bundle: true, - } - - resource, err := cm.client.Certificate.Obtain(request) - if err != nil { - return nil, fmt.Errorf("failed to obtain certificate: %w", err) - } - - cm.saveCertificateToDisk(baseDomain, resource) - - tlsCert, err := tls.X509KeyPair(resource.Certificate, resource.PrivateKey) - if err != nil { - return nil, fmt.Errorf("failed to create tls key pair from obtained certificate: %w", err) - } - - if len(tlsCert.Certificate) > 0 { - x509Cert, err := x509.ParseCertificate(tlsCert.Certificate[0]) - if err != nil { - return nil, fmt.Errorf("failed to parse obtained certificate leaf: %w", err) - } - tlsCert.Leaf = x509Cert - } - - cm.certificates.Add(baseDomain, &tlsCert) - - cm.logger.Info().Str("domain", baseDomain).Time("expiry", tlsCert.Leaf.NotAfter).Msg("successfully obtained and cached certificate") - return &tlsCert, nil -} - -func (cm *CertificateManager) saveCertificateToDisk(baseDomain string, resource *certificate.Resource) { - certPath := filepath.Join(cm.certDir, baseDomain+".crt") - keyPath := filepath.Join(cm.certDir, baseDomain+".key") - - err := os.WriteFile(certPath, resource.Certificate, 0644) - if err != nil { - cm.logger.Error().Err(err).Str("path", certPath).Msg("failed to save certificate to disk") - } - - err = os.WriteFile(keyPath, resource.PrivateKey, 0600) - if err != nil { - cm.logger.Error().Err(err).Str("path", keyPath).Msg("failed to save private key to disk") - } - - cm.logger.Info(). - Str("domain", baseDomain). - Str("cert_path", certPath). - Str("key_path", keyPath). - Msg("certificate saved to disk") -} - -func (cm *CertificateManager) isCertificateValid(cert *tls.Certificate) bool { - if len(cert.Certificate) == 0 { - return false - } - - x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) - if err != nil { - cm.logger.Error().Err(err).Msg("failed to parse certificate") - return false - } - - expirationThreshold := time.Now().Add(30 * 24 * time.Hour) - - if time.Now().After(x509Cert.NotAfter) { - cm.logger.Debug().Time("expiry", x509Cert.NotAfter).Msg("certificate is expired") - return false - } - - if x509Cert.NotAfter.Before(expirationThreshold) { - cm.logger.Debug().Time("expiry", x509Cert.NotAfter).Msg("certificate expires soon") - return false - } - - return true -} - -func (cm *CertificateManager) PreloadCertificates() error { - cm.logger.Info().Msg("starting certificate preload") - - managedDomains := make(map[string]struct{}) - for _, p := range cm.providerConfig.Providers { - for _, d := range p.Domains { - managedDomains[strings.ToLower(strings.TrimSpace(d))] = struct{}{} - } - } - - for domain := range managedDomains { - cm.logger.Info().Str("domain", domain).Msg("checking certificate during preload") - - cert, ok := cm.certificates.Get(domain) - if ok && cm.isCertificateValid(cert) { - cm.logger.Info().Str("domain", domain).Msg("certificate is already in cache and valid") - continue - } - - cert, err := cm.loadCertificateFromDisk(domain) - if err == nil && cm.isCertificateValid(cert) { - cm.logger.Info().Str("domain", domain).Msg("loaded valid certificate from disk into cache") - cm.certificates.Add(domain, cert) - continue - } - - cm.logger.Info().Str("domain", domain).Msg("no valid certificate on disk, obtaining new one") - _, err = cm.obtainCertificate(domain) - if err != nil { - cm.logger.Error().Err(err).Str("domain", domain).Msg("failed to obtain certificate during preload") - return fmt.Errorf("failed to obtain certificate for %s: %w", domain, err) - } else { - cm.logger.Info().Str("domain", domain).Msg("successfully obtained and cached certificate") - } - } - - cm.logger.Info().Msg("certificate preload finished successfully") - return nil -} - -func loadProviderConfig(configPath string) (*ProviderConfig, error) { - logger := shared.GetLogger("certs.config") - data, err := os.ReadFile(configPath) - if err != nil { - return nil, fmt.Errorf("failed to read config file: %w", err) - } - - var config ProviderConfig - if err := json.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("failed to parse config JSON: %w", err) - } - - logger.Info().Str("path", configPath).Int("providers", len(config.Providers)).Msg("loaded DNS provider config") - return &config, nil -} - -func (cm *CertificateManager) loadCertificateFromDisk(baseDomain string) (*tls.Certificate, error) { - certPath := filepath.Join(cm.certDir, baseDomain+".crt") - keyPath := filepath.Join(cm.certDir, baseDomain+".key") - - certBytes, err := os.ReadFile(certPath) - if err != nil { - return nil, err - } - - keyBytes, err := os.ReadFile(keyPath) - if err != nil { - return nil, err - } - - tlsCert, err := tls.X509KeyPair(certBytes, keyBytes) - if err != nil { - return nil, err - } - - if len(tlsCert.Certificate) > 0 { - x509Cert, err := x509.ParseCertificate(tlsCert.Certificate[0]) - if err != nil { - return nil, fmt.Errorf("failed to parse certificate to get leaf: %w", err) - } - tlsCert.Leaf = x509Cert - } - - return &tlsCert, nil -} diff --git a/server/certs_test.go b/server/certs_test.go deleted file mode 100644 index 4558d4f..0000000 --- a/server/certs_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package server - -import ( - "fmt" - "testing" - - "github.com/go-acme/lego/v4/challenge" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type mockProvider struct { - name string - presentFn func(domain, token, keyAuth string) error - cleanUpFn func(domain, token, keyAuth string) error -} - -func (m *mockProvider) Present(domain, token, keyAuth string) error { - if m.presentFn != nil { - return m.presentFn(domain, token, keyAuth) - } - return nil -} - -func (m *mockProvider) CleanUp(domain, token, keyAuth string) error { - if m.cleanUpFn != nil { - return m.cleanUpFn(domain, token, keyAuth) - } - return nil -} - -func TestNewProviderMux(t *testing.T) { - mockProviderCreator := func(pconf DNSProviderConfig) (challenge.Provider, error) { - if pconf.Provider == "unsupported" { - return nil, fmt.Errorf("unsupported DNS provider: %s", pconf.Provider) - } - return &mockProvider{name: pconf.Name}, nil - } - - tests := []struct { - name string - config *ProviderConfig - expectedError string - validate func(t *testing.T, mux *ProviderMux) - }{ - { - name: "valid config with multiple providers and domains", - config: &ProviderConfig{ - Providers: []DNSProviderConfig{ - { - Name: "aws-prod", - Provider: "route53", - Domains: []string{"example.com", "*.example.com"}, - Config: map[string]string{"AWS_REGION": "us-east-1"}, - }, - { - Name: "cloudflare-main", - Provider: "cloudflare", - Domains: []string{"myapp.org", "*.myapp.org"}, - Config: map[string]string{"CLOUDFLARE_DNS_API_TOKEN": "secret"}, - }, - }, - }, - validate: func(t *testing.T, mux *ProviderMux) { - require.NotNil(t, mux) - assert.Equal(t, 4, len(mux.providers)) - - p1 := mux.providers["example.com"] - p2 := mux.providers["*.example.com"] - p3 := mux.providers["myapp.org"] - p4 := mux.providers["*.myapp.org"] - - require.NotNil(t, p1) - require.NotNil(t, p2) - require.NotNil(t, p3) - require.NotNil(t, p4) - - assert.True(t, p1 == p2, "Expected same provider instance for example.com and *.example.com") - assert.True(t, p3 == p4, "Expected same provider instance for myapp.org and *.myapp.org") - assert.False(t, p1 == p3, "Expected different provider instances for different providers") - - assert.Equal(t, "aws-prod", p1.(*mockProvider).name) - assert.Equal(t, "cloudflare-main", p3.(*mockProvider).name) - }, - }, - { - name: "unsupported provider", - config: &ProviderConfig{ - Providers: []DNSProviderConfig{ - { - Name: "unsupported", - Provider: "unsupported", - Domains: []string{"test.com"}, - }, - }, - }, - expectedError: "unsupported DNS provider: unsupported", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mux, err := newProviderMuxWithCreator(tc.config, mockProviderCreator) - - if tc.expectedError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedError) - assert.Nil(t, mux) - } else { - require.NoError(t, err) - tc.validate(t, mux) - } - }) - } -} - -func TestProviderMux_FindProvider(t *testing.T) { - mockProviderCreator := func(pconf DNSProviderConfig) (challenge.Provider, error) { - return &mockProvider{name: pconf.Name}, nil - } - - config := &ProviderConfig{ - Providers: []DNSProviderConfig{ - { - Name: "aws-prod", - Provider: "route53", - Domains: []string{"example.com", "*.example.com"}, - }, - { - Name: "cloudflare-main", - Provider: "cloudflare", - Domains: []string{"myapp.org"}, - }, - { - Name: "aws-staging", - Provider: "route53", - Domains: []string{"staging.example.com", "*.staging.example.com"}, - }, - }, - } - - mux, err := newProviderMuxWithCreator(config, mockProviderCreator) - require.NoError(t, err) - require.NotNil(t, mux) - - tests := []struct { - domain string - expectedProvider string - expectError bool - }{ - {domain: "example.com", expectedProvider: "aws-prod"}, - {domain: "www.example.com", expectedProvider: "aws-prod"}, - {domain: "test.example.com", expectedProvider: "aws-prod"}, - {domain: "_acme-challenge.example.com", expectedProvider: "aws-prod"}, - {domain: "_acme-challenge.www.example.com", expectedProvider: "aws-prod"}, - {domain: "myapp.org", expectedProvider: "cloudflare-main"}, - {domain: "staging.example.com", expectedProvider: "aws-staging"}, - {domain: "api.staging.example.com", expectedProvider: "aws-staging"}, - {domain: "unknown.com", expectError: true}, - {domain: "example.org", expectError: true}, - } - - for _, tc := range tests { - t.Run(tc.domain, func(t *testing.T) { - provider, err := mux.findProvider(tc.domain) - if tc.expectError { - require.Error(t, err) - assert.Nil(t, provider) - } else { - require.NoError(t, err) - require.NotNil(t, provider) - assert.Equal(t, tc.expectedProvider, provider.(*mockProvider).name) - } - }) - } -} diff --git a/server/docs/docs.go b/server/docs/docs.go deleted file mode 100644 index 2eaf2bf..0000000 --- a/server/docs/docs.go +++ /dev/null @@ -1,972 +0,0 @@ -// Package docs Code generated by swaggo/swag. DO NOT EDIT -package docs - -import "github.com/swaggo/swag" - -const docTemplate = `{ - "schemes": {{ marshal .Schemes }}, - "swagger": "2.0", - "info": { - "description": "{{escape .Description}}", - "title": "{{.Title}}", - "contact": { - "name": "Funnel Support", - "url": "https://github.com/karol-broda/funnel" - }, - "license": { - "name": "MIT", - "url": "https://github.com/karol-broda/funnel/blob/master/LICENSE.md" - }, - "version": "{{.Version}}" - }, - "host": "{{.Host}}", - "basePath": "{{.BasePath}}", - "paths": { - "/health": { - "get": { - "description": "Returns the health status of the server", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Server" - ], - "summary": "Health check", - "responses": { - "200": { - "description": "Server is healthy", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.HealthData" - } - } - } - ] - } - } - } - } - }, - "/metrics": { - "get": { - "description": "Returns detailed server-wide metrics including performance, resource usage, and aggregated statistics", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Server" - ], - "summary": "Get comprehensive server metrics", - "responses": { - "200": { - "description": "Comprehensive server metrics", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.ServerMetricsResponse" - } - } - } - ] - } - } - } - } - }, - "/metrics/historical": { - "get": { - "description": "Returns time-series data for server-wide metrics over a specified time range", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Server" - ], - "summary": "Get historical server metrics", - "parameters": [ - { - "type": "string", - "default": "24h", - "description": "Time range (1h, 24h, 7d)", - "name": "range", - "in": "query" - }, - { - "type": "string", - "default": "5m", - "description": "Data interval (1m, 5m, 1h)", - "name": "interval", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Historical server metrics", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.HistoricalMetricsResponse" - } - } - } - ] - } - } - } - } - }, - "/server/stats": { - "get": { - "description": "Returns comprehensive server-wide statistics including tunnel count, request metrics, and cache performance", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Server" - ], - "summary": "Get server statistics", - "responses": { - "200": { - "description": "Server statistics retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.ServerStats" - } - } - } - ] - } - } - } - } - }, - "/tunnels": { - "get": { - "description": "Returns a list of all currently active tunnel connections", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "List active tunnels", - "responses": { - "200": { - "description": "List of active tunnels", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/server.TunnelInfo" - } - } - } - } - ] - } - } - } - } - }, - "/tunnels/{tunnelId}": { - "get": { - "description": "Returns detailed information about a specific tunnel", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Get tunnel details", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID (3-63 characters, lowercase letters, numbers, and hyphens only)", - "name": "tunnelId", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Tunnel details retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.TunnelInfo" - } - } - } - ] - } - }, - "400": { - "description": "Invalid tunnel ID format", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - }, - "delete": { - "description": "Forcibly closes a tunnel connection", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Delete tunnel", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID to delete", - "name": "tunnelId", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Tunnel deleted successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.TunnelDeleteData" - } - } - } - ] - } - }, - "400": { - "description": "Invalid tunnel ID format", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - } - }, - "/tunnels/{tunnelId}/historical": { - "get": { - "description": "Returns time-series data for a specific tunnel over a specified time range", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Get tunnel historical data", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID", - "name": "tunnelId", - "in": "path", - "required": true - }, - { - "type": "string", - "default": "24h", - "description": "Time range (1h, 24h, 7d)", - "name": "range", - "in": "query" - }, - { - "type": "string", - "description": "Start time (RFC3339 format)", - "name": "start", - "in": "query" - }, - { - "type": "string", - "description": "End time (RFC3339 format)", - "name": "end", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Tunnel historical data", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.HistoricalMetricsResponse" - } - } - } - ] - } - }, - "400": { - "description": "Invalid parameters", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - } - }, - "/tunnels/{tunnelId}/metrics": { - "get": { - "description": "Returns detailed performance and usage metrics for a specific tunnel", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Get comprehensive tunnel metrics", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID", - "name": "tunnelId", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Comprehensive tunnel metrics", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.DetailedTunnelMetrics" - } - } - } - ] - } - }, - "400": { - "description": "Invalid tunnel ID format", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - } - }, - "/tunnels/{tunnelId}/stats": { - "get": { - "description": "Returns detailed statistics and metrics for a specific tunnel", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Get tunnel statistics", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID", - "name": "tunnelId", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Tunnel statistics retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.TunnelStats" - } - } - } - ] - } - }, - "400": { - "description": "Invalid tunnel ID format", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - } - } - }, - "definitions": { - "server.APIResponse": { - "type": "object", - "properties": { - "data": { - "description": "Response data (present on success)" - }, - "error": { - "description": "Error message (present on failure)", - "type": "string", - "example": "error message" - }, - "success": { - "description": "Indicates if the request was successful", - "type": "boolean", - "example": true - } - } - }, - "server.DetailedTunnelMetrics": { - "type": "object", - "properties": { - "average_response_time_ms": { - "description": "Average response time in ms", - "type": "number", - "example": 25.5 - }, - "client_ip": { - "description": "Client IP address", - "type": "string", - "example": "192.168.1.100:52341" - }, - "connection_errors": { - "description": "Connection errors", - "type": "integer", - "example": 2 - }, - "connections_active": { - "description": "Active connections", - "type": "integer", - "example": 1 - }, - "connections_total": { - "description": "Total connections made", - "type": "integer", - "example": 3 - }, - "created_at": { - "description": "Creation time", - "type": "string", - "example": "2025-08-08T18:30:00Z" - }, - "data_errors": { - "description": "Data errors", - "type": "integer", - "example": 1 - }, - "last_activity": { - "description": "Last activity time", - "type": "string", - "example": "2025-08-08T20:30:00Z" - }, - "max_response_time_ms": { - "description": "Maximum response time in ms", - "type": "number", - "example": 150.8 - }, - "min_response_time_ms": { - "description": "Minimum response time in ms", - "type": "number", - "example": 5.2 - }, - "reconnect_count": { - "description": "Number of reconnections", - "type": "integer", - "example": 2 - }, - "requests_per_second": { - "description": "Requests per second", - "type": "number", - "example": 12.4 - }, - "timeout_count": { - "description": "Number of timeouts", - "type": "integer", - "example": 5 - }, - "total_bytes_in": { - "description": "Total bytes received", - "type": "integer", - "example": 125430 - }, - "total_bytes_out": { - "description": "Total bytes sent", - "type": "integer", - "example": 987650 - }, - "total_requests": { - "description": "Total requests processed", - "type": "integer", - "example": 1542 - }, - "total_requests_error": { - "description": "Failed requests", - "type": "integer", - "example": 44 - }, - "total_requests_success": { - "description": "Successful requests", - "type": "integer", - "example": 1498 - }, - "tunnel_id": { - "description": "Tunnel identifier", - "type": "string", - "example": "my-tunnel" - }, - "user_agent": { - "description": "Client user agent", - "type": "string", - "example": "FunnelClient/1.0" - } - } - }, - "server.HealthData": { - "type": "object", - "properties": { - "status": { - "description": "Service status", - "type": "string", - "example": "healthy" - }, - "timestamp": { - "description": "Current timestamp", - "type": "string", - "example": "2025-08-08T20:11:00Z" - }, - "uptime": { - "description": "Server uptime duration", - "type": "string", - "example": "6.4281425s" - } - } - }, - "server.HistoricalDataPoint": { - "type": "object", - "properties": { - "active_connections": { - "type": "integer" - }, - "bytes_in": { - "type": "integer" - }, - "bytes_out": { - "type": "integer" - }, - "error_count": { - "type": "integer" - }, - "request_count": { - "type": "integer" - }, - "response_time_ms": { - "type": "number" - }, - "timestamp": { - "type": "string" - } - } - }, - "server.HistoricalMetricsResponse": { - "type": "object", - "properties": { - "data_points": { - "description": "Historical data points", - "type": "array", - "items": { - "$ref": "#/definitions/server.HistoricalDataPoint" - } - }, - "interval": { - "description": "Data point interval", - "type": "string", - "example": "1m" - }, - "time_range": { - "description": "Time range of data", - "type": "string", - "example": "24h" - }, - "tunnel_id": { - "description": "Tunnel identifier", - "type": "string", - "example": "my-tunnel" - } - } - }, - "server.ServerMetricsResponse": { - "type": "object", - "properties": { - "active_tunnels": { - "description": "Currently active tunnels", - "type": "integer", - "example": 8 - }, - "average_response_time_ms": { - "description": "Server average response time", - "type": "number", - "example": 28.7 - }, - "cpu_usage_percent": { - "description": "CPU usage percentage", - "type": "number", - "example": 15.5 - }, - "error_rate_percent": { - "description": "Error rate percentage", - "type": "number", - "example": 0.18 - }, - "memory_usage_bytes": { - "description": "Memory usage in bytes", - "type": "integer", - "example": 67108864 - }, - "network_connections": { - "description": "Active network connections", - "type": "integer", - "example": 12 - }, - "server_requests_per_second": { - "description": "Server-wide RPS", - "type": "number", - "example": 45.2 - }, - "server_uptime": { - "description": "Server uptime", - "type": "string", - "example": "5h23m15s" - }, - "total_bytes_transferred": { - "description": "Total bytes transferred", - "type": "integer", - "example": 5670000 - }, - "total_errors": { - "description": "Total errors", - "type": "integer", - "example": 234 - }, - "total_requests": { - "description": "Total requests processed", - "type": "integer", - "example": 125430 - }, - "total_tunnels": { - "description": "Total tunnels created", - "type": "integer", - "example": 15 - } - } - }, - "server.ServerStats": { - "type": "object", - "properties": { - "active_tunnels": { - "description": "Number of currently active tunnel connections", - "type": "integer", - "example": 2 - }, - "cache_hit_rate": { - "description": "Cache hit rate percentage", - "type": "string", - "example": "90.9%" - }, - "cache_hits": { - "description": "Number of cache hits for hostname resolution", - "type": "integer", - "example": 1401 - }, - "cache_misses": { - "description": "Number of cache misses for hostname resolution", - "type": "integer", - "example": 141 - }, - "router_stats": { - "description": "Detailed router statistics", - "type": "object", - "additionalProperties": true - }, - "total_requests": { - "description": "Total number of HTTP requests processed", - "type": "integer", - "example": 1542 - }, - "uptime": { - "description": "Server uptime duration", - "type": "string", - "example": "2h15m30s" - } - } - }, - "server.TunnelDeleteData": { - "type": "object", - "properties": { - "message": { - "description": "Success message", - "type": "string", - "example": "tunnel deleted successfully" - }, - "tunnel_id": { - "description": "Deleted tunnel ID", - "type": "string", - "example": "my-tunnel" - } - } - }, - "server.TunnelInfo": { - "type": "object", - "properties": { - "bytes_received": { - "description": "Total bytes received from client", - "type": "integer", - "example": 12543 - }, - "bytes_sent": { - "description": "Total bytes sent to client", - "type": "integer", - "example": 98765 - }, - "created_at": { - "description": "Tunnel creation timestamp", - "type": "string", - "example": "2025-08-08T18:30:00Z" - }, - "id": { - "description": "Unique tunnel identifier", - "type": "string", - "example": "my-tunnel" - }, - "messages_received": { - "description": "Number of messages received from client", - "type": "integer", - "example": 45 - }, - "messages_sent": { - "description": "Number of messages sent to client", - "type": "integer", - "example": 45 - }, - "status": { - "description": "Current tunnel status", - "type": "string", - "enum": [ - "connected", - "disconnected" - ], - "example": "connected" - }, - "uptime": { - "description": "Tunnel connection uptime", - "type": "string", - "example": "1h15m30s" - } - } - }, - "server.TunnelStats": { - "type": "object", - "properties": { - "bytes_received": { - "description": "Bytes received total", - "type": "integer", - "example": 12543 - }, - "bytes_sent": { - "description": "Bytes sent total", - "type": "integer", - "example": 98765 - }, - "connection_info": { - "description": "Connection details", - "type": "object", - "additionalProperties": true - }, - "created_at": { - "description": "Creation timestamp", - "type": "string", - "example": "2025-08-08T18:30:00Z" - }, - "messages_received": { - "description": "Messages received count", - "type": "integer", - "example": 45 - }, - "messages_sent": { - "description": "Messages sent count", - "type": "integer", - "example": 45 - }, - "status": { - "description": "Connection status", - "type": "string", - "example": "connected" - }, - "tunnel_id": { - "description": "Tunnel identifier", - "type": "string", - "example": "my-tunnel" - }, - "uptime": { - "description": "Connection uptime", - "type": "string", - "example": "1h15m30s" - } - } - } - }, - "tags": [ - { - "description": "Server health and statistics endpoints", - "name": "Server" - }, - { - "description": "Tunnel management and monitoring endpoints", - "name": "Tunnels" - } - ] -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "1.0", - Host: "localhost:8080", - BasePath: "/api", - Schemes: []string{"http", "https"}, - Title: "Funnel Server API", - Description: "REST API for tunnel management and monitoring in the funnel tunneling solution\n\nThis API provides endpoints for:\n- Health checking\n- Server statistics and monitoring\n- Tunnel management (list, view, delete)\n- Individual tunnel statistics", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/server/docs/swagger.json b/server/docs/swagger.json deleted file mode 100644 index 153b88d..0000000 --- a/server/docs/swagger.json +++ /dev/null @@ -1,952 +0,0 @@ -{ - "schemes": [ - "http", - "https" - ], - "swagger": "2.0", - "info": { - "description": "REST API for tunnel management and monitoring in the funnel tunneling solution\n\nThis API provides endpoints for:\n- Health checking\n- Server statistics and monitoring\n- Tunnel management (list, view, delete)\n- Individual tunnel statistics", - "title": "Funnel Server API", - "contact": { - "name": "Funnel Support", - "url": "https://github.com/karol-broda/funnel" - }, - "license": { - "name": "MIT", - "url": "https://github.com/karol-broda/funnel/blob/master/LICENSE.md" - }, - "version": "1.0" - }, - "host": "localhost:8080", - "basePath": "/api", - "paths": { - "/health": { - "get": { - "description": "Returns the health status of the server", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Server" - ], - "summary": "Health check", - "responses": { - "200": { - "description": "Server is healthy", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.HealthData" - } - } - } - ] - } - } - } - } - }, - "/metrics": { - "get": { - "description": "Returns detailed server-wide metrics including performance, resource usage, and aggregated statistics", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Server" - ], - "summary": "Get comprehensive server metrics", - "responses": { - "200": { - "description": "Comprehensive server metrics", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.ServerMetricsResponse" - } - } - } - ] - } - } - } - } - }, - "/metrics/historical": { - "get": { - "description": "Returns time-series data for server-wide metrics over a specified time range", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Server" - ], - "summary": "Get historical server metrics", - "parameters": [ - { - "type": "string", - "default": "24h", - "description": "Time range (1h, 24h, 7d)", - "name": "range", - "in": "query" - }, - { - "type": "string", - "default": "5m", - "description": "Data interval (1m, 5m, 1h)", - "name": "interval", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Historical server metrics", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.HistoricalMetricsResponse" - } - } - } - ] - } - } - } - } - }, - "/server/stats": { - "get": { - "description": "Returns comprehensive server-wide statistics including tunnel count, request metrics, and cache performance", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Server" - ], - "summary": "Get server statistics", - "responses": { - "200": { - "description": "Server statistics retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.ServerStats" - } - } - } - ] - } - } - } - } - }, - "/tunnels": { - "get": { - "description": "Returns a list of all currently active tunnel connections", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "List active tunnels", - "responses": { - "200": { - "description": "List of active tunnels", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/server.TunnelInfo" - } - } - } - } - ] - } - } - } - } - }, - "/tunnels/{tunnelId}": { - "get": { - "description": "Returns detailed information about a specific tunnel", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Get tunnel details", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID (3-63 characters, lowercase letters, numbers, and hyphens only)", - "name": "tunnelId", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Tunnel details retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.TunnelInfo" - } - } - } - ] - } - }, - "400": { - "description": "Invalid tunnel ID format", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - }, - "delete": { - "description": "Forcibly closes a tunnel connection", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Delete tunnel", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID to delete", - "name": "tunnelId", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Tunnel deleted successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.TunnelDeleteData" - } - } - } - ] - } - }, - "400": { - "description": "Invalid tunnel ID format", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - } - }, - "/tunnels/{tunnelId}/historical": { - "get": { - "description": "Returns time-series data for a specific tunnel over a specified time range", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Get tunnel historical data", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID", - "name": "tunnelId", - "in": "path", - "required": true - }, - { - "type": "string", - "default": "24h", - "description": "Time range (1h, 24h, 7d)", - "name": "range", - "in": "query" - }, - { - "type": "string", - "description": "Start time (RFC3339 format)", - "name": "start", - "in": "query" - }, - { - "type": "string", - "description": "End time (RFC3339 format)", - "name": "end", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Tunnel historical data", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.HistoricalMetricsResponse" - } - } - } - ] - } - }, - "400": { - "description": "Invalid parameters", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - } - }, - "/tunnels/{tunnelId}/metrics": { - "get": { - "description": "Returns detailed performance and usage metrics for a specific tunnel", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Get comprehensive tunnel metrics", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID", - "name": "tunnelId", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Comprehensive tunnel metrics", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.DetailedTunnelMetrics" - } - } - } - ] - } - }, - "400": { - "description": "Invalid tunnel ID format", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - } - }, - "/tunnels/{tunnelId}/stats": { - "get": { - "description": "Returns detailed statistics and metrics for a specific tunnel", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Tunnels" - ], - "summary": "Get tunnel statistics", - "parameters": [ - { - "maxLength": 63, - "minLength": 3, - "type": "string", - "description": "Tunnel ID", - "name": "tunnelId", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Tunnel statistics retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/server.TunnelStats" - } - } - } - ] - } - }, - "400": { - "description": "Invalid tunnel ID format", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - }, - "404": { - "description": "Tunnel not found", - "schema": { - "$ref": "#/definitions/server.APIResponse" - } - } - } - } - } - }, - "definitions": { - "server.APIResponse": { - "type": "object", - "properties": { - "data": { - "description": "Response data (present on success)" - }, - "error": { - "description": "Error message (present on failure)", - "type": "string", - "example": "error message" - }, - "success": { - "description": "Indicates if the request was successful", - "type": "boolean", - "example": true - } - } - }, - "server.DetailedTunnelMetrics": { - "type": "object", - "properties": { - "average_response_time_ms": { - "description": "Average response time in ms", - "type": "number", - "example": 25.5 - }, - "client_ip": { - "description": "Client IP address", - "type": "string", - "example": "192.168.1.100:52341" - }, - "connection_errors": { - "description": "Connection errors", - "type": "integer", - "example": 2 - }, - "connections_active": { - "description": "Active connections", - "type": "integer", - "example": 1 - }, - "connections_total": { - "description": "Total connections made", - "type": "integer", - "example": 3 - }, - "created_at": { - "description": "Creation time", - "type": "string", - "example": "2025-08-08T18:30:00Z" - }, - "data_errors": { - "description": "Data errors", - "type": "integer", - "example": 1 - }, - "last_activity": { - "description": "Last activity time", - "type": "string", - "example": "2025-08-08T20:30:00Z" - }, - "max_response_time_ms": { - "description": "Maximum response time in ms", - "type": "number", - "example": 150.8 - }, - "min_response_time_ms": { - "description": "Minimum response time in ms", - "type": "number", - "example": 5.2 - }, - "reconnect_count": { - "description": "Number of reconnections", - "type": "integer", - "example": 2 - }, - "requests_per_second": { - "description": "Requests per second", - "type": "number", - "example": 12.4 - }, - "timeout_count": { - "description": "Number of timeouts", - "type": "integer", - "example": 5 - }, - "total_bytes_in": { - "description": "Total bytes received", - "type": "integer", - "example": 125430 - }, - "total_bytes_out": { - "description": "Total bytes sent", - "type": "integer", - "example": 987650 - }, - "total_requests": { - "description": "Total requests processed", - "type": "integer", - "example": 1542 - }, - "total_requests_error": { - "description": "Failed requests", - "type": "integer", - "example": 44 - }, - "total_requests_success": { - "description": "Successful requests", - "type": "integer", - "example": 1498 - }, - "tunnel_id": { - "description": "Tunnel identifier", - "type": "string", - "example": "my-tunnel" - }, - "user_agent": { - "description": "Client user agent", - "type": "string", - "example": "FunnelClient/1.0" - } - } - }, - "server.HealthData": { - "type": "object", - "properties": { - "status": { - "description": "Service status", - "type": "string", - "example": "healthy" - }, - "timestamp": { - "description": "Current timestamp", - "type": "string", - "example": "2025-08-08T20:11:00Z" - }, - "uptime": { - "description": "Server uptime duration", - "type": "string", - "example": "6.4281425s" - } - } - }, - "server.HistoricalDataPoint": { - "type": "object", - "properties": { - "active_connections": { - "type": "integer" - }, - "bytes_in": { - "type": "integer" - }, - "bytes_out": { - "type": "integer" - }, - "error_count": { - "type": "integer" - }, - "request_count": { - "type": "integer" - }, - "response_time_ms": { - "type": "number" - }, - "timestamp": { - "type": "string" - } - } - }, - "server.HistoricalMetricsResponse": { - "type": "object", - "properties": { - "data_points": { - "description": "Historical data points", - "type": "array", - "items": { - "$ref": "#/definitions/server.HistoricalDataPoint" - } - }, - "interval": { - "description": "Data point interval", - "type": "string", - "example": "1m" - }, - "time_range": { - "description": "Time range of data", - "type": "string", - "example": "24h" - }, - "tunnel_id": { - "description": "Tunnel identifier", - "type": "string", - "example": "my-tunnel" - } - } - }, - "server.ServerMetricsResponse": { - "type": "object", - "properties": { - "active_tunnels": { - "description": "Currently active tunnels", - "type": "integer", - "example": 8 - }, - "average_response_time_ms": { - "description": "Server average response time", - "type": "number", - "example": 28.7 - }, - "cpu_usage_percent": { - "description": "CPU usage percentage", - "type": "number", - "example": 15.5 - }, - "error_rate_percent": { - "description": "Error rate percentage", - "type": "number", - "example": 0.18 - }, - "memory_usage_bytes": { - "description": "Memory usage in bytes", - "type": "integer", - "example": 67108864 - }, - "network_connections": { - "description": "Active network connections", - "type": "integer", - "example": 12 - }, - "server_requests_per_second": { - "description": "Server-wide RPS", - "type": "number", - "example": 45.2 - }, - "server_uptime": { - "description": "Server uptime", - "type": "string", - "example": "5h23m15s" - }, - "total_bytes_transferred": { - "description": "Total bytes transferred", - "type": "integer", - "example": 5670000 - }, - "total_errors": { - "description": "Total errors", - "type": "integer", - "example": 234 - }, - "total_requests": { - "description": "Total requests processed", - "type": "integer", - "example": 125430 - }, - "total_tunnels": { - "description": "Total tunnels created", - "type": "integer", - "example": 15 - } - } - }, - "server.ServerStats": { - "type": "object", - "properties": { - "active_tunnels": { - "description": "Number of currently active tunnel connections", - "type": "integer", - "example": 2 - }, - "cache_hit_rate": { - "description": "Cache hit rate percentage", - "type": "string", - "example": "90.9%" - }, - "cache_hits": { - "description": "Number of cache hits for hostname resolution", - "type": "integer", - "example": 1401 - }, - "cache_misses": { - "description": "Number of cache misses for hostname resolution", - "type": "integer", - "example": 141 - }, - "router_stats": { - "description": "Detailed router statistics", - "type": "object", - "additionalProperties": true - }, - "total_requests": { - "description": "Total number of HTTP requests processed", - "type": "integer", - "example": 1542 - }, - "uptime": { - "description": "Server uptime duration", - "type": "string", - "example": "2h15m30s" - } - } - }, - "server.TunnelDeleteData": { - "type": "object", - "properties": { - "message": { - "description": "Success message", - "type": "string", - "example": "tunnel deleted successfully" - }, - "tunnel_id": { - "description": "Deleted tunnel ID", - "type": "string", - "example": "my-tunnel" - } - } - }, - "server.TunnelInfo": { - "type": "object", - "properties": { - "bytes_received": { - "description": "Total bytes received from client", - "type": "integer", - "example": 12543 - }, - "bytes_sent": { - "description": "Total bytes sent to client", - "type": "integer", - "example": 98765 - }, - "created_at": { - "description": "Tunnel creation timestamp", - "type": "string", - "example": "2025-08-08T18:30:00Z" - }, - "id": { - "description": "Unique tunnel identifier", - "type": "string", - "example": "my-tunnel" - }, - "messages_received": { - "description": "Number of messages received from client", - "type": "integer", - "example": 45 - }, - "messages_sent": { - "description": "Number of messages sent to client", - "type": "integer", - "example": 45 - }, - "status": { - "description": "Current tunnel status", - "type": "string", - "enum": [ - "connected", - "disconnected" - ], - "example": "connected" - }, - "uptime": { - "description": "Tunnel connection uptime", - "type": "string", - "example": "1h15m30s" - } - } - }, - "server.TunnelStats": { - "type": "object", - "properties": { - "bytes_received": { - "description": "Bytes received total", - "type": "integer", - "example": 12543 - }, - "bytes_sent": { - "description": "Bytes sent total", - "type": "integer", - "example": 98765 - }, - "connection_info": { - "description": "Connection details", - "type": "object", - "additionalProperties": true - }, - "created_at": { - "description": "Creation timestamp", - "type": "string", - "example": "2025-08-08T18:30:00Z" - }, - "messages_received": { - "description": "Messages received count", - "type": "integer", - "example": 45 - }, - "messages_sent": { - "description": "Messages sent count", - "type": "integer", - "example": 45 - }, - "status": { - "description": "Connection status", - "type": "string", - "example": "connected" - }, - "tunnel_id": { - "description": "Tunnel identifier", - "type": "string", - "example": "my-tunnel" - }, - "uptime": { - "description": "Connection uptime", - "type": "string", - "example": "1h15m30s" - } - } - } - }, - "tags": [ - { - "description": "Server health and statistics endpoints", - "name": "Server" - }, - { - "description": "Tunnel management and monitoring endpoints", - "name": "Tunnels" - } - ] -} \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml deleted file mode 100644 index e3967b3..0000000 --- a/server/docs/swagger.yaml +++ /dev/null @@ -1,658 +0,0 @@ -basePath: /api -definitions: - server.APIResponse: - properties: - data: - description: Response data (present on success) - error: - description: Error message (present on failure) - example: error message - type: string - success: - description: Indicates if the request was successful - example: true - type: boolean - type: object - server.DetailedTunnelMetrics: - properties: - average_response_time_ms: - description: Average response time in ms - example: 25.5 - type: number - client_ip: - description: Client IP address - example: 192.168.1.100:52341 - type: string - connection_errors: - description: Connection errors - example: 2 - type: integer - connections_active: - description: Active connections - example: 1 - type: integer - connections_total: - description: Total connections made - example: 3 - type: integer - created_at: - description: Creation time - example: "2025-08-08T18:30:00Z" - type: string - data_errors: - description: Data errors - example: 1 - type: integer - last_activity: - description: Last activity time - example: "2025-08-08T20:30:00Z" - type: string - max_response_time_ms: - description: Maximum response time in ms - example: 150.8 - type: number - min_response_time_ms: - description: Minimum response time in ms - example: 5.2 - type: number - reconnect_count: - description: Number of reconnections - example: 2 - type: integer - requests_per_second: - description: Requests per second - example: 12.4 - type: number - timeout_count: - description: Number of timeouts - example: 5 - type: integer - total_bytes_in: - description: Total bytes received - example: 125430 - type: integer - total_bytes_out: - description: Total bytes sent - example: 987650 - type: integer - total_requests: - description: Total requests processed - example: 1542 - type: integer - total_requests_error: - description: Failed requests - example: 44 - type: integer - total_requests_success: - description: Successful requests - example: 1498 - type: integer - tunnel_id: - description: Tunnel identifier - example: my-tunnel - type: string - user_agent: - description: Client user agent - example: FunnelClient/1.0 - type: string - type: object - server.HealthData: - properties: - status: - description: Service status - example: healthy - type: string - timestamp: - description: Current timestamp - example: "2025-08-08T20:11:00Z" - type: string - uptime: - description: Server uptime duration - example: 6.4281425s - type: string - type: object - server.HistoricalDataPoint: - properties: - active_connections: - type: integer - bytes_in: - type: integer - bytes_out: - type: integer - error_count: - type: integer - request_count: - type: integer - response_time_ms: - type: number - timestamp: - type: string - type: object - server.HistoricalMetricsResponse: - properties: - data_points: - description: Historical data points - items: - $ref: '#/definitions/server.HistoricalDataPoint' - type: array - interval: - description: Data point interval - example: 1m - type: string - time_range: - description: Time range of data - example: 24h - type: string - tunnel_id: - description: Tunnel identifier - example: my-tunnel - type: string - type: object - server.ServerMetricsResponse: - properties: - active_tunnels: - description: Currently active tunnels - example: 8 - type: integer - average_response_time_ms: - description: Server average response time - example: 28.7 - type: number - cpu_usage_percent: - description: CPU usage percentage - example: 15.5 - type: number - error_rate_percent: - description: Error rate percentage - example: 0.18 - type: number - memory_usage_bytes: - description: Memory usage in bytes - example: 67108864 - type: integer - network_connections: - description: Active network connections - example: 12 - type: integer - server_requests_per_second: - description: Server-wide RPS - example: 45.2 - type: number - server_uptime: - description: Server uptime - example: 5h23m15s - type: string - total_bytes_transferred: - description: Total bytes transferred - example: 5670000 - type: integer - total_errors: - description: Total errors - example: 234 - type: integer - total_requests: - description: Total requests processed - example: 125430 - type: integer - total_tunnels: - description: Total tunnels created - example: 15 - type: integer - type: object - server.ServerStats: - properties: - active_tunnels: - description: Number of currently active tunnel connections - example: 2 - type: integer - cache_hit_rate: - description: Cache hit rate percentage - example: 90.9% - type: string - cache_hits: - description: Number of cache hits for hostname resolution - example: 1401 - type: integer - cache_misses: - description: Number of cache misses for hostname resolution - example: 141 - type: integer - router_stats: - additionalProperties: true - description: Detailed router statistics - type: object - total_requests: - description: Total number of HTTP requests processed - example: 1542 - type: integer - uptime: - description: Server uptime duration - example: 2h15m30s - type: string - type: object - server.TunnelDeleteData: - properties: - message: - description: Success message - example: tunnel deleted successfully - type: string - tunnel_id: - description: Deleted tunnel ID - example: my-tunnel - type: string - type: object - server.TunnelInfo: - properties: - bytes_received: - description: Total bytes received from client - example: 12543 - type: integer - bytes_sent: - description: Total bytes sent to client - example: 98765 - type: integer - created_at: - description: Tunnel creation timestamp - example: "2025-08-08T18:30:00Z" - type: string - id: - description: Unique tunnel identifier - example: my-tunnel - type: string - messages_received: - description: Number of messages received from client - example: 45 - type: integer - messages_sent: - description: Number of messages sent to client - example: 45 - type: integer - status: - description: Current tunnel status - enum: - - connected - - disconnected - example: connected - type: string - uptime: - description: Tunnel connection uptime - example: 1h15m30s - type: string - type: object - server.TunnelStats: - properties: - bytes_received: - description: Bytes received total - example: 12543 - type: integer - bytes_sent: - description: Bytes sent total - example: 98765 - type: integer - connection_info: - additionalProperties: true - description: Connection details - type: object - created_at: - description: Creation timestamp - example: "2025-08-08T18:30:00Z" - type: string - messages_received: - description: Messages received count - example: 45 - type: integer - messages_sent: - description: Messages sent count - example: 45 - type: integer - status: - description: Connection status - example: connected - type: string - tunnel_id: - description: Tunnel identifier - example: my-tunnel - type: string - uptime: - description: Connection uptime - example: 1h15m30s - type: string - type: object -host: localhost:8080 -info: - contact: - name: Funnel Support - url: https://github.com/karol-broda/funnel - description: |- - REST API for tunnel management and monitoring in the funnel tunneling solution - - This API provides endpoints for: - - Health checking - - Server statistics and monitoring - - Tunnel management (list, view, delete) - - Individual tunnel statistics - license: - name: MIT - url: https://github.com/karol-broda/funnel/blob/master/LICENSE.md - title: Funnel Server API - version: "1.0" -paths: - /health: - get: - consumes: - - application/json - description: Returns the health status of the server - produces: - - application/json - responses: - "200": - description: Server is healthy - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.HealthData' - type: object - summary: Health check - tags: - - Server - /metrics: - get: - consumes: - - application/json - description: Returns detailed server-wide metrics including performance, resource - usage, and aggregated statistics - produces: - - application/json - responses: - "200": - description: Comprehensive server metrics - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.ServerMetricsResponse' - type: object - summary: Get comprehensive server metrics - tags: - - Server - /metrics/historical: - get: - consumes: - - application/json - description: Returns time-series data for server-wide metrics over a specified - time range - parameters: - - default: 24h - description: Time range (1h, 24h, 7d) - in: query - name: range - type: string - - default: 5m - description: Data interval (1m, 5m, 1h) - in: query - name: interval - type: string - produces: - - application/json - responses: - "200": - description: Historical server metrics - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.HistoricalMetricsResponse' - type: object - summary: Get historical server metrics - tags: - - Server - /server/stats: - get: - consumes: - - application/json - description: Returns comprehensive server-wide statistics including tunnel count, - request metrics, and cache performance - produces: - - application/json - responses: - "200": - description: Server statistics retrieved successfully - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.ServerStats' - type: object - summary: Get server statistics - tags: - - Server - /tunnels: - get: - consumes: - - application/json - description: Returns a list of all currently active tunnel connections - produces: - - application/json - responses: - "200": - description: List of active tunnels - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - items: - $ref: '#/definitions/server.TunnelInfo' - type: array - type: object - summary: List active tunnels - tags: - - Tunnels - /tunnels/{tunnelId}: - delete: - consumes: - - application/json - description: Forcibly closes a tunnel connection - parameters: - - description: Tunnel ID to delete - in: path - maxLength: 63 - minLength: 3 - name: tunnelId - required: true - type: string - produces: - - application/json - responses: - "200": - description: Tunnel deleted successfully - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.TunnelDeleteData' - type: object - "400": - description: Invalid tunnel ID format - schema: - $ref: '#/definitions/server.APIResponse' - "404": - description: Tunnel not found - schema: - $ref: '#/definitions/server.APIResponse' - summary: Delete tunnel - tags: - - Tunnels - get: - consumes: - - application/json - description: Returns detailed information about a specific tunnel - parameters: - - description: Tunnel ID (3-63 characters, lowercase letters, numbers, and hyphens - only) - in: path - maxLength: 63 - minLength: 3 - name: tunnelId - required: true - type: string - produces: - - application/json - responses: - "200": - description: Tunnel details retrieved successfully - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.TunnelInfo' - type: object - "400": - description: Invalid tunnel ID format - schema: - $ref: '#/definitions/server.APIResponse' - "404": - description: Tunnel not found - schema: - $ref: '#/definitions/server.APIResponse' - summary: Get tunnel details - tags: - - Tunnels - /tunnels/{tunnelId}/historical: - get: - consumes: - - application/json - description: Returns time-series data for a specific tunnel over a specified - time range - parameters: - - description: Tunnel ID - in: path - maxLength: 63 - minLength: 3 - name: tunnelId - required: true - type: string - - default: 24h - description: Time range (1h, 24h, 7d) - in: query - name: range - type: string - - description: Start time (RFC3339 format) - in: query - name: start - type: string - - description: End time (RFC3339 format) - in: query - name: end - type: string - produces: - - application/json - responses: - "200": - description: Tunnel historical data - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.HistoricalMetricsResponse' - type: object - "400": - description: Invalid parameters - schema: - $ref: '#/definitions/server.APIResponse' - "404": - description: Tunnel not found - schema: - $ref: '#/definitions/server.APIResponse' - summary: Get tunnel historical data - tags: - - Tunnels - /tunnels/{tunnelId}/metrics: - get: - consumes: - - application/json - description: Returns detailed performance and usage metrics for a specific tunnel - parameters: - - description: Tunnel ID - in: path - maxLength: 63 - minLength: 3 - name: tunnelId - required: true - type: string - produces: - - application/json - responses: - "200": - description: Comprehensive tunnel metrics - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.DetailedTunnelMetrics' - type: object - "400": - description: Invalid tunnel ID format - schema: - $ref: '#/definitions/server.APIResponse' - "404": - description: Tunnel not found - schema: - $ref: '#/definitions/server.APIResponse' - summary: Get comprehensive tunnel metrics - tags: - - Tunnels - /tunnels/{tunnelId}/stats: - get: - consumes: - - application/json - description: Returns detailed statistics and metrics for a specific tunnel - parameters: - - description: Tunnel ID - in: path - maxLength: 63 - minLength: 3 - name: tunnelId - required: true - type: string - produces: - - application/json - responses: - "200": - description: Tunnel statistics retrieved successfully - schema: - allOf: - - $ref: '#/definitions/server.APIResponse' - - properties: - data: - $ref: '#/definitions/server.TunnelStats' - type: object - "400": - description: Invalid tunnel ID format - schema: - $ref: '#/definitions/server.APIResponse' - "404": - description: Tunnel not found - schema: - $ref: '#/definitions/server.APIResponse' - summary: Get tunnel statistics - tags: - - Tunnels -schemes: -- http -- https -swagger: "2.0" -tags: -- description: Server health and statistics endpoints - name: Server -- description: Tunnel management and monitoring endpoints - name: Tunnels diff --git a/server/go.mod b/server/go.mod deleted file mode 100644 index 215632c..0000000 --- a/server/go.mod +++ /dev/null @@ -1,214 +0,0 @@ -module github.com/karol-broda/funnel/server - -go 1.24.4 - -require ( - github.com/go-acme/lego/v4 v4.23.1 - github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.3 - github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/karol-broda/funnel/shared v0.0.0-00010101000000-000000000000 - github.com/rs/zerolog v1.34.0 - github.com/stretchr/testify v1.10.0 - github.com/swaggo/http-swagger/v2 v2.0.2 - github.com/swaggo/swag v1.16.6 -) - -require ( - cloud.google.com/go/auth v0.15.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect - github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect - github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.30 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect - github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect - github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect - github.com/KyleBanks/depth v1.2.1 // indirect - github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect - github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect - github.com/aliyun/alibaba-cloud-sdk-go v1.63.100 // indirect - github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect - github.com/aws/aws-sdk-go-v2/config v1.29.9 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect - github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 // indirect - github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect - github.com/aws/smithy-go v1.22.2 // indirect - github.com/baidubce/bce-sdk-go v0.9.223 // indirect - github.com/benbjohnson/clock v1.3.0 // indirect - github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/civo/civogo v0.3.11 // indirect - github.com/cloudflare/cloudflare-go v0.115.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/dnsimple/dnsimple-go v1.7.0 // indirect - github.com/exoscale/egoscale/v3 v3.1.13 // indirect - github.com/fatih/structs v1.1.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-errors/errors v1.0.1 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/spec v0.20.6 // indirect - github.com/go-openapi/swag v0.19.15 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.16.0 // indirect - github.com/go-resty/resty/v2 v2.16.5 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/gofrs/flock v0.12.1 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/gophercloud/gophercloud v1.14.1 // indirect - github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.141 // indirect - github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect - github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect - github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect - github.com/labbsr0x/goh v1.0.1 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/linode/linodego v1.48.1 // indirect - github.com/liquidweb/liquidweb-cli v0.6.9 // indirect - github.com/liquidweb/liquidweb-go v1.6.4 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.6 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.64 // indirect - github.com/mimuret/golang-iij-dpf v0.9.1 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect - github.com/nrdcg/auroradns v1.1.0 // indirect - github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect - github.com/nrdcg/desec v0.10.0 // indirect - github.com/nrdcg/dnspod-go v0.4.0 // indirect - github.com/nrdcg/freemyip v0.3.0 // indirect - github.com/nrdcg/goacmedns v0.2.0 // indirect - github.com/nrdcg/goinwx v0.10.0 // indirect - github.com/nrdcg/mailinabox v0.2.0 // indirect - github.com/nrdcg/namesilo v0.2.1 // indirect - github.com/nrdcg/nodion v0.1.0 // indirect - github.com/nrdcg/porkbun v0.4.0 // indirect - github.com/nzdjb/go-metaname v1.0.0 // indirect - github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect - github.com/oracle/oci-go-sdk/v65 v65.87.0 // indirect - github.com/ovh/go-ovh v1.7.0 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/peterhellberg/link v1.2.0 // indirect - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/pquerna/otp v1.4.0 // indirect - github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect - github.com/sacloud/api-client-go v0.2.10 // indirect - github.com/sacloud/go-http v0.1.8 // indirect - github.com/sacloud/iaas-api-go v1.14.0 // indirect - github.com/sacloud/packages-go v0.0.10 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 // indirect - github.com/selectel/domains-go v1.1.0 // indirect - github.com/selectel/go-selvpcclient/v3 v3.2.1 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect - github.com/softlayer/softlayer-go v1.1.7 // indirect - github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect - github.com/sony/gobreaker v0.5.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/spf13/viper v1.18.2 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/swaggo/files/v2 v2.0.0 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 // indirect - github.com/tjfoc/gmsm v1.4.1 // indirect - github.com/transip/gotransip/v6 v6.26.0 // indirect - github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect - github.com/vinyldns/go-vinyldns v0.9.16 // indirect - github.com/volcengine/volc-sdk-golang v1.0.199 // indirect - github.com/vultr/govultr/v3 v3.17.0 // indirect - github.com/yandex-cloud/go-genproto v0.0.0-20250319153614-fb9d3e5eb01a // indirect - github.com/yandex-cloud/go-sdk v0.0.0-20250320143332-9cbcfc5de4ae // indirect - go.mongodb.org/mongo-driver v1.13.1 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - go.uber.org/ratelimit v0.3.0 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.32.0 // indirect - google.golang.org/api v0.227.0 // indirect - google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect - google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace github.com/karol-broda/funnel/shared => ../shared diff --git a/server/go.sum b/server/go.sum deleted file mode 100644 index 0f2499a..0000000 --- a/server/go.sum +++ /dev/null @@ -1,1424 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE= -github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= -github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= -github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= -github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= -github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= -github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= -github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= -github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= -github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY= -github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/aliyun/alibaba-cloud-sdk-go v1.63.100 h1:yUkCbrSM1cWtgBfRVKMQtdt22KhDvKY7g4V+92eG9wA= -github.com/aliyun/alibaba-cloud-sdk-go v1.63.100/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= -github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= -github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 h1:0j58UseBtLuBcP6nY2z4SM1qZEvLF0ylyH6+ggnphLg= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1/go.mod h1:Qy22QnQSdHbZwMZrarsWZBIuK51isPlkD+Z4sztxX0o= -github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 h1:/nkJHXtJXJeelXHqG0898+fWKgvfaXBhGzbCsSmn9j8= -github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0/go.mod h1:kGYOjvTa0Vw0qxrqrOLut1vMnui6qLxqv/SX3vYeM8Y= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= -github.com/baidubce/bce-sdk-go v0.9.223 h1:vvDeIemf7ePPP59nLHCntQ/vS++ok2HKbRPgmz1VZKU= -github.com/baidubce/bce-sdk-go v0.9.223/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= -github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= -github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= -github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= -github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= -github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY= -github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/exoscale/egoscale/v3 v3.1.13 h1:CAGC7QRjp2AiGj01agsSD0VKCp4OZmW5f51vV2IguNQ= -github.com/exoscale/egoscale/v3 v3.1.13/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-acme/lego/v4 v4.23.1 h1:lZ5fGtGESA2L9FB8dNTvrQUq3/X4QOb8ExkKyY7LSV4= -github.com/go-acme/lego/v4 v4.23.1/go.mod h1:7UMVR7oQbIYw6V7mTgGwi4Er7B6Ww0c+c8feiBM0EgI= -github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= -github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= -github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= -github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= -github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= -github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 h1:sH7xkTfYzxIEgzq1tDHIMKRh1vThOEOGNsettdEeLbE= -github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56/go.mod h1:VSalo4adEk+3sNkmVJLnhHoOyOYYS8sTWLG4mv5BKto= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= -github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.141 h1:8i57QAi5u+iPAYze92bkIvZoHiS0J45ndul5glr/NE8= -github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.141/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY= -github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E= -github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 h1:wS8kTlQVeVbrepeY83s9X+XdSa6Qah5KO+tdW+zRQXU= -github.com/infobloxopen/infoblox-go-client/v2 v2.9.0/go.mod h1:NeNJpz09efw/edzqkVivGv1bWqBXTomqYBRFbP+XBqg= -github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= -github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= -github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labbsr0x/bindman-dns-webhook v1.0.2 h1:I7ITbmQPAVwrDdhd6dHKi+MYJTJqPCK0jE6YNBAevnk= -github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= -github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk= -github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/linode/linodego v1.48.1 h1:Ojw1S+K5jJr1dggO8/H6r4FINxXnJbOU5GkbpaTfmhU= -github.com/linode/linodego v1.48.1/go.mod h1:fc3t60If8X+yZTFAebhCnNDFrhwQhq9HDU92WnBousQ= -github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= -github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= -github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= -github.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9fgIbjMc= -github.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= -github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= -github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= -github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= -github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M= -github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= -github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= -github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= -github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= -github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= -github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= -github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= -github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo= -github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= -github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 h1:ouZ2JWDl8IW5k1qugYbmpbmW8hn85Ig6buSMBRlz3KI= -github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3/go.mod h1:ZwadWt7mVhMHMbAQ1w8IhDqtWO3eWqWq72W7trnaiE8= -github.com/nrdcg/desec v0.10.0 h1:qrEDiqnsvNU9QE7lXIXi/tIHAfyaFXKxF2/8/52O8uM= -github.com/nrdcg/desec v0.10.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs= -github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= -github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= -github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc= -github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM= -github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0= -github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= -github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM= -github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4= -github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk= -github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc= -github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= -github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= -github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= -github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= -github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9Hg= -github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= -github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= -github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= -github.com/oracle/oci-go-sdk/v65 v65.87.0 h1:CeVuK8t0dYODGT3P9IDhz4vyXF8poYE1ijoiO5vrKl0= -github.com/oracle/oci-go-sdk/v65 v65.87.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= -github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw= -github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= -github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= -github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= -github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= -github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE= -github.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sacloud/api-client-go v0.2.10 h1:+rv3jDohD+pkdYwOTBiB+jZsM0xK3AxadXRzhp3q66c= -github.com/sacloud/api-client-go v0.2.10/go.mod h1:Jj3CTy2+O4bcMedVDXlbHuqqche85HEPuVXoQFhLaRc= -github.com/sacloud/go-http v0.1.8 h1:ynreWA/vnM8G2ksbMlmefBHsXURKPz49qlPRqQ9IQdw= -github.com/sacloud/go-http v0.1.8/go.mod h1:7TL7TN1fnPKHsMifIqURDkGujnKViCgEz5Ei/LQdFK8= -github.com/sacloud/iaas-api-go v1.14.0 h1:xjkFWqdo4ilTrKPNNYBNWR/CZ/kVRsJrdAHAad6J/AQ= -github.com/sacloud/iaas-api-go v1.14.0/go.mod h1:C8os2Mnj0TOmMdSllwhaDWKMVG2ysFnpe69kyA4M3V0= -github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D+AOck= -github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 h1:4+LP7qmsLSGbmc66m1s5dKRMBwztRppfxFKlYqYte/c= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32/go.mod h1:kzh+BSAvpoyHHdHBCDhmSWtBc1NbLMZ2lWHqnBoxFks= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5gSHo= -github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= -github.com/selectel/go-selvpcclient/v3 v3.2.1 h1:ny6WIAMiHzKxOgOEnwcWE79wIQij1AHHylzPA41MXCw= -github.com/selectel/go-selvpcclient/v3 v3.2.1/go.mod h1:3EfSf8aEWyhspOGbvZ6mvnFg7JN5uckxNyBFPGWsXNQ= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= -github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= -github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= -github.com/softlayer/softlayer-go v1.1.7 h1:SgTL+pQZt1h+5QkAhVmHORM/7N9c1X0sljJhuOIHxWE= -github.com/softlayer/softlayer-go v1.1.7/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw= -github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= -github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= -github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= -github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= -github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= -github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= -github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= -github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128 h1:NGnqDc8FQL0YdiCHgTO4Wkso6ToD8rE3JW9VOzoPBNA= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 h1:mrJ5Fbkd7sZIJ5F6oRfh5zebPQaudPH9Y0+GUmFytYU= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128/go.mod h1:zbsYIBT+VTX4z4ocjTAdLBIWyNYj3z0BRqd0iPdnjsk= -github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= -github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550= -github.com/transip/gotransip/v6 v6.26.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= -github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= -github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI= -github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= -github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= -github.com/volcengine/volc-sdk-golang v1.0.199 h1:zv9QOqTl/IsLwtfC37GlJtcz6vMAHi+pjq8ILWjLYUc= -github.com/volcengine/volc-sdk-golang v1.0.199/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ= -github.com/vultr/govultr/v3 v3.17.0 h1:His5Jh5N8KKqaJxfy3uG6jQbLXy0TmQhNxOiRvkKk00= -github.com/vultr/govultr/v3 v3.17.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yandex-cloud/go-genproto v0.0.0-20250319153614-fb9d3e5eb01a h1:YO8gGyAV4N5SR3NzloZ1128IahSpXWr78oU7aEe7f04= -github.com/yandex-cloud/go-genproto v0.0.0-20250319153614-fb9d3e5eb01a/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= -github.com/yandex-cloud/go-sdk v0.0.0-20250320143332-9cbcfc5de4ae h1:x+uGuST05LVlgCxF5TsP8kQCCTW7uIeAQJ1dKtSmWqE= -github.com/yandex-cloud/go-sdk v0.0.0-20250320143332-9cbcfc5de4ae/go.mod h1:V71iJlJnS/NtNNdg/B7SwccBS19aXxwY3fv/wut9D74= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= -go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -go.uber.org/ratelimit v0.3.0 h1:IdZd9wqvFXnvLvSEBo0KPcGfkoBGNkpTHlrE3Rcjkjw= -go.uber.org/ratelimit v0.3.0/go.mod h1:So5LG7CV1zWpY1sHe+DXTJqQvOx+FFPFaAs2SnoyBaI= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc= -google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= -google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= -gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.13.0 h1:I5NNqI9Bi1SGK92TVkOvLTwux5LNrix/99H2datVh48= -gopkg.in/ns1/ns1-go.v2 v2.13.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/server/router.go b/server/router.go deleted file mode 100644 index 6274fc2..0000000 --- a/server/router.go +++ /dev/null @@ -1,550 +0,0 @@ -package server - -import ( - "fmt" - "io" - "net/http" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/google/uuid" - "github.com/karol-broda/funnel/shared" -) - -type TunnelRouter struct { - server *Server - - hostCache sync.Map // map[string]string (host -> subdomain) - - requests int64 - cacheHits int64 - cacheMisses int64 - - bufferPool sync.Pool - apiHandler *APIHandler -} - -func NewTunnelRouter(server *Server) *TunnelRouter { - logger := shared.GetLogger("server.router") - - router := &TunnelRouter{ - server: server, - bufferPool: sync.Pool{ - New: func() interface{} { - b := make([]byte, 1024) - return &b - }, - }, - } - - logger.Info(). - Int("buffer_pool_size", 1024). - Msg("tunnel router initialized") - - return router -} - -func (tr *TunnelRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { - requestStart := time.Now() - requestUUID, err := uuid.NewV7() - if err != nil { - requestUUID = uuid.New() - } - requestID := requestUUID.String() - - logger := shared.GetLogger("server.router") - - atomic.AddInt64(&tr.requests, 1) - currentRequests := atomic.LoadInt64(&tr.requests) - - logger.Info(). - Str("request_id", requestID). - Str("method", r.Method). - Str("host", r.Host). - Str("path", r.URL.Path). - Str("remote_addr", r.RemoteAddr). - Str("user_agent", r.UserAgent()). - Int64("total_requests", currentRequests). - Msg("incoming HTTP request") - - // handle websocket upgrade requests first - if len(r.Header["Upgrade"]) > 0 && r.Header["Upgrade"][0] == "websocket" { - logger.Debug().Str("request_id", requestID).Msg("routing to websocket handler") - tr.server.HandleWebSocket(w, r) - return - } - - // handle swagger UI requests - if strings.HasPrefix(r.URL.Path, "/swagger/") { - logger.Debug().Str("request_id", requestID).Msg("routing to Swagger UI handler") - tr.ServeSwaggerUI(w, r) - return - } - - // handle API requests - if strings.HasPrefix(r.URL.Path, "/api/") { - logger.Debug().Str("request_id", requestID).Msg("routing to API handler") - if tr.apiHandler != nil { - tr.apiHandler.HandleAPIRequest(w, r) - } else { - http.Error(w, "API handler not initialized", http.StatusInternalServerError) - } - return - } - - subdomainStart := time.Now() - subdomain := tr.getSubdomain(r.Host) - subdomainDuration := time.Since(subdomainStart) - - if subdomain == "" { - processingDuration := time.Since(requestStart) - logger.Warn(). - Str("request_id", requestID). - Str("host", r.Host). - Dur("processing_time", processingDuration). - Msg("tunnel not found - invalid host") - http.Error(w, "tunnel not found", http.StatusNotFound) - return - } - - logger.Debug(). - Str("request_id", requestID). - Str("subdomain", subdomain). - Dur("subdomain_extraction_time", subdomainDuration). - Msg("extracted subdomain from host") - - tunnel, exists := tr.server.GetTunnel(subdomain) - if !exists { - processingDuration := time.Since(requestStart) - logger.Warn(). - Str("request_id", requestID). - Str("subdomain", subdomain). - Dur("processing_time", processingDuration). - Msg("tunnel not found - no active tunnel") - http.Error(w, "tunnel not found", http.StatusNotFound) - return - } - - logger.Debug(). - Str("request_id", requestID). - Str("tunnel_id", subdomain). - Msg("found active tunnel, routing request") - - tr.handleTunnelRequest(w, r, tunnel, requestID, requestStart) -} - -func (tr *TunnelRouter) getSubdomain(host string) string { - logger := shared.GetLogger("server.router") - - if cached, ok := tr.hostCache.Load(host); ok { - atomic.AddInt64(&tr.cacheHits, 1) - hits := atomic.LoadInt64(&tr.cacheHits) - logger.Debug(). - Str("host", host). - Str("cached_subdomain", cached.(string)). - Int64("total_cache_hits", hits). - Msg("subdomain cache hit") - return cached.(string) - } - - atomic.AddInt64(&tr.cacheMisses, 1) - misses := atomic.LoadInt64(&tr.cacheMisses) - - logger.Debug(). - Str("host", host). - Int64("total_cache_misses", misses). - Msg("subdomain cache miss") - - subdomain := tr.extractSubdomain(host) - - if subdomain != "" { - tr.hostCache.Store(host, subdomain) - logger.Debug(). - Str("host", host). - Str("extracted_subdomain", subdomain). - Msg("subdomain cached") - } else { - logger.Debug(). - Str("host", host). - Msg("no valid subdomain found") - } - - return subdomain -} - -func (tr *TunnelRouter) extractSubdomain(host string) string { - if len(host) == 0 { - return "" - } - - hostLen := len(host) - for i := hostLen - 1; i >= 0; i-- { - if host[i] == ':' { - host = host[:i] - break - } - if host[i] < '0' || host[i] > '9' { - break - } - } - - for i := 0; i < len(host); i++ { - if host[i] == '.' { - if i == 0 { - return "" - } - return host[:i] - } - } - - return "" -} - -func (tr *TunnelRouter) handleTunnelRequest(w http.ResponseWriter, r *http.Request, tunnel *Tunnel, requestID string, requestStart time.Time) { - logger := shared.GetRequestLogger("server.router", tunnel.ID, requestID) - - bodyReadStart := time.Now() - body, err := tr.readBody(r) - bodyReadDuration := time.Since(bodyReadStart) - - if err != nil { - processingDuration := time.Since(requestStart) - logger.Error().Err(err). - Dur("body_read_duration", bodyReadDuration). - Dur("total_processing_time", processingDuration). - Msg("failed to read request body") - http.Error(w, "failed to read body", http.StatusInternalServerError) - return - } - - logger.Debug(). - Int("body_size", len(body)). - Dur("body_read_time", bodyReadDuration). - Msg("request body read successfully") - - headers := tr.prepareForwardingHeaders(r) - - msg := &shared.Message{ - Type: "request", - TunnelID: tunnel.ID, - RequestID: requestID, - Method: r.Method, - Path: r.URL.String(), - Headers: headers, - Body: body, - } - - channelStart := time.Now() - respChan := tunnel.registerResponseChannel(msg.RequestID) - defer tunnel.unregisterResponseChannel(msg.RequestID) - channelDuration := time.Since(channelStart) - - sendStart := time.Now() - if err := tunnel.SendMessage(msg); err != nil { - sendDuration := time.Since(sendStart) - processingDuration := time.Since(requestStart) - logger.Error().Err(err). - Dur("send_duration", sendDuration). - Dur("total_processing_time", processingDuration). - Msg("failed to send message to tunnel") - http.Error(w, "tunnel connection lost", http.StatusBadGateway) - return - } - sendDuration := time.Since(sendStart) - - logger.Debug(). - Dur("channel_setup_time", channelDuration). - Dur("message_send_time", sendDuration). - Msg("request forwarded to tunnel") - - waitStart := time.Now() - timeout := 30 * time.Second - - select { - case resp := <-respChan: - waitDuration := time.Since(waitStart) - if resp != nil { - logger.Info(). - Int("status_code", resp.Status). - Int("response_size", len(resp.Body)). - Dur("tunnel_response_time", waitDuration). - Dur("total_request_time", time.Since(requestStart)). - Msg("received response from tunnel") - tr.writeResponse(w, resp, requestID, requestStart) - } else { - processingDuration := time.Since(requestStart) - logger.Error(). - Dur("wait_duration", waitDuration). - Dur("total_processing_time", processingDuration). - Msg("received nil response from tunnel") - http.Error(w, "tunnel connection lost", http.StatusBadGateway) - } - case <-time.After(timeout): - processingDuration := time.Since(requestStart) - logger.Error(). - Dur("timeout_duration", timeout). - Dur("wait_duration", time.Since(waitStart)). - Dur("total_processing_time", processingDuration). - Msg("request timeout waiting for tunnel response") - http.Error(w, "request timed out", http.StatusGatewayTimeout) - - case <-r.Context().Done(): - processingDuration := time.Since(requestStart) - logger.Warn(). - Dur("wait_duration", time.Since(waitStart)). - Dur("total_processing_time", processingDuration). - Msg("client closed connection") - http.Error(w, "client closed connection", 499) - } -} - -func (tr *TunnelRouter) prepareForwardingHeaders(r *http.Request) map[string][]string { - logger := shared.GetLogger("server.router") - - headers := make(map[string][]string) - for k, v := range r.Header { - headers[k] = make([]string, len(v)) - copy(headers[k], v) - } - - clientIP := tr.getClientIP(r) - - currentIP := r.RemoteAddr - if colonIndex := strings.LastIndex(currentIP, ":"); colonIndex != -1 { - currentIP = currentIP[:colonIndex] - } - if strings.HasPrefix(currentIP, "[") && strings.HasSuffix(currentIP, "]") { - currentIP = currentIP[1 : len(currentIP)-1] - } - - if existingForwardedFor := headers["X-Forwarded-For"]; len(existingForwardedFor) > 0 { - headers["X-Forwarded-For"] = []string{existingForwardedFor[0] + ", " + currentIP} - } else { - headers["X-Forwarded-For"] = []string{currentIP} - } - - if r.Host != "" { - headers["X-Forwarded-Host"] = []string{r.Host} - } - - proto := "http" - if r.TLS != nil { - proto = "https" - } - if existingProto := r.Header.Get("X-Forwarded-Proto"); existingProto != "" { - proto = existingProto - } - headers["X-Forwarded-Proto"] = []string{proto} - - headers["X-Real-IP"] = []string{clientIP} - - if r.Header.Get("X-Forwarded-Server") == "" { - headers["X-Forwarded-Server"] = []string{r.Host} - } - - logger.Debug(). - Str("client_ip", clientIP). - Str("forwarded_host", r.Host). - Str("forwarded_proto", proto). - Str("forwarded_for", headers["X-Forwarded-For"][0]). - Msg("prepared forwarding headers") - - return headers -} - -func (tr *TunnelRouter) getClientIP(r *http.Request) string { - if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - if commaIndex := strings.Index(xff, ","); commaIndex != -1 { - return strings.TrimSpace(xff[:commaIndex]) - } - return strings.TrimSpace(xff) - } - - if xri := r.Header.Get("X-Real-IP"); xri != "" { - return strings.TrimSpace(xri) - } - - if xf := r.Header.Get("X-Forwarded"); xf != "" { - if forIndex := strings.Index(xf, "for="); forIndex != -1 { - start := forIndex + 4 - end := strings.Index(xf[start:], ";") - if end == -1 { - end = len(xf) - } else { - end += start - } - return strings.TrimSpace(xf[start:end]) - } - } - - ip := r.RemoteAddr - - if strings.HasPrefix(ip, "[") { - endBracket := strings.Index(ip, "]") - if endBracket != -1 { - return ip[1:endBracket] - } - } - - if colonIndex := strings.LastIndex(ip, ":"); colonIndex != -1 { - if strings.Count(ip, ":") > 1 { - return ip - } - return ip[:colonIndex] - } - - return ip -} - -func (tr *TunnelRouter) readBody(r *http.Request) ([]byte, error) { - logger := shared.GetLogger("server.router") - - if r == nil || r.Body == nil { - logger.Debug().Msg("no request body to read") - return nil, nil - } - - defer r.Body.Close() - - if r.ContentLength == 0 { - logger.Debug().Msg("zero content length, skipping body read") - return nil, nil - } - - if r.ContentLength > 0 && r.ContentLength < 1024 { - logger.Debug(). - Int64("content_length", r.ContentLength). - Msg("using buffer pool for small body") - - bufPtr := tr.bufferPool.Get().(*[]byte) - buf := *bufPtr - defer func() { - for i := range buf { - buf[i] = 0 - } - tr.bufferPool.Put(bufPtr) - }() - - n, err := io.ReadFull(r.Body, buf[:r.ContentLength]) - if err != nil { - logger.Error().Err(err).Int("bytes_read", n).Msg("failed to read small body") - return nil, err - } - return buf[:n], nil - } - - logger.Debug(). - Int64("content_length", r.ContentLength). - Msg("reading large body with ReadAll") - - body, err := io.ReadAll(r.Body) - if err != nil { - logger.Error().Err(err). - Int64("content_length", r.ContentLength). - Msg("error reading large body") - return nil, err - } - - logger.Debug(). - Int("bytes_read", len(body)). - Msg("large body read successfully") - return body, nil -} - -func (tr *TunnelRouter) writeResponse(w http.ResponseWriter, resp *shared.Message, requestID string, requestStart time.Time) { - logger := shared.GetLogger("server.router") - - writeStart := time.Now() - - headerCount := 0 - for k, values := range resp.Headers { - headerCount += len(values) - for _, v := range values { - w.Header().Add(k, v) - } - } - - if resp.Status > 0 { - w.WriteHeader(resp.Status) - } - - bytesWritten := 0 - if len(resp.Body) > 0 { - n, err := w.Write(resp.Body) - bytesWritten = n - if err != nil { - logger.Error().Err(err). - Str("request_id", requestID). - Int("bytes_written", bytesWritten). - Msg("error writing response body") - return - } - } - - writeDuration := time.Since(writeStart) - totalDuration := time.Since(requestStart) - - logger.Info(). - Str("request_id", requestID). - Int("status_code", resp.Status). - Int("header_count", headerCount). - Int("bytes_written", bytesWritten). - Dur("write_duration", writeDuration). - Dur("total_request_duration", totalDuration). - Msg("response written successfully") -} - -func (tr *TunnelRouter) InvalidateCache(tunnelID string) { - logger := shared.GetLogger("server.router") - - invalidatedCount := 0 - tr.hostCache.Range(func(key, value interface{}) bool { - if value.(string) == tunnelID { - tr.hostCache.Delete(key) - invalidatedCount++ - } - return true - }) - - logger.Info(). - Str("tunnel_id", tunnelID). - Int("invalidated_entries", invalidatedCount). - Msg("cache invalidated for tunnel") -} - -func (tr *TunnelRouter) GetStats() map[string]interface{} { - requests := atomic.LoadInt64(&tr.requests) - hits := atomic.LoadInt64(&tr.cacheHits) - misses := atomic.LoadInt64(&tr.cacheMisses) - - hitRate := float64(0) - if hits+misses > 0 { - hitRate = float64(hits) / float64(hits+misses) * 100 - } - - stats := map[string]interface{}{ - "total_requests": requests, - "cache_hits": hits, - "cache_misses": misses, - "cache_hit_rate": fmt.Sprintf("%.1f%%", hitRate), - } - - logger := shared.GetLogger("server.router") - logger.Debug(). - Int64("total_requests", requests). - Int64("cache_hits", hits). - Int64("cache_misses", misses). - Float64("hit_rate", hitRate). - Msg("router statistics requested") - - return stats -} - -// SetAPIHandler sets the API handler for the router -func (tr *TunnelRouter) SetAPIHandler(apiHandler *APIHandler) { - logger := shared.GetLogger("server.router") - tr.apiHandler = apiHandler - logger.Debug().Msg("API handler configured for router") -} diff --git a/server/router_bench_test.go b/server/router_bench_test.go deleted file mode 100644 index 4820355..0000000 --- a/server/router_bench_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package server - -import ( - "strings" - "testing" -) - -func BenchmarkSubdomainExtraction(b *testing.B) { - server := NewServer() - tunnelRouter := NewTunnelRouter(server) - testHost := "K7mN9pXq.localhost:8080" - - b.Run("TunnelRouter", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - tunnelRouter.extractSubdomain(testHost) - } - }) - - b.Run("StringsSplit", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - host := testHost - if idx := strings.Index(host, ":"); idx != -1 { - host = host[:idx] - } - parts := strings.Split(host, ".") - if len(parts) >= 2 { - _ = parts[0] - } - } - }) -} - -func BenchmarkCacheEfficiency(b *testing.B) { - server := NewServer() - tunnelRouter := NewTunnelRouter(server) - - testHost := "cached-tunnel.localhost:8080" - tunnelRouter.getSubdomain(testHost) - - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - tunnelRouter.getSubdomain(testHost) - } -} - -func TestTunnelRouterFunctionality(t *testing.T) { - server := NewServer() - tunnelRouter := NewTunnelRouter(server) - - tests := []struct { - host string - expected string - }{ - {"K7mN9pXq.localhost:8080", "K7mN9pXq"}, - {"test-app.example.com", "test-app"}, - {"abc123.localhost", "abc123"}, - {"localhost:8080", ""}, - {"invalid", ""}, - {".localhost", ""}, - {"", ""}, - } - - for _, tt := range tests { - result := tunnelRouter.getSubdomain(tt.host) - if result != tt.expected { - t.Errorf("getSubdomain(%s) = %s, want %s", tt.host, result, tt.expected) - } - } -} - -func TestTunnelRouterStats(t *testing.T) { - server := NewServer() - tunnelRouter := NewTunnelRouter(server) - - host1 := "test1.localhost:8080" - host2 := "test2.localhost:8080" - - tunnelRouter.getSubdomain(host1) - tunnelRouter.getSubdomain(host2) - - tunnelRouter.getSubdomain(host1) - - stats := tunnelRouter.GetStats() - - if stats["cache_hits"].(int64) != 1 { - t.Errorf("Expected 1 cache hit, got %v", stats["cache_hits"]) - } - - if stats["cache_misses"].(int64) != 2 { - t.Errorf("Expected 2 cache misses, got %v", stats["cache_misses"]) - } -} diff --git a/server/router_header_test.go b/server/router_header_test.go deleted file mode 100644 index 815cdd0..0000000 --- a/server/router_header_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package server - -import ( - "crypto/tls" - "net/http/httptest" - "testing" -) - -func TestTunnelRouter_getClientIP(t *testing.T) { - server := &Server{ - Tunnels: make(map[string]*Tunnel), - } - router := NewTunnelRouter(server) - - tests := []struct { - name string - headers map[string]string - remoteAddr string - expectedIP string - }{ - { - name: "X-Forwarded-For single IP", - headers: map[string]string{ - "X-Forwarded-For": "192.168.1.100", - }, - remoteAddr: "10.0.0.1:12345", - expectedIP: "192.168.1.100", - }, - { - name: "X-Forwarded-For multiple IPs", - headers: map[string]string{ - "X-Forwarded-For": "192.168.1.100, 10.0.0.50, 172.16.0.1", - }, - remoteAddr: "10.0.0.1:12345", - expectedIP: "192.168.1.100", - }, - { - name: "X-Real-IP when no X-Forwarded-For", - headers: map[string]string{ - "X-Real-IP": "203.0.113.45", - }, - remoteAddr: "10.0.0.1:12345", - expectedIP: "203.0.113.45", - }, - { - name: "X-Forwarded header format", - headers: map[string]string{ - "X-Forwarded": "for=198.51.100.17;proto=http", - }, - remoteAddr: "10.0.0.1:12345", - expectedIP: "198.51.100.17", - }, - { - name: "X-Forwarded header with semicolon", - headers: map[string]string{ - "X-Forwarded": "for=198.51.100.17;host=example.com;proto=https", - }, - remoteAddr: "10.0.0.1:12345", - expectedIP: "198.51.100.17", - }, - { - name: "fallback to RemoteAddr with port", - headers: map[string]string{}, - remoteAddr: "203.0.113.195:54321", - expectedIP: "203.0.113.195", - }, - { - name: "fallback to RemoteAddr IPv6 with brackets", - headers: map[string]string{}, - remoteAddr: "[2001:db8::1]:8080", - expectedIP: "2001:db8::1", - }, - { - name: "fallback to RemoteAddr IPv6 without port", - headers: map[string]string{}, - remoteAddr: "2001:db8::1", - expectedIP: "2001:db8::1", - }, - { - name: "X-Forwarded-For takes precedence over X-Real-IP", - headers: map[string]string{ - "X-Forwarded-For": "192.168.1.100", - "X-Real-IP": "203.0.113.45", - }, - remoteAddr: "10.0.0.1:12345", - expectedIP: "192.168.1.100", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest("GET", "http://example.com/test", nil) - req.RemoteAddr = tt.remoteAddr - - for k, v := range tt.headers { - req.Header.Set(k, v) - } - - result := router.getClientIP(req) - if result != tt.expectedIP { - t.Errorf("getClientIP() = %v, want %v", result, tt.expectedIP) - } - }) - } -} - -func TestTunnelRouter_prepareForwardingHeaders(t *testing.T) { - server := &Server{ - Tunnels: make(map[string]*Tunnel), - } - router := NewTunnelRouter(server) - - tests := []struct { - name string - originalHeaders map[string][]string - host string - remoteAddr string - tls bool - expectedHeaders map[string]string - }{ - { - name: "basic forwarding headers", - originalHeaders: map[string][]string{ - "User-Agent": {"TestClient/1.0"}, - "Accept": {"application/json"}, - "Content-Type": {"application/json"}, - }, - host: "api.example.com", - remoteAddr: "192.168.1.100:12345", - tls: false, - expectedHeaders: map[string]string{ - "X-Forwarded-For": "192.168.1.100", - "X-Forwarded-Host": "api.example.com", - "X-Forwarded-Proto": "http", - "X-Real-IP": "192.168.1.100", - "X-Forwarded-Server": "api.example.com", - "User-Agent": "TestClient/1.0", - "Accept": "application/json", - "Content-Type": "application/json", - }, - }, - { - name: "HTTPS request", - originalHeaders: map[string][]string{ - "User-Agent": {"TestClient/1.0"}, - }, - host: "secure.example.com", - remoteAddr: "203.0.113.45:54321", - tls: true, - expectedHeaders: map[string]string{ - "X-Forwarded-For": "203.0.113.45", - "X-Forwarded-Host": "secure.example.com", - "X-Forwarded-Proto": "https", - "X-Real-IP": "203.0.113.45", - "X-Forwarded-Server": "secure.example.com", - "User-Agent": "TestClient/1.0", - }, - }, - { - name: "existing X-Forwarded-For chain", - originalHeaders: map[string][]string{ - "X-Forwarded-For": {"192.168.1.50, 10.0.0.25"}, - "User-Agent": {"TestClient/1.0"}, - }, - host: "chain.example.com", - remoteAddr: "172.16.0.1:8080", - tls: false, - expectedHeaders: map[string]string{ - "X-Forwarded-For": "192.168.1.50, 10.0.0.25, 172.16.0.1", - "X-Forwarded-Host": "chain.example.com", - "X-Forwarded-Proto": "http", - "X-Real-IP": "192.168.1.50", - "X-Forwarded-Server": "chain.example.com", - "User-Agent": "TestClient/1.0", - }, - }, - { - name: "existing X-Forwarded-Proto preserved", - originalHeaders: map[string][]string{ - "X-Forwarded-Proto": {"https"}, - "User-Agent": {"TestClient/1.0"}, - }, - host: "proxy.example.com", - remoteAddr: "198.51.100.1:443", - tls: false, - expectedHeaders: map[string]string{ - "X-Forwarded-For": "198.51.100.1", - "X-Forwarded-Host": "proxy.example.com", - "X-Forwarded-Proto": "https", - "X-Real-IP": "198.51.100.1", - "X-Forwarded-Server": "proxy.example.com", - "User-Agent": "TestClient/1.0", - }, - }, - { - name: "existing X-Forwarded-Server preserved", - originalHeaders: map[string][]string{ - "X-Forwarded-Server": {"upstream.example.com"}, - "User-Agent": {"TestClient/1.0"}, - }, - host: "current.example.com", - remoteAddr: "203.0.113.100:9000", - tls: false, - expectedHeaders: map[string]string{ - "X-Forwarded-For": "203.0.113.100", - "X-Forwarded-Host": "current.example.com", - "X-Forwarded-Proto": "http", - "X-Real-IP": "203.0.113.100", - "X-Forwarded-Server": "upstream.example.com", - "User-Agent": "TestClient/1.0", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest("GET", "http://"+tt.host+"/test", nil) - req.Host = tt.host - req.RemoteAddr = tt.remoteAddr - if tt.tls { - req.TLS = &tls.ConnectionState{} - } - - for k, values := range tt.originalHeaders { - for _, v := range values { - req.Header.Add(k, v) - } - } - - result := router.prepareForwardingHeaders(req) - - for expectedKey, expectedValue := range tt.expectedHeaders { - if values, exists := result[expectedKey]; !exists { - t.Errorf("Expected header %s not found", expectedKey) - } else if len(values) == 0 || values[0] != expectedValue { - t.Errorf("Header %s = %v, want %v", expectedKey, values, []string{expectedValue}) - } - } - - if len(result["X-Forwarded-Host"]) > 0 && result["X-Forwarded-Host"][0] != tt.host { - t.Errorf("X-Forwarded-Host = %v, want %v", result["X-Forwarded-Host"][0], tt.host) - } - }) - } -} - -func TestTunnelRouter_prepareForwardingHeaders_EmptyHost(t *testing.T) { - server := &Server{ - Tunnels: make(map[string]*Tunnel), - } - router := NewTunnelRouter(server) - - req := httptest.NewRequest("GET", "http://example.com/test", nil) - req.Host = "" - req.RemoteAddr = "192.168.1.1:12345" - - result := router.prepareForwardingHeaders(req) - - if _, exists := result["X-Forwarded-Host"]; exists { - t.Error("X-Forwarded-Host should not be set when host is empty") - } - - if len(result["X-Forwarded-For"]) == 0 || result["X-Forwarded-For"][0] != "192.168.1.1" { - t.Errorf("X-Forwarded-For = %v, want %v", result["X-Forwarded-For"], []string{"192.168.1.1"}) - } -} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 2e7fa44..0000000 --- a/server/server.go +++ /dev/null @@ -1,76 +0,0 @@ -package server - -import ( - "net/http" - "sync" - - "github.com/karol-broda/funnel/shared" - - "github.com/gorilla/websocket" -) - -type Server struct { - Tunnels map[string]*Tunnel - TunnelsMu sync.RWMutex - Upgrader websocket.Upgrader - router RouterInterface - tokenStore *TokenStore -} - -type RouterInterface interface { - InvalidateCache(tunnelID string) -} - -func NewServer() *Server { - logger := shared.GetLogger("server") - - server := &Server{ - Tunnels: make(map[string]*Tunnel), - Upgrader: websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - ReadBufferSize: 1024 * 64, - WriteBufferSize: 1024 * 64, - }, - } - - logger.Info(). - Int("read_buffer_size", server.Upgrader.ReadBufferSize). - Int("write_buffer_size", server.Upgrader.WriteBufferSize). - Bool("check_origin_disabled", true). - Msg("server initialized") - - return server -} - -func (s *Server) SetRouter(router RouterInterface) { - logger := shared.GetLogger("server") - - s.router = router - logger.Debug().Msg("router configured for server") -} - -func (s *Server) SetTokenStore(tokenStore *TokenStore) { - logger := shared.GetLogger("server") - - s.tokenStore = tokenStore - if tokenStore.IsEnabled() { - logger.Info(). - Int("active_tokens", tokenStore.Count()). - Msg("token store configured - authentication enabled") - } else { - logger.Warn().Msg("token store not configured - authentication disabled") - } -} - -func (s *Server) GetTokenStore() *TokenStore { - return s.tokenStore -} - -func (s *Server) ValidateToken(token string) (*TokenRecord, bool) { - if s.tokenStore == nil { - return nil, true - } - return s.tokenStore.Validate(token) -} diff --git a/server/statistics.go b/server/statistics.go deleted file mode 100644 index 8546432..0000000 --- a/server/statistics.go +++ /dev/null @@ -1,242 +0,0 @@ -package server - -import ( - "sync" - "time" -) - -type TunnelStatistics struct { - TotalRequests int64 `json:"total_requests"` - TotalRequestsSuccess int64 `json:"total_requests_success"` - TotalRequestsError int64 `json:"total_requests_error"` - TotalBytesIn int64 `json:"total_bytes_in"` - TotalBytesOut int64 `json:"total_bytes_out"` - - AverageResponseTime float64 `json:"average_response_time_ms"` - MinResponseTime float64 `json:"min_response_time_ms"` - MaxResponseTime float64 `json:"max_response_time_ms"` - RequestsPerSecond float64 `json:"requests_per_second"` - - ConnectionsActive int `json:"connections_active"` - ConnectionsTotal int64 `json:"connections_total"` - ReconnectCount int64 `json:"reconnect_count"` - LastActivity time.Time `json:"last_activity"` - - TimeoutCount int64 `json:"timeout_count"` - ConnectionErrors int64 `json:"connection_errors"` - DataErrors int64 `json:"data_errors"` - - ClientIP string `json:"client_ip"` - UserAgent string `json:"user_agent,omitempty"` - - CreatedAt time.Time `json:"created_at"` - LastUpdated time.Time `json:"last_updated"` - - mu sync.RWMutex -} - -type HistoricalDataPoint struct { - Timestamp time.Time `json:"timestamp"` - RequestCount int64 `json:"request_count"` - BytesIn int64 `json:"bytes_in"` - BytesOut int64 `json:"bytes_out"` - ResponseTime float64 `json:"response_time_ms"` - ErrorCount int64 `json:"error_count"` - ActiveConnections int `json:"active_connections"` -} - -type TunnelHistoricalData struct { - TunnelID string `json:"tunnel_id"` - DataPoints []HistoricalDataPoint `json:"data_points"` - MaxPoints int `json:"max_points"` - Interval time.Duration `json:"interval_seconds"` - mu sync.RWMutex -} - -type ServerMetrics struct { - TotalTunnels int64 `json:"total_tunnels"` - ActiveTunnels int `json:"active_tunnels"` - TotalRequests int64 `json:"total_requests"` - TotalBytesTransferred int64 `json:"total_bytes_transferred"` - - AverageResponseTime float64 `json:"average_response_time_ms"` - ServerRequestsPerSec float64 `json:"server_requests_per_second"` - - MemoryUsage int64 `json:"memory_usage_bytes"` - CPUUsage float64 `json:"cpu_usage_percent"` - NetworkConnections int `json:"network_connections"` - - TotalErrors int64 `json:"total_errors"` - ErrorRate float64 `json:"error_rate_percent"` - - ServerUptime string `json:"server_uptime"` - LastUpdated time.Time `json:"last_updated"` - - mu sync.RWMutex -} - -func NewTunnelStatistics(tunnelID, clientIP, userAgent string) *TunnelStatistics { - now := time.Now() - return &TunnelStatistics{ - ClientIP: clientIP, - UserAgent: userAgent, - CreatedAt: now, - LastUpdated: now, - LastActivity: now, - MinResponseTime: -1, - ConnectionsActive: 1, - ConnectionsTotal: 1, - } -} - -func (ts *TunnelStatistics) RecordRequest(responseTimeMs float64, success bool, bytesIn, bytesOut int64) { - ts.mu.Lock() - defer ts.mu.Unlock() - - now := time.Now() - ts.TotalRequests++ - ts.TotalBytesIn += bytesIn - ts.TotalBytesOut += bytesOut - ts.LastActivity = now - ts.LastUpdated = now - - if success { - ts.TotalRequestsSuccess++ - } else { - ts.TotalRequestsError++ - } - - if ts.MinResponseTime == -1 || responseTimeMs < ts.MinResponseTime { - ts.MinResponseTime = responseTimeMs - } - if responseTimeMs > ts.MaxResponseTime { - ts.MaxResponseTime = responseTimeMs - } - - if ts.TotalRequests == 1 { - ts.AverageResponseTime = responseTimeMs - } else { - ts.AverageResponseTime = (ts.AverageResponseTime*float64(ts.TotalRequests-1) + responseTimeMs) / float64(ts.TotalRequests) - } - - duration := now.Sub(ts.CreatedAt).Seconds() - if duration > 0 { - ts.RequestsPerSecond = float64(ts.TotalRequests) / duration - } -} - -func (ts *TunnelStatistics) RecordError(errorType string) { - ts.mu.Lock() - defer ts.mu.Unlock() - - ts.LastUpdated = time.Now() - - switch errorType { - case "timeout": - ts.TimeoutCount++ - case "connection": - ts.ConnectionErrors++ - case "data": - ts.DataErrors++ - } -} - -func (ts *TunnelStatistics) RecordReconnect() { - ts.mu.Lock() - defer ts.mu.Unlock() - - ts.ReconnectCount++ - ts.ConnectionsTotal++ - ts.LastUpdated = time.Now() -} - -func (ts *TunnelStatistics) GetSnapshot() TunnelStatistics { - ts.mu.RLock() - defer ts.mu.RUnlock() - - return *ts -} - -func NewTunnelHistoricalData(tunnelID string, maxPoints int, interval time.Duration) *TunnelHistoricalData { - return &TunnelHistoricalData{ - TunnelID: tunnelID, - DataPoints: make([]HistoricalDataPoint, 0, maxPoints), - MaxPoints: maxPoints, - Interval: interval, - } -} - -func (thd *TunnelHistoricalData) AddDataPoint(stats TunnelStatistics) { - thd.mu.Lock() - defer thd.mu.Unlock() - - point := HistoricalDataPoint{ - Timestamp: time.Now(), - RequestCount: stats.TotalRequests, - BytesIn: stats.TotalBytesIn, - BytesOut: stats.TotalBytesOut, - ResponseTime: stats.AverageResponseTime, - ErrorCount: stats.TotalRequestsError, - ActiveConnections: stats.ConnectionsActive, - } - - thd.DataPoints = append(thd.DataPoints, point) - - if len(thd.DataPoints) > thd.MaxPoints { - thd.DataPoints = thd.DataPoints[len(thd.DataPoints)-thd.MaxPoints:] - } -} - -func (thd *TunnelHistoricalData) GetDataPoints() []HistoricalDataPoint { - thd.mu.RLock() - defer thd.mu.RUnlock() - - result := make([]HistoricalDataPoint, len(thd.DataPoints)) - copy(result, thd.DataPoints) - return result -} - -func (thd *TunnelHistoricalData) GetDataPointsInRange(start, end time.Time) []HistoricalDataPoint { - thd.mu.RLock() - defer thd.mu.RUnlock() - - var result []HistoricalDataPoint - for _, point := range thd.DataPoints { - if point.Timestamp.After(start) && point.Timestamp.Before(end) { - result = append(result, point) - } - } - return result -} - -func NewServerMetrics(startTime time.Time) *ServerMetrics { - return &ServerMetrics{ - LastUpdated: time.Now(), - } -} - -func (sm *ServerMetrics) UpdateServerMetrics(activeTunnels int, totalRequests int64, routerStats map[string]interface{}, startTime time.Time) { - sm.mu.Lock() - defer sm.mu.Unlock() - - sm.ActiveTunnels = activeTunnels - sm.TotalRequests = totalRequests - sm.ServerUptime = time.Since(startTime).String() - sm.LastUpdated = time.Now() - - uptime := time.Since(startTime).Seconds() - if uptime > 0 { - sm.ServerRequestsPerSec = float64(totalRequests) / uptime - } - - if totalRequests > 0 { - sm.ErrorRate = (float64(sm.TotalErrors) / float64(totalRequests)) * 100 - } -} - -func (sm *ServerMetrics) GetSnapshot() ServerMetrics { - sm.mu.RLock() - defer sm.mu.RUnlock() - - return *sm -} diff --git a/server/swagger.go b/server/swagger.go deleted file mode 100644 index 050ac11..0000000 --- a/server/swagger.go +++ /dev/null @@ -1,30 +0,0 @@ -package server - -import ( - "net/http" - "strings" - - "github.com/karol-broda/funnel/shared" - httpSwagger "github.com/swaggo/http-swagger/v2" -) - -func (tr *TunnelRouter) ServeSwaggerUI(w http.ResponseWriter, r *http.Request) { - logger := shared.GetLogger("server.swagger") - - logger.Info(). - Str("method", r.Method). - Str("path", r.URL.Path). - Str("remote_addr", r.RemoteAddr). - Msg("Swagger UI request received") - - path := strings.TrimPrefix(r.URL.Path, "/swagger") - - swaggerHandler := httpSwagger.Handler( - httpSwagger.URL("/api/swagger/doc.json"), - ) - - newPath := "/swagger" + path - r.URL.Path = newPath - - swaggerHandler.ServeHTTP(w, r) -} diff --git a/server/tunnel.go b/server/tunnel.go deleted file mode 100644 index 9c47c8c..0000000 --- a/server/tunnel.go +++ /dev/null @@ -1,411 +0,0 @@ -package server - -import ( - "fmt" - "sync" - "time" - - "github.com/karol-broda/funnel/shared" - - "github.com/gorilla/websocket" -) - -type Tunnel struct { - ID string - conn *websocket.Conn - incomingMessages chan *shared.Message - outgoingMessages chan *shared.Message - server *Server - closeOnce sync.Once - runOnce sync.Once - removed bool - removedMu sync.Mutex - - ResponseChannels map[string]chan *shared.Message - ResponseMu sync.RWMutex - - createdAt time.Time - messagesReceived int64 - messagesSent int64 - bytesReceived int64 - bytesSent int64 - - statistics *TunnelStatistics - historicalData *TunnelHistoricalData -} - -func (s *Server) GetTunnel(id string) (*Tunnel, bool) { - s.TunnelsMu.RLock() - defer s.TunnelsMu.RUnlock() - tunnel, exists := s.Tunnels[id] - return tunnel, exists -} - -func (s *Server) AddTunnel(id string, conn *websocket.Conn, wg *sync.WaitGroup) *Tunnel { - logger := shared.GetTunnelLogger("server.tunnel", id) - - clientIP := "unknown" - userAgent := "" - if conn != nil && conn.RemoteAddr() != nil { - clientIP = conn.RemoteAddr().String() - } - - tunnel := &Tunnel{ - ID: id, - conn: conn, - ResponseChannels: make(map[string]chan *shared.Message), - incomingMessages: make(chan *shared.Message, 100), - outgoingMessages: make(chan *shared.Message, 100), - server: s, - createdAt: time.Now(), - statistics: NewTunnelStatistics(id, clientIP, userAgent), - historicalData: NewTunnelHistoricalData(id, 1440, time.Minute), // 24 hours of minute-by-minute data - } - - s.TunnelsMu.Lock() - tunnelCount := len(s.Tunnels) - s.Tunnels[id] = tunnel - s.TunnelsMu.Unlock() - - logger.Info(). - Int("total_tunnels", tunnelCount+1). - Int("incoming_buffer_size", cap(tunnel.incomingMessages)). - Int("outgoing_buffer_size", cap(tunnel.outgoingMessages)). - Msg("tunnel added to server") - - return tunnel -} - -func (s *Server) RemoveTunnel(id string) { - s.TunnelsMu.Lock() - tunnel, exists := s.Tunnels[id] - delete(s.Tunnels, id) - remainingTunnels := len(s.Tunnels) - s.TunnelsMu.Unlock() - - if exists { - tunnel.removedMu.Lock() - tunnel.removed = true - tunnel.removedMu.Unlock() - - tunnel.closeConnection() - - if s.router != nil { - s.router.InvalidateCache(id) - } - - logger := shared.GetTunnelLogger("server.tunnel", id) - lifetime := time.Since(tunnel.createdAt) - logger.Info(). - Int("remaining_tunnels", remainingTunnels). - Dur("tunnel_lifetime", lifetime). - Int64("messages_received", tunnel.messagesReceived). - Int64("messages_sent", tunnel.messagesSent). - Int64("bytes_received", tunnel.bytesReceived). - Int64("bytes_sent", tunnel.bytesSent). - Msg("tunnel removed from server") - } else { - logger := shared.GetTunnelLogger("server.tunnel", id) - logger.Warn().Msg("attempted to remove non-existent tunnel") - } -} - -func (s *Server) TunnelExists(id string) bool { - s.TunnelsMu.RLock() - defer s.TunnelsMu.RUnlock() - _, exists := s.Tunnels[id] - return exists -} - -func (t *Tunnel) readMessages() { - defer func() { - if r := recover(); r != nil { - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - logger.Error().Msgf("recovered in readmessages: %v", r) - } - if t.incomingMessages != nil { - close(t.incomingMessages) - } - t.closeConnection() - }() - - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - logger.Debug().Msg("starting message reader goroutine") - - if t.conn == nil { - logger.Error().Msg("tunnel connection is nil, cannot read messages") - return - } - - for { - var msg shared.Message - - readStart := time.Now() - if t.conn == nil { - logger.Error().Msg("tunnel connection became nil during read") - return - } - err := t.conn.ReadJSON(&msg) - readDuration := time.Since(readStart) - - if err != nil { - logger.Error().Err(err). - Dur("read_duration", readDuration). - Int64("total_messages_received", t.messagesReceived). - Msg("websocket read failed") - t.closeConnection() - return - } - - t.messagesReceived++ - messageSize := int64(len(msg.Body)) - t.bytesReceived += messageSize - - logger.Debug(). - Str("message_type", msg.Type). - Str("request_id", msg.RequestID). - Int64("message_size", messageSize). - Dur("read_duration", readDuration). - Msg("message received from client") - - select { - case t.incomingMessages <- &msg: - default: - logger.Warn(). - Str("message_type", msg.Type). - Int("queue_capacity", cap(t.incomingMessages)). - Msg("message channel full, dropping message") - } - } -} - -func (t *Tunnel) writeMessages() { - defer func() { - if r := recover(); r != nil { - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - logger.Error().Msgf("recovered in writemessages: %v", r) - } - t.closeConnection() - }() - - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - logger.Debug().Msg("starting message writer goroutine") - - if t.conn == nil { - logger.Error().Msg("tunnel connection is nil, cannot write messages") - return - } - - for msg := range t.outgoingMessages { - writeStart := time.Now() - if t.conn == nil { - logger.Error().Msg("tunnel connection became nil during write") - return - } - err := t.conn.WriteJSON(msg) - writeDuration := time.Since(writeStart) - - if err != nil { - logger.Error().Err(err). - Str("message_type", msg.Type). - Str("request_id", msg.RequestID). - Dur("write_duration", writeDuration). - Msg("websocket write failed") - t.closeConnection() - return - } - - t.messagesSent++ - messageSize := int64(len(msg.Body)) - t.bytesSent += messageSize - - logger.Debug(). - Str("message_type", msg.Type). - Str("request_id", msg.RequestID). - Int64("message_size", messageSize). - Dur("write_duration", writeDuration). - Msg("message sent to client") - } -} - -func (t *Tunnel) SendMessage(msg *shared.Message) error { - if t.conn == nil { - return fmt.Errorf("tunnel connection is nil") - } - - if t.outgoingMessages == nil { - return fmt.Errorf("tunnel is closed, dropping message") - } - - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - if msg.TunnelID != t.ID { - logger.Error(). - Str("expected_tunnel_id", t.ID). - Str("message_tunnel_id", msg.TunnelID). - Str("request_id", msg.RequestID). - Msg("message dropped: tunnel id mismatch") - return fmt.Errorf("message dropped: tunnel id mismatch") - } - - select { - case t.outgoingMessages <- msg: - logger.Debug(). - Str("message_type", msg.Type). - Str("request_id", msg.RequestID). - Int("queue_len", len(t.outgoingMessages)). - Msg("message queued for sending") - return nil - default: - logger.Error(). - Str("message_type", msg.Type). - Str("request_id", msg.RequestID). - Int("queue_capacity", cap(t.outgoingMessages)). - Msg("outgoing message queue full, dropping message") - return fmt.Errorf("outgoing message queue full") - } -} - -func (t *Tunnel) routeMessages() { - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - logger.Debug().Msg("starting message router goroutine") - - defer func() { - t.ResponseMu.Lock() - if len(t.ResponseChannels) > 0 { - logger.Warn(). - Int("closed_channels", len(t.ResponseChannels)). - Msg("cleaned up pending response channels") - for id, ch := range t.ResponseChannels { - close(ch) - delete(t.ResponseChannels, id) - } - } - t.ResponseMu.Unlock() - logger.Debug().Msg("message router stopped") - }() - - for msg := range t.incomingMessages { - switch msg.Type { - case "response": - t.ResponseMu.RLock() - if respChan, ok := t.ResponseChannels[msg.RequestID]; ok { - select { - case respChan <- msg: - default: - logger.Warn(). - Str("request_id", msg.RequestID). - Msg("response channel was full, response dropped") - } - } - t.ResponseMu.RUnlock() - case "ping": - t.SendMessage(&shared.Message{Type: "pong"}) - default: - logger.Debug(). - Str("message_type", msg.Type). - Str("request_id", msg.RequestID). - Msg("unhandled message type in router") - } - } -} - -func (t *Tunnel) registerResponseChannel(requestID string) chan *shared.Message { - respChan := make(chan *shared.Message, 1) - t.ResponseMu.Lock() - t.ResponseChannels[requestID] = respChan - t.ResponseMu.Unlock() - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - logger.Debug(). - Str("request_id", requestID). - Msg("response channel registered") - return respChan -} - -func (t *Tunnel) unregisterResponseChannel(requestID string) { - t.ResponseMu.Lock() - defer t.ResponseMu.Unlock() - delete(t.ResponseChannels, requestID) -} - -func (t *Tunnel) Run() { - t.runOnce.Do(func() { - defer func() { - if r := recover(); r != nil { - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - logger.Error().Msgf("recovered in run: %v", r) - } - t.closeConnection() - t.removedMu.Lock() - if !t.removed && t.server != nil { - t.removed = true - t.removedMu.Unlock() - t.server.RemoveTunnel(t.ID) - } else { - t.removedMu.Unlock() - } - }() - - logger := shared.GetTunnelLogger("server.tunnel", t.ID) - logger.Debug().Msg("tunnel is now running") - - if t.conn == nil { - logger.Warn().Msg("tunnel has a nil connection, not starting goroutines") - return - } - - done := make(chan struct{}) - var wg sync.WaitGroup - wg.Add(3) - - go func() { - defer wg.Done() - t.readMessages() - }() - - go func() { - defer wg.Done() - t.writeMessages() - }() - - go func() { - defer wg.Done() - t.routeMessages() - }() - - go func() { - wg.Wait() - close(done) - }() - - <-done - logger.Debug().Msg("tunnel has stopped") - }) -} - -func (t *Tunnel) getRemoteAddr() string { - if t.conn != nil && t.conn.RemoteAddr() != nil { - return t.conn.RemoteAddr().String() - } - return "unknown" -} - -// GetRemoteAddr returns the remote address of the tunnel connection -func (t *Tunnel) GetRemoteAddr() string { - return t.getRemoteAddr() -} - -// GetCreatedAt returns the creation time of the tunnel -func (t *Tunnel) GetCreatedAt() time.Time { - return t.createdAt -} - -func (t *Tunnel) closeConnection() { - t.closeOnce.Do(func() { - if t.conn != nil { - t.conn.Close() - } - if t.outgoingMessages != nil { - close(t.outgoingMessages) - } - }) -} diff --git a/server/tunnel_test.go b/server/tunnel_test.go deleted file mode 100644 index 5b269d4..0000000 --- a/server/tunnel_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package server - -import ( - "fmt" - "sync" - "testing" - - "github.com/karol-broda/funnel/shared" -) - -func TestServerTunnelManagement(t *testing.T) { - server := NewServer() - - t.Run("add tunnel", func(t *testing.T) { - tunnelID := "test-tunnel-123" - tunnel := server.AddTunnel(tunnelID, nil, nil) - defer server.RemoveTunnel(tunnelID) - - if tunnel == nil { - t.Error("expected tunnel to be created") - return - } - - if tunnel.ID != tunnelID { - t.Errorf("expected tunnel ID %s, got %s", tunnelID, tunnel.ID) - } - - if tunnel.conn != nil { - t.Error("expected tunnel connection to be nil for this test") - } - - if tunnel.ResponseChannels == nil { - t.Error("response channels not initialized") - } - - if tunnel.createdAt.IsZero() { - t.Error("created timestamp not set") - } - }) - - t.Run("get tunnel", func(t *testing.T) { - tunnelID := "test-tunnel-get" - server.AddTunnel(tunnelID, nil, nil) - defer server.RemoveTunnel(tunnelID) - - tunnel, exists := server.GetTunnel(tunnelID) - if !exists { - t.Error("expected tunnel to exist") - } - - if tunnel.ID != tunnelID { - t.Errorf("expected tunnel ID %s, got %s", tunnelID, tunnel.ID) - } - - _, exists = server.GetTunnel("non-existent") - if exists { - t.Error("non-existent tunnel should not exist") - } - }) - - t.Run("tunnel exists", func(t *testing.T) { - tunnelID := "test-tunnel-exists" - - if server.TunnelExists(tunnelID) { - t.Error("tunnel should not exist before creation") - } - - server.AddTunnel(tunnelID, nil, nil) - defer server.RemoveTunnel(tunnelID) - - if !server.TunnelExists(tunnelID) { - t.Error("tunnel should exist after creation") - } - }) - - t.Run("remove tunnel", func(t *testing.T) { - tunnelID := "test-tunnel-remove" - server.AddTunnel(tunnelID, nil, nil) - - if !server.TunnelExists(tunnelID) { - t.Error("tunnel should exist before removal") - } - - server.RemoveTunnel(tunnelID) - - if server.TunnelExists(tunnelID) { - t.Error("tunnel should not exist after removal") - } - - server.RemoveTunnel("non-existent-tunnel") - }) -} - -func TestTunnelConcurrency(t *testing.T) { - server := NewServer() - numTunnels := 100 - - t.Run("concurrent add", func(t *testing.T) { - var wg sync.WaitGroup - for i := 0; i < numTunnels; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - tunnelID := fmt.Sprintf("concurrent-tunnel-%d", i) - server.AddTunnel(tunnelID, nil, nil) - }(i) - } - wg.Wait() - - server.TunnelsMu.RLock() - count := len(server.Tunnels) - server.TunnelsMu.RUnlock() - - if count != numTunnels { - t.Errorf("expected %d tunnels, got %d", numTunnels, count) - } - - for i := 0; i < numTunnels; i++ { - tunnelID := fmt.Sprintf("concurrent-tunnel-%d", i) - server.RemoveTunnel(tunnelID) - } - }) - - t.Run("concurrent access", func(t *testing.T) { - var wg sync.WaitGroup - for i := 0; i < numTunnels; i++ { - tunnelID := fmt.Sprintf("concurrent-tunnel-%d", i) - server.AddTunnel(tunnelID, nil, nil) - defer server.RemoveTunnel(tunnelID) - } - - for i := 0; i < numTunnels*2; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - tunnelID := fmt.Sprintf("concurrent-tunnel-%d", i%numTunnels) - _, exists := server.GetTunnel(tunnelID) - if !exists { - t.Errorf("tunnel %s should exist", tunnelID) - } - }(i) - } - wg.Wait() - }) - - t.Run("concurrent remove", func(t *testing.T) { - var wg sync.WaitGroup - for i := 0; i < numTunnels; i++ { - tunnelID := fmt.Sprintf("concurrent-tunnel-%d", i) - server.AddTunnel(tunnelID, nil, nil) - } - - for i := 0; i < numTunnels; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - tunnelID := fmt.Sprintf("concurrent-tunnel-%d", i) - server.RemoveTunnel(tunnelID) - }(i) - } - wg.Wait() - - server.TunnelsMu.RLock() - count := len(server.Tunnels) - server.TunnelsMu.RUnlock() - if count != 0 { - t.Errorf("expected 0 tunnels, got %d", count) - } - }) -} - -func TestTunnelMessageHandling(t *testing.T) { - server := NewServer() - - t.Run("send message to nil connection tunnel", func(t *testing.T) { - tunnelID := "test-send-receive" - tunnel := server.AddTunnel(tunnelID, nil, nil) - defer server.RemoveTunnel(tunnelID) - - testMsg := &shared.Message{ - Type: "test", - RequestID: "test-123", - TunnelID: tunnelID, - } - err := tunnel.SendMessage(testMsg) - if err == nil { - t.Error("expected error when sending to a nil connection tunnel, but got nil") - } - }) -} - -func TestTunnelResponseChannels(t *testing.T) { - server := NewServer() - tunnel := server.AddTunnel("test-response-channels", nil, nil) - defer server.RemoveTunnel(tunnel.ID) - - requestID := "test-req-1" - ch := tunnel.registerResponseChannel(requestID) - if ch == nil { - t.Fatal("response channel should not be nil") - } - - tunnel.ResponseMu.RLock() - _, exists := tunnel.ResponseChannels[requestID] - tunnel.ResponseMu.RUnlock() - if !exists { - t.Fatal("response channel should be registered") - } - - tunnel.unregisterResponseChannel(requestID) - - tunnel.ResponseMu.RLock() - _, exists = tunnel.ResponseChannels[requestID] - tunnel.ResponseMu.RUnlock() - if exists { - t.Fatal("response channel should be unregistered") - } -} - -func BenchmarkTunnelCreation(b *testing.B) { - server := NewServer() - b.ResetTimer() - for i := 0; i < b.N; i++ { - tunnelID := fmt.Sprintf("bench-create-%d", i) - tunnel := server.AddTunnel(tunnelID, nil, nil) - server.RemoveTunnel(tunnel.ID) - } -} - -func BenchmarkTunnelLookup(b *testing.B) { - server := NewServer() - tunnel := server.AddTunnel("bench-lookup", nil, nil) - defer server.RemoveTunnel(tunnel.ID) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - server.GetTunnel(tunnel.ID) - } -} diff --git a/server/websocket.go b/server/websocket.go deleted file mode 100644 index 2d8c4cc..0000000 --- a/server/websocket.go +++ /dev/null @@ -1,137 +0,0 @@ -package server - -import ( - "net/http" - "strings" - "time" - - "github.com/karol-broda/funnel/shared" -) - -func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) { - logger := shared.GetLogger("server.websocket") - - logger.Debug(). - Str("remote_addr", r.RemoteAddr). - Str("user_agent", r.UserAgent()). - Str("origin", r.Header.Get("Origin")). - Msg("websocket connection attempt") - - token := extractToken(r) - tokenRecord, valid := s.ValidateToken(token) - if !valid { - logger.Warn(). - Str("remote_addr", r.RemoteAddr). - Msg("websocket upgrade rejected - invalid or missing token") - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - - tokenName := "" - if tokenRecord != nil { - tokenName = tokenRecord.Name - } - - tunnelID := r.URL.Query().Get("id") - if tunnelID == "" { - logger.Warn(). - Str("remote_addr", r.RemoteAddr). - Str("token_name", tokenName). - Msg("websocket upgrade rejected - missing tunnel id") - http.Error(w, "tunnel id required", http.StatusBadRequest) - return - } - - if err := shared.ValidateTunnelID(tunnelID); err != nil { - logger.Warn(). - Str("remote_addr", r.RemoteAddr). - Str("tunnel_id", tunnelID). - Str("token_name", tokenName). - Err(err). - Msg("websocket upgrade rejected - invalid tunnel id format") - http.Error(w, "invalid tunnel id format: "+err.Error(), http.StatusBadRequest) - return - } - - tunnelLogger := shared.GetTunnelLogger("server.websocket", tunnelID) - - tunnelLogger.Info(). - Str("remote_addr", r.RemoteAddr). - Str("user_agent", r.UserAgent()). - Str("token_name", tokenName). - Msg("websocket upgrade requested") - - if s.TunnelExists(tunnelID) { - tunnelLogger.Warn(). - Str("remote_addr", r.RemoteAddr). - Msg("websocket upgrade rejected - tunnel id already in use") - http.Error(w, "tunnel id already in use", http.StatusConflict) - return - } - - upgradeStart := time.Now() - conn, err := s.Upgrader.Upgrade(w, r, nil) - upgradeDuration := time.Since(upgradeStart) - - if err != nil { - tunnelLogger.Error().Err(err). - Dur("upgrade_duration", upgradeDuration). - Str("remote_addr", r.RemoteAddr). - Msg("websocket upgrade failed") - return - } - - tunnelLogger.Info(). - Dur("upgrade_duration", upgradeDuration). - Str("remote_addr", r.RemoteAddr). - Msg("websocket upgrade successful") - - tunnel := s.AddTunnel(tunnelID, conn, nil) - tunnelLogger.Info().Msg("tunnel connected via websocket") - - defer func() { - s.RemoveTunnel(tunnelID) - tunnelLogger.Info().Msg("tunnel disconnected and cleaned up") - }() - - s.setupWebSocketConnection(tunnel) - tunnel.Run() -} - -func (s *Server) setupWebSocketConnection(tunnel *Tunnel) { - logger := shared.GetTunnelLogger("server.websocket", tunnel.ID) - - readDeadline := 300 * time.Second - if err := tunnel.conn.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { - logger.Error().Err(err).Msg("failed to set initial read deadline") - return - } - - logger.Debug(). - Dur("read_deadline", readDeadline). - Msg("websocket read deadline configured") - - tunnel.conn.SetPingHandler(func(appData string) error { - logger.Debug().Msg("ping received, read deadline extended") - if err := tunnel.conn.SetReadDeadline(time.Now().Add(readDeadline)); err != nil { - logger.Error().Err(err).Msg("failed to extend read deadline on ping") - } - return nil - }) - - logger.Debug().Msg("websocket connection setup completed") -} - -func extractToken(r *http.Request) string { - authHeader := r.Header.Get("Authorization") - if authHeader != "" { - return strings.TrimPrefix(authHeader, "Bearer ") - } - - token := r.URL.Query().Get("token") - if token != "" { - return token - } - - return "" -} diff --git a/server/websocket_test.go b/server/websocket_test.go deleted file mode 100644 index 2df71a9..0000000 --- a/server/websocket_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package server - -import ( - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/karol-broda/funnel/shared" -) - -func TestWebSocketTunnelIDValidation(t *testing.T) { - server := NewServer() - - tests := []struct { - name string - tunnelID string - expectedStatus int - expectedError string - }{ - { - name: "valid tunnel ID", - tunnelID: "abc123", - expectedStatus: http.StatusInternalServerError, // websocket upgrade fails in tests due to httptest.ResponseRecorder limitations - }, - { - name: "valid tunnel ID with hyphens", - tunnelID: "abc-123-def", - expectedStatus: http.StatusInternalServerError, // websocket upgrade fails in tests due to httptest.ResponseRecorder limitations - }, - { - name: "empty tunnel ID", - tunnelID: "", - expectedStatus: http.StatusBadRequest, - expectedError: "tunnel id required", - }, - { - name: "tunnel ID with uppercase", - tunnelID: "Abc123", - expectedStatus: http.StatusBadRequest, - expectedError: "invalid tunnel id format", - }, - { - name: "tunnel ID too short", - tunnelID: "ab", - expectedStatus: http.StatusBadRequest, - expectedError: "invalid tunnel id format", - }, - { - name: "tunnel ID too long", - tunnelID: strings.Repeat("a", 64), - expectedStatus: http.StatusBadRequest, - expectedError: "invalid tunnel id format", - }, - { - name: "tunnel ID starts with hyphen", - tunnelID: "-abc123", - expectedStatus: http.StatusBadRequest, - expectedError: "invalid tunnel id format", - }, - { - name: "tunnel ID ends with hyphen", - tunnelID: "abc123-", - expectedStatus: http.StatusBadRequest, - expectedError: "invalid tunnel id format", - }, - { - name: "tunnel ID with invalid characters", - tunnelID: "abc_123", - expectedStatus: http.StatusBadRequest, - expectedError: "invalid tunnel id format", - }, - { - name: "tunnel ID with spaces", - tunnelID: "abc 123", - expectedStatus: http.StatusBadRequest, - expectedError: "invalid tunnel id format", - }, - { - name: "tunnel ID with special characters", - tunnelID: "abc@123", - expectedStatus: http.StatusBadRequest, - expectedError: "invalid tunnel id format", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // create request with tunnel ID - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Connection", "upgrade") - req.Header.Set("Upgrade", "websocket") - req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") - req.Header.Set("Sec-WebSocket-Version", "13") - - // add tunnel ID as query parameter - if tt.tunnelID != "" { - q := req.URL.Query() - q.Add("id", tt.tunnelID) - req.URL.RawQuery = q.Encode() - } - - // create response recorder - w := httptest.NewRecorder() - - // call the handler - server.HandleWebSocket(w, req) - - // check status code - if w.Code != tt.expectedStatus { - t.Errorf("HandleWebSocket() status = %v, want %v", w.Code, tt.expectedStatus) - } - - // check error message if expected - if tt.expectedError != "" { - body := w.Body.String() - if !strings.Contains(body, tt.expectedError) { - t.Errorf("HandleWebSocket() body = %v, want to contain %v", body, tt.expectedError) - } - } - }) - } -} - -func TestWebSocketTunnelIDConflict(t *testing.T) { - server := NewServer() - - // add a tunnel first - existingTunnelID := "existing123" - server.AddTunnel(existingTunnelID, nil, nil) - defer server.RemoveTunnel(existingTunnelID) - - // try to create another tunnel with the same ID - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("Connection", "upgrade") - req.Header.Set("Upgrade", "websocket") - req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") - req.Header.Set("Sec-WebSocket-Version", "13") - - q := req.URL.Query() - q.Add("id", existingTunnelID) - req.URL.RawQuery = q.Encode() - - w := httptest.NewRecorder() - server.HandleWebSocket(w, req) - - if w.Code != http.StatusConflict { - t.Errorf("HandleWebSocket() status = %v, want %v", w.Code, http.StatusConflict) - } - - body := w.Body.String() - if !strings.Contains(body, "tunnel id already in use") { - t.Errorf("HandleWebSocket() body = %v, want to contain 'tunnel id already in use'", body) - } -} - -func TestWebSocketTunnelIDGeneration(t *testing.T) { - // test that generated IDs are valid - for i := 0; i < 100; i++ { - id := shared.MustGenerateDomainSafeID() - - if err := shared.ValidateTunnelID(id); err != nil { - t.Errorf("Generated tunnel ID %s is invalid: %v", id, err) - } - - // ensure it's domain-safe (no uppercase) - for _, char := range id { - if char >= 'A' && char <= 'Z' { - t.Errorf("Generated tunnel ID %s contains uppercase character: %c", id, char) - } - } - } -} - -func TestWebSocketURLParsing(t *testing.T) { - tests := []struct { - name string - rawURL string - expected string - }{ - { - name: "simple tunnel ID", - rawURL: "/?id=abc123", - expected: "abc123", - }, - { - name: "tunnel ID with hyphens", - rawURL: "/?id=abc-123-def", - expected: "abc-123-def", - }, - { - name: "tunnel ID with other params", - rawURL: "/?id=abc123&other=value", - expected: "abc123", - }, - { - name: "URL encoded tunnel ID", - rawURL: "/?id=" + url.QueryEscape("abc-123"), - expected: "abc-123", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest("GET", tt.rawURL, nil) - tunnelID := req.URL.Query().Get("id") - - if tunnelID != tt.expected { - t.Errorf("URL parsing tunnelID = %v, want %v", tunnelID, tt.expected) - } - }) - } -} diff --git a/shared/go.mod b/shared/go.mod deleted file mode 100644 index ef4196a..0000000 --- a/shared/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -module github.com/karol-broda/funnel/shared - -go 1.24.4 - -require github.com/rs/zerolog v1.34.0 - -require ( - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/sys v0.32.0 // indirect -) diff --git a/shared/go.sum b/shared/go.sum deleted file mode 100644 index 1d8cb30..0000000 --- a/shared/go.sum +++ /dev/null @@ -1,15 +0,0 @@ -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= diff --git a/shared/logging.go b/shared/logging.go deleted file mode 100644 index 417782b..0000000 --- a/shared/logging.go +++ /dev/null @@ -1,81 +0,0 @@ -package shared - -import ( - "os" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -type LogConfig struct { - Level string `json:"level"` - Format string `json:"format"` - TimeFormat string `json:"time_format"` -} - -func DefaultLogConfig() LogConfig { - return LogConfig{ - Level: "debug", - Format: "console", - TimeFormat: time.RFC3339, - } -} - -func ProductionLogConfig() LogConfig { - return LogConfig{ - Level: "info", - Format: "json", - TimeFormat: time.RFC3339, - } -} - -func InitializeLogging(config LogConfig) { - level := zerolog.InfoLevel - switch config.Level { - case "debug": - level = zerolog.DebugLevel - case "info": - level = zerolog.InfoLevel - case "warn": - level = zerolog.WarnLevel - case "error": - level = zerolog.ErrorLevel - case "trace": - level = zerolog.TraceLevel - } - - if config.Format == "console" { - log.Logger = log.Output(zerolog.ConsoleWriter{ - Out: os.Stderr, - TimeFormat: config.TimeFormat, - NoColor: false, - }).Level(level).With().Timestamp().Caller().Logger() - } else { - log.Logger = zerolog.New(os.Stderr). - Level(level). - With(). - Timestamp(). - Caller(). - Logger() - } -} - -func GetLogger(component string) zerolog.Logger { - return log.With().Str("component", component).Logger() -} - -func GetTunnelLogger(component, tunnelID string) zerolog.Logger { - return log.With(). - Str("component", component). - Str("tunnel_id", tunnelID). - Logger() -} - -func GetRequestLogger(component, tunnelID, requestID string) zerolog.Logger { - return log.With(). - Str("component", component). - Str("tunnel_id", tunnelID). - Str("request_id", requestID). - Logger() -} diff --git a/shared/message.go b/shared/message.go deleted file mode 100644 index 7819a69..0000000 --- a/shared/message.go +++ /dev/null @@ -1,12 +0,0 @@ -package shared - -type Message struct { - Type string `json:"type"` - TunnelID string `json:"tunnel_id,omitempty"` - RequestID string `json:"request_id,omitempty"` - Method string `json:"method,omitempty"` - Path string `json:"path,omitempty"` - Headers map[string][]string `json:"headers,omitempty"` - Body []byte `json:"body,omitempty"` - Status int `json:"status,omitempty"` -} diff --git a/shared/message_test.go b/shared/message_test.go deleted file mode 100644 index 3f99171..0000000 --- a/shared/message_test.go +++ /dev/null @@ -1,383 +0,0 @@ -package shared - -import ( - "encoding/json" - "reflect" - "testing" -) - -// messagesEqual compares two Message structs for equality, -// treating nil and empty maps/slices as equivalent -func messagesEqual(a, b Message) bool { - if a.Type != b.Type || - a.RequestID != b.RequestID || - a.Method != b.Method || - a.Path != b.Path || - a.Status != b.Status { - return false - } - - // compare headers - treat nil and empty maps as equal - if !headersEqual(a.Headers, b.Headers) { - return false - } - - // compare body - treat nil and empty slices as equal - return bodiesEqual(a.Body, b.Body) -} - -// headersEqual compares two header maps, treating nil and empty maps as equal -func headersEqual(a, b map[string][]string) bool { - if len(a) == 0 && len(b) == 0 { - return true - } - return reflect.DeepEqual(a, b) -} - -// bodiesEqual compares two byte slices, treating nil and empty slices as equal -func bodiesEqual(a, b []byte) bool { - if len(a) == 0 && len(b) == 0 { - return true - } - return reflect.DeepEqual(a, b) -} - -func TestMessageJSONSerialization(t *testing.T) { - tests := []struct { - name string - message Message - }{ - { - name: "complete message", - message: Message{ - Type: "request", - RequestID: "test-123", - Method: "GET", - Path: "/api/test", - Headers: map[string][]string{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer token123"}, - }, - Body: []byte("test body content"), - Status: 200, - }, - }, - { - name: "minimal message", - message: Message{ - Type: "ping", - }, - }, - { - name: "response message", - message: Message{ - Type: "response", - RequestID: "req-456", - Status: 404, - Headers: map[string][]string{ - "Content-Type": {"text/plain"}, - }, - Body: []byte("Not Found"), - }, - }, - { - name: "empty headers", - message: Message{ - Type: "request", - Method: "POST", - Path: "/submit", - Headers: map[string][]string{}, - Body: []byte("{}"), - }, - }, - { - name: "nil headers", - message: Message{ - Type: "request", - Method: "DELETE", - Path: "/resource/123", - }, - }, - { - name: "empty body", - message: Message{ - Type: "request", - Method: "HEAD", - Path: "/health", - Body: []byte{}, - }, - }, - { - name: "nil body", - message: Message{ - Type: "request", - Method: "OPTIONS", - Path: "/api", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name+" marshal", func(t *testing.T) { - data, err := json.Marshal(tt.message) - if err != nil { - t.Errorf("failed to marshal message: %v", err) - return - } - - if len(data) == 0 { - t.Error("marshaled data is empty") - } - }) - - t.Run(tt.name+" roundtrip", func(t *testing.T) { - // marshal - data, err := json.Marshal(tt.message) - if err != nil { - t.Errorf("failed to marshal message: %v", err) - return - } - - // unmarshal - var unmarshaled Message - err = json.Unmarshal(data, &unmarshaled) - if err != nil { - t.Errorf("failed to unmarshal message: %v", err) - return - } - - // compare - if !messagesEqual(tt.message, unmarshaled) { - t.Errorf("roundtrip failed:\noriginal: %+v\nunmarshaled: %+v", tt.message, unmarshaled) - } - }) - } -} - -func TestMessageFieldValidation(t *testing.T) { - tests := []struct { - name string - jsonData string - wantErr bool - }{ - { - name: "valid JSON", - jsonData: `{"type":"request","method":"GET","path":"/test"}`, - wantErr: false, - }, - { - name: "invalid JSON", - jsonData: `{"type":"request","method":"GET"`, - wantErr: true, - }, - { - name: "empty JSON object", - jsonData: `{}`, - wantErr: false, - }, - { - name: "null values", - jsonData: `{"type":null,"method":null}`, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var msg Message - err := json.Unmarshal([]byte(tt.jsonData), &msg) - - if tt.wantErr && err == nil { - t.Error("expected error but got none") - } - if !tt.wantErr && err != nil { - t.Errorf("unexpected error: %v", err) - } - }) - } -} - -func TestMessageHeaders(t *testing.T) { - tests := []struct { - name string - headers map[string][]string - }{ - { - name: "single value headers", - headers: map[string][]string{ - "Content-Type": {"application/json"}, - "Accept": {"application/json"}, - }, - }, - { - name: "multi-value headers", - headers: map[string][]string{ - "Accept": {"text/html", "application/xml"}, - "Cache-Control": {"no-cache", "no-store"}, - }, - }, - { - name: "empty value headers", - headers: map[string][]string{ - "X-Custom": {""}, - "X-Empty": {}, - }, - }, - { - name: "mixed case headers", - headers: map[string][]string{ - "Content-TYPE": {"text/plain"}, - "accept": {"*/*"}, - "X-Custom": {"value"}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - msg := Message{ - Type: "request", - Headers: tt.headers, - } - - data, err := json.Marshal(msg) - if err != nil { - t.Errorf("failed to marshal: %v", err) - return - } - - var unmarshaled Message - err = json.Unmarshal(data, &unmarshaled) - if err != nil { - t.Errorf("failed to unmarshal: %v", err) - return - } - - if !messagesEqual(msg, unmarshaled) { - t.Errorf("headers mismatch:\noriginal: %+v\nunmarshaled: %+v", msg.Headers, unmarshaled.Headers) - } - }) - } -} - -func TestMessageBodyHandling(t *testing.T) { - tests := []struct { - name string - body []byte - }{ - { - name: "text body", - body: []byte("hello world"), - }, - { - name: "json body", - body: []byte(`{"key":"value","number":42}`), - }, - { - name: "binary body", - body: []byte{0x00, 0x01, 0x02, 0xFF, 0xFE}, - }, - { - name: "empty body", - body: []byte{}, - }, - { - name: "nil body", - body: nil, - }, - { - name: "large body", - body: make([]byte, 10000), // 10KB of zeros - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - msg := Message{ - Type: "request", - Body: tt.body, - } - - data, err := json.Marshal(msg) - if err != nil { - t.Errorf("failed to marshal: %v", err) - return - } - - var unmarshaled Message - err = json.Unmarshal(data, &unmarshaled) - if err != nil { - t.Errorf("failed to unmarshal: %v", err) - return - } - - if !messagesEqual(msg, unmarshaled) { - t.Errorf("body mismatch:\noriginal: %+v\nunmarshaled: %+v", msg.Body, unmarshaled.Body) - } - }) - } -} - -// benchmark tests -func BenchmarkMessageMarshal(b *testing.B) { - msg := Message{ - Type: "request", - RequestID: "bench-test-123", - Method: "POST", - Path: "/api/benchmark", - Headers: map[string][]string{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer token123"}, - "User-Agent": {"benchmark-client/1.0"}, - }, - Body: []byte(`{"test":"data","number":42,"array":[1,2,3]}`), - } - - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _, err := json.Marshal(msg) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkMessageUnmarshal(b *testing.B) { - data := []byte(`{"type":"request","request_id":"bench-test-123","method":"POST","path":"/api/benchmark","headers":{"Content-Type":["application/json"],"Authorization":["Bearer token123"]},"body":"eyJ0ZXN0IjoiZGF0YSJ9"}`) - - b.ReportAllocs() - for i := 0; i < b.N; i++ { - var msg Message - err := json.Unmarshal(data, &msg) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkMessageRoundtrip(b *testing.B) { - msg := Message{ - Type: "request", - RequestID: "bench-test-123", - Method: "GET", - Path: "/api/test", - Headers: map[string][]string{ - "Accept": {"application/json"}, - }, - Body: []byte("test data"), - } - - b.ReportAllocs() - for i := 0; i < b.N; i++ { - data, err := json.Marshal(msg) - if err != nil { - b.Fatal(err) - } - - var unmarshaled Message - err = json.Unmarshal(data, &unmarshaled) - if err != nil { - b.Fatal(err) - } - } -} diff --git a/shared/utils.go b/shared/utils.go deleted file mode 100644 index 7197e5a..0000000 --- a/shared/utils.go +++ /dev/null @@ -1,188 +0,0 @@ -package shared - -import ( - "crypto/rand" - "errors" - "math" - "regexp" - "strings" -) - -var defaultAlphabet = []rune("_-0123456789abcdefghijklmnopqrstuvwxyz") -var domainSafeAlphabet = []rune("0123456789abcdefghijklmnopqrstuvwxyz") - -const defaultSize = 8 - -var tunnelIDRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*[a-z0-9]$`) - -func getMask(alphabetSize int) int { - for i := 1; i <= 8; i++ { - mask := (1 << uint(i)) - 1 - if mask >= alphabetSize-1 { - return mask - } - } - return 0 -} - -func ValidateTunnelID(id string) error { - logger := GetLogger("shared.utils") - - if len(id) == 0 { - logger.Error().Msg("tunnel ID cannot be empty") - return errors.New("tunnel ID cannot be empty") - } - - if len(id) < 3 { - logger.Error().Int("length", len(id)).Msg("tunnel ID too short") - return errors.New("tunnel ID must be at least 3 characters long") - } - - if len(id) > 63 { - logger.Error().Int("length", len(id)).Msg("tunnel ID too long") - return errors.New("tunnel ID must be no more than 63 characters long") - } - - lowerID := strings.ToLower(id) - if id != lowerID { - logger.Error().Str("id", id).Str("expected", lowerID).Msg("tunnel ID contains uppercase letters") - return errors.New("tunnel ID must be lowercase") - } - - if !tunnelIDRegex.MatchString(id) { - logger.Error().Str("id", id).Msg("tunnel ID contains invalid characters") - return errors.New("tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen") - } - - return nil -} - -func GenerateDomainSafeID(length ...int) (string, error) { - logger := GetLogger("shared.utils") - - var size int - switch { - case len(length) == 0: - size = defaultSize - case len(length) == 1: - size = length[0] - if size < 3 { - logger.Error().Int("requested_length", size).Msg("domain-safe ID length too short") - return "", errors.New("domain-safe ID must be at least 3 characters long") - } - if size > 63 { - logger.Error().Int("requested_length", size).Msg("domain-safe ID length too long") - return "", errors.New("domain-safe ID must be no more than 63 characters long") - } - default: - logger.Error().Int("param_count", len(length)).Msg("too many parameters for domain-safe ID generation") - return "", errors.New("unexpected parameter") - } - - return GenerateNanoIDWithAlphabet(string(domainSafeAlphabet), size) -} - -func MustGenerateDomainSafeID(length ...int) string { - logger := GetLogger("shared.utils") - - id, err := GenerateDomainSafeID(length...) - if err != nil { - logger.Error().Err(err).Msg("critical failure in domain-safe ID generation - panicking") - panic(err) - } - return id -} - -func GenerateNanoIDWithAlphabet(alphabet string, size int) (string, error) { - logger := GetLogger("shared.utils") - - chars := []rune(alphabet) - - if len(alphabet) == 0 || len(alphabet) > 255 { - logger.Error().Int("alphabet_length", len(alphabet)).Msg("invalid alphabet length for ID generation") - return "", errors.New("alphabet must not be empty and contain no more than 255 chars") - } - if size <= 0 { - logger.Error().Int("size", size).Msg("invalid size for ID generation") - return "", errors.New("size must be positive integer") - } - - mask := getMask(len(chars)) - ceilArg := 1.6 * float64(mask*size) / float64(len(alphabet)) - step := int(math.Ceil(ceilArg)) - - id := make([]rune, size) - bytes := make([]byte, step) - for j := 0; ; { - _, err := rand.Read(bytes) - if err != nil { - logger.Error().Err(err).Msg("failed to read random bytes for ID generation") - return "", err - } - for i := 0; i < step; i++ { - currByte := bytes[i] & byte(mask) - if currByte < byte(len(chars)) { - id[j] = chars[currByte] - j++ - if j == size { - generatedID := string(id[:size]) - return generatedID, nil - } - } - } - } -} - -func GenerateNanoID(length ...int) (string, error) { - logger := GetLogger("shared.utils") - - var size int - switch { - case len(length) == 0: - size = defaultSize - case len(length) == 1: - size = length[0] - if size < 0 { - logger.Error().Int("requested_length", size).Msg("negative ID length requested") - return "", errors.New("negative id length") - } - default: - logger.Error().Int("param_count", len(length)).Msg("too many parameters for ID generation") - return "", errors.New("unexpected parameter") - } - - bytes := make([]byte, size) - _, err := rand.Read(bytes) - if err != nil { - logger.Error().Err(err).Msg("failed to read random bytes for standard ID generation") - return "", err - } - - id := make([]rune, size) - alphabetLen := len(defaultAlphabet) - for i := 0; i < size; i++ { - id[i] = defaultAlphabet[int(bytes[i])%alphabetLen] - } - - generatedID := string(id[:size]) - return generatedID, nil -} - -func MustGenerateNanoID(length ...int) string { - logger := GetLogger("shared.utils") - - id, err := GenerateNanoID(length...) - if err != nil { - logger.Error().Err(err).Msg("critical failure in ID generation - panicking") - panic(err) - } - return id -} - -func GetBaseDomain(domain string) string { - parts := strings.Split(domain, ".") - if len(parts) > 2 { - return strings.Join(parts[len(parts)-2:], ".") - } - return domain -} diff --git a/shared/utils_test.go b/shared/utils_test.go deleted file mode 100644 index 3e7ffcb..0000000 --- a/shared/utils_test.go +++ /dev/null @@ -1,540 +0,0 @@ -package shared - -import ( - "regexp" - "strings" - "testing" -) - -func TestGenerateNanoID(t *testing.T) { - tests := []struct { - name string - length []int - expectedLen int - shouldError bool - }{ - { - name: "default length", - length: []int{}, - expectedLen: 8, - shouldError: false, - }, - { - name: "custom length 12", - length: []int{12}, - expectedLen: 12, - shouldError: false, - }, - { - name: "zero length", - length: []int{0}, - expectedLen: 0, - shouldError: false, - }, - { - name: "negative length", - length: []int{-1}, - expectedLen: 0, - shouldError: true, - }, - { - name: "too many parameters", - length: []int{8, 10}, - expectedLen: 0, - shouldError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - id, err := GenerateNanoID(tt.length...) - - if tt.shouldError { - if err == nil { - t.Errorf("expected error but got none") - } - return - } - - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - if len(id) != tt.expectedLen { - t.Errorf("expected length %d, got %d", tt.expectedLen, len(id)) - } - - // verify characters are from the default alphabet - validChars := regexp.MustCompile(`^[_\-0-9a-zA-Z]*$`) - if !validChars.MatchString(id) { - t.Errorf("generated ID contains invalid characters: %s", id) - } - }) - } -} - -func TestGenerateNanoIDUniqueness(t *testing.T) { - generated := make(map[string]bool) - iterations := 1000 - - for i := 0; i < iterations; i++ { - id, err := GenerateNanoID() - if err != nil { - t.Fatalf("unexpected error generating ID: %v", err) - } - - if generated[id] { - t.Errorf("duplicate ID generated: %s", id) - } - generated[id] = true - } - - if len(generated) != iterations { - t.Errorf("expected %d unique IDs, got %d", iterations, len(generated)) - } -} - -func TestGenerateNanoIDWithAlphabet(t *testing.T) { - tests := []struct { - name string - alphabet string - size int - shouldError bool - errorMsg string - }{ - { - name: "valid alphabet and size", - alphabet: "abc123", - size: 8, - shouldError: false, - }, - { - name: "single character alphabet", - alphabet: "a", - size: 5, - shouldError: false, - }, - { - name: "empty alphabet", - alphabet: "", - size: 8, - shouldError: true, - errorMsg: "alphabet must not be empty", - }, - { - name: "too large alphabet", - alphabet: strings.Repeat("a", 256), - size: 8, - shouldError: true, - errorMsg: "alphabet must not be empty and contain no more than 255 chars", - }, - { - name: "zero size", - alphabet: "abc", - size: 0, - shouldError: true, - errorMsg: "size must be positive integer", - }, - { - name: "negative size", - alphabet: "abc", - size: -1, - shouldError: true, - errorMsg: "size must be positive integer", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - id, err := GenerateNanoIDWithAlphabet(tt.alphabet, tt.size) - - if tt.shouldError { - if err == nil { - t.Errorf("expected error but got none") - return - } - if !strings.Contains(err.Error(), tt.errorMsg) { - t.Errorf("expected error containing '%s', got '%s'", tt.errorMsg, err.Error()) - } - return - } - - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - if len(id) != tt.size { - t.Errorf("expected length %d, got %d", tt.size, len(id)) - } - - // verify all characters are from the provided alphabet - for _, char := range id { - if !strings.ContainsRune(tt.alphabet, char) { - t.Errorf("generated ID contains character '%c' not in alphabet '%s'", char, tt.alphabet) - } - } - }) - } -} - -func TestGenerateNanoIDWithAlphabetUniqueness(t *testing.T) { - alphabet := "abcdef123456" - size := 8 - generated := make(map[string]bool) - iterations := 500 - - for i := 0; i < iterations; i++ { - id, err := GenerateNanoIDWithAlphabet(alphabet, size) - if err != nil { - t.Fatalf("unexpected error generating ID: %v", err) - } - - if generated[id] { - t.Errorf("duplicate ID generated: %s", id) - } - generated[id] = true - } -} - -func TestMustGenerateNanoID(t *testing.T) { - t.Run("successful generation", func(t *testing.T) { - defer func() { - if r := recover(); r != nil { - t.Errorf("unexpected panic: %v", r) - } - }() - - id := MustGenerateNanoID(10) - if len(id) != 10 { - t.Errorf("expected length 10, got %d", len(id)) - } - }) - - t.Run("panic on error", func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Errorf("expected panic but got none") - } - }() - - // this should panic due to negative length - MustGenerateNanoID(-1) - }) -} - -func TestGetMask(t *testing.T) { - tests := []struct { - alphabetSize int - expected int - }{ - {1, 1}, - {2, 1}, - {3, 3}, - {4, 3}, - {8, 7}, - {16, 15}, - {32, 31}, - {64, 63}, - {128, 127}, - {256, 255}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := getMask(tt.alphabetSize) - if result != tt.expected { - t.Errorf("getMask(%d) = %d, want %d", tt.alphabetSize, result, tt.expected) - } - }) - } -} - -// benchmark tests -func BenchmarkGenerateNanoID(b *testing.B) { - b.Run("default length", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _, err := GenerateNanoID() - if err != nil { - b.Fatal(err) - } - } - }) - - b.Run("length 16", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _, err := GenerateNanoID(16) - if err != nil { - b.Fatal(err) - } - } - }) -} - -func BenchmarkGenerateNanoIDWithAlphabet(b *testing.B) { - alphabet := "0123456789abcdef" - - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _, err := GenerateNanoIDWithAlphabet(alphabet, 8) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkMustGenerateNanoID(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = MustGenerateNanoID() - } -} - -func TestValidateTunnelID(t *testing.T) { - tests := []struct { - name string - id string - wantErr bool - errMsg string - }{ - { - name: "valid lowercase alphanumeric", - id: "abc123", - wantErr: false, - }, - { - name: "valid with hyphens", - id: "abc-123-def", - wantErr: false, - }, - { - name: "valid minimum length", - id: "abc", - wantErr: false, - }, - { - name: "valid maximum length", - id: strings.Repeat("a", 63), - wantErr: false, - }, - { - name: "empty string", - id: "", - wantErr: true, - errMsg: "tunnel ID cannot be empty", - }, - { - name: "too short", - id: "ab", - wantErr: true, - errMsg: "tunnel ID must be at least 3 characters long", - }, - { - name: "too long", - id: strings.Repeat("a", 64), - wantErr: true, - errMsg: "tunnel ID must be no more than 63 characters long", - }, - { - name: "contains uppercase", - id: "Abc123", - wantErr: true, - errMsg: "tunnel ID must be lowercase", - }, - { - name: "starts with hyphen", - id: "-abc123", - wantErr: true, - errMsg: "tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen", - }, - { - name: "ends with hyphen", - id: "abc123-", - wantErr: true, - errMsg: "tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen", - }, - { - name: "contains invalid characters", - id: "abc_123", - wantErr: true, - errMsg: "tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen", - }, - { - name: "contains spaces", - id: "abc 123", - wantErr: true, - errMsg: "tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen", - }, - { - name: "contains special characters", - id: "abc@123", - wantErr: true, - errMsg: "tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ValidateTunnelID(tt.id) - if tt.wantErr { - if err == nil { - t.Errorf("ValidateTunnelID() expected error but got none") - return - } - if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { - t.Errorf("ValidateTunnelID() error = %v, want error containing %v", err, tt.errMsg) - } - } else { - if err != nil { - t.Errorf("ValidateTunnelID() unexpected error = %v", err) - } - } - }) - } -} - -func TestGenerateDomainSafeID(t *testing.T) { - tests := []struct { - name string - length []int - wantErr bool - wantLength int - }{ - { - name: "default length", - length: []int{}, - wantErr: false, - wantLength: 8, - }, - { - name: "custom length 10", - length: []int{10}, - wantErr: false, - wantLength: 10, - }, - { - name: "minimum length 3", - length: []int{3}, - wantErr: false, - wantLength: 3, - }, - { - name: "maximum length 63", - length: []int{63}, - wantErr: false, - wantLength: 63, - }, - { - name: "too short", - length: []int{2}, - wantErr: true, - }, - { - name: "too long", - length: []int{64}, - wantErr: true, - }, - { - name: "too many parameters", - length: []int{8, 10}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - id, err := GenerateDomainSafeID(tt.length...) - if tt.wantErr { - if err == nil { - t.Errorf("GenerateDomainSafeID() expected error but got none") - } - return - } - - if err != nil { - t.Errorf("GenerateDomainSafeID() unexpected error = %v", err) - return - } - - if len(id) != tt.wantLength { - t.Errorf("GenerateDomainSafeID() length = %v, want %v", len(id), tt.wantLength) - } - - // validate the generated ID - if err := ValidateTunnelID(id); err != nil { - t.Errorf("GenerateDomainSafeID() generated invalid ID: %v, error: %v", id, err) - } - - // check that it only contains domain-safe characters - for _, char := range id { - if !((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9')) { - t.Errorf("GenerateDomainSafeID() generated ID contains invalid character: %c in %s", char, id) - } - } - }) - } -} - -func TestMustGenerateDomainSafeID(t *testing.T) { - t.Run("successful generation", func(t *testing.T) { - id := MustGenerateDomainSafeID() - if len(id) != 8 { - t.Errorf("MustGenerateDomainSafeID() length = %v, want 8", len(id)) - } - - if err := ValidateTunnelID(id); err != nil { - t.Errorf("MustGenerateDomainSafeID() generated invalid ID: %v, error: %v", id, err) - } - }) - - t.Run("custom length", func(t *testing.T) { - id := MustGenerateDomainSafeID(12) - if len(id) != 12 { - t.Errorf("MustGenerateDomainSafeID(12) length = %v, want 12", len(id)) - } - - if err := ValidateTunnelID(id); err != nil { - t.Errorf("MustGenerateDomainSafeID(12) generated invalid ID: %v, error: %v", id, err) - } - }) -} - -func TestDomainSafeIDUniqueness(t *testing.T) { - // generate multiple IDs and ensure they're unique - ids := make(map[string]bool) - numIDs := 1000 - - for i := 0; i < numIDs; i++ { - id := MustGenerateDomainSafeID() - if ids[id] { - t.Errorf("GenerateDomainSafeID() generated duplicate ID: %s", id) - } - ids[id] = true - } - - if len(ids) != numIDs { - t.Errorf("Expected %d unique IDs, got %d", numIDs, len(ids)) - } -} - -func TestBackwardCompatibilityNanoID(t *testing.T) { - // ensure the regular NanoID still works but now uses lowercase - id, err := GenerateNanoID() - if err != nil { - t.Errorf("GenerateNanoID() unexpected error = %v", err) - } - - if len(id) != 8 { - t.Errorf("GenerateNanoID() length = %v, want 8", len(id)) - } - - // check that it doesn't contain uppercase letters anymore - for _, char := range id { - if char >= 'A' && char <= 'Z' { - t.Errorf("GenerateNanoID() generated ID contains uppercase character: %c in %s", char, id) - } - } -} diff --git a/version/go.mod b/version/go.mod deleted file mode 100644 index 45556a4..0000000 --- a/version/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/karol-broda/funnel/version - -go 1.24.4 diff --git a/version/version.go b/version/version.go deleted file mode 100644 index 40e212d..0000000 --- a/version/version.go +++ /dev/null @@ -1,57 +0,0 @@ -package version - -import ( - "fmt" - "runtime" -) - -var ( - Version = "0.0.8" - BuildDate = "unknown" - GitCommit = "unknown" - GitTag = "unknown" -) - -type BuildInfo struct { - Version string - BuildDate string - GitCommit string - GitTag string - GoVersion string -} - -func GetBuildInfo() BuildInfo { - return BuildInfo{ - Version: Version, - BuildDate: BuildDate, - GitCommit: GitCommit, - GitTag: GitTag, - GoVersion: runtime.Version(), - } -} - -func GetVersion() string { - return Version -} - -func GetFullVersion() string { - info := GetBuildInfo() - if info.GitCommit != "unknown" && len(info.GitCommit) > 7 { - info.GitCommit = info.GitCommit[:7] - } - - if info.GitTag != "unknown" && info.GitTag != "" { - return fmt.Sprintf("%s (%s)", info.GitTag, info.GitCommit) - } - - return fmt.Sprintf("%s (%s)", info.Version, info.GitCommit) -} - -func PrintVersionInfo(appName string) { - info := GetBuildInfo() - fmt.Printf("%s version %s\n", appName, info.Version) - fmt.Printf("Build date: %s\n", info.BuildDate) - fmt.Printf("Git commit: %s\n", info.GitCommit) - fmt.Printf("Git tag: %s\n", info.GitTag) - fmt.Printf("Go version: %s\n", info.GoVersion) -} From fe17d96e20879c907075741c40a6c5527dc0b481 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sat, 2 May 2026 12:30:00 +0200 Subject: [PATCH 15/18] chore: track Cargo.lock for reproducible builds --- .gitignore | 1 - Cargo.lock | 4619 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 4619 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index f4b6ed6..240de45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # Rust /target **/*.rs.bk -Cargo.lock # Environment .env diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..363ce43 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4619 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "any_spawner" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41058deaa38c9d9dd933d6d238d825227cffa668e2839b52879f6619c63eee3b" +dependencies = [ + "futures", + "thiserror 2.0.18", + "wasm-bindgen-futures", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core 0.5.6", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite 0.29.0", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.4.0", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-test" +version = "16.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e3a443d2608936a02a222da7b746eb412fede7225b3030b64fe9be99eab8dc" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum 0.7.9", + "bytes", + "bytesize", + "cookie", + "http 1.4.0", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower", + "url", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytesize" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "codee" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.15.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" +dependencies = [ + "convert_case 0.6.0", + "pathdiff", + "serde_core", + "toml 1.1.2+spec-1.1.0", + "winnow 1.0.2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_str_slice_concat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "either_of" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5060e0a4cbf26a87550792688ade88e6b8aec9208613631a7a363bda7bc2d4cd" +dependencies = [ + "paste", + "pin-project-lite", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funnel-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.8.9", + "clap", + "dirs", + "flate2", + "funnel-core", + "futures", + "http 1.4.0", + "reqwest", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite 0.26.2", + "tokio-util", + "toml 0.8.23", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "funnel-core" +version = "0.1.0" +dependencies = [ + "base64", + "bytes", + "getrandom 0.3.4", + "hex", + "http 1.4.0", + "nanoid", + "pretty_assertions", + "regex", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "uuid", +] + +[[package]] +name = "funnel-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "axum 0.8.9", + "axum-test", + "chrono", + "clap", + "dashmap", + "funnel-core", + "futures", + "ipnetwork", + "jsonwebtoken", + "pretty_assertions", + "serde", + "serde_json", + "sqlx", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "funnel-web" +version = "0.1.0" +dependencies = [ + "funnel-core", + "leptos", + "serde", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 1.4.0", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "guardian" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "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", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hydration_context" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35485b3dcbf7e044b8f28c73f04f13e7b509c2466fd10cb2a8a447e38f8a93a" +dependencies = [ + "futures", + "once_cell", + "or_poisoned", + "pin-project-lite", + "serde", + "throw_error", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.0", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.0", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.7", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "leptos" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b8731cb00f3f0894058155410b95c8955b17273181d2bc72600ab84edd24f1" +dependencies = [ + "any_spawner", + "cfg-if", + "either_of", + "futures", + "hydration_context", + "leptos_config", + "leptos_dom", + "leptos_hot_reload", + "leptos_macro", + "leptos_server", + "oco_ref", + "or_poisoned", + "paste", + "reactive_graph", + "rustc-hash", + "send_wrapper", + "serde", + "serde_qs", + "server_fn", + "slotmap", + "tachys", + "thiserror 2.0.18", + "throw_error", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_config" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bae3e0ead5a7a814c8340eef7cb8b6cba364125bd8174b15dc9fe1b3cab7e03" +dependencies = [ + "config", + "regex", + "serde", + "thiserror 2.0.18", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89d4eb263bd5a9e7c49f780f17063f15aca56fd638c90b9dfd5f4739152e87d" +dependencies = [ + "js-sys", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80219388501d99b246f43b6e7d08a28f327cdd34ba630a35654d917f3e1788e" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn", + "walkdir", +] + +[[package]] +name = "leptos_macro" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e621f8f5342b9bdc93bb263b839cee7405027a74560425a2dabea9de7952b1fd" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.7.1", + "html-escape", + "itertools", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn", + "uuid", +] + +[[package]] +name = "leptos_server" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66985242812ec95e224fb48effe651ba02728beca92c461a9464c811a71aab11" +dependencies = [ + "any_spawner", + "base64", + "codee", + "futures", + "hydration_context", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "serde", + "serde_json", + "server_fn", + "tachys", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.6", +] + +[[package]] +name = "next_tuple" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "oco_ref" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" +dependencies = [ + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "reactive_graph" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a0ccddbc11a648bd09761801dac9e3f246ef7641130987d6120fced22515e6" +dependencies = [ + "any_spawner", + "async-lock", + "futures", + "guardian", + "hydration_context", + "or_poisoned", + "pin-project-lite", + "rustc-hash", + "send_wrapper", + "serde", + "slotmap", + "thiserror 2.0.18", + "web-sys", +] + +[[package]] +name = "reactive_stores" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aadc7c19e3a360bf19cd595d2dc8b58ce67b9240b95a103fbc1317a8ff194237" +dependencies = [ + "guardian", + "itertools", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores_macro", + "rustc-hash", +] + +[[package]] +name = "reactive_stores_macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221095cb028dc51fbc2833743ea8b1a585da1a2af19b440b3528027495bf1f2d" +dependencies = [ + "convert_case 0.7.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.7", +] + +[[package]] +name = "reserve-port" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstml" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" +dependencies = [ + "derive-where", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", + "syn_derive", + "thiserror 2.0.18", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 0.2.12", + "mime", + "mime_guess", + "rand 0.8.6", + "thiserror 1.0.69", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d05a9e3fd8d7404985418db38c6617cc793a1a27f398d4fbc9dfe8e41b804e6" +dependencies = [ + "bytes", + "const_format", + "dashmap", + "futures", + "gloo-net", + "http 1.4.0", + "js-sys", + "once_cell", + "pin-project-lite", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.18", + "throw_error", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504b35e883267b3206317b46d02952ed7b8bf0e11b2e209e2eb453b609a5e052" +dependencies = [ + "const_format", + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb8b274f568c94226a8045668554aace8142a59b8bca5414ac5a79627c825568" +dependencies = [ + "server_fn_macro", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "ipnetwork", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "ipnetwork", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tachys" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66c3b70c32844a6f1e2943c72a33ebb777ad6acbeb20d1329d62e3a7806d6ec" +dependencies = [ + "any_spawner", + "async-trait", + "const_str_slice_concat", + "drain_filter_polyfill", + "dyn-clone", + "either_of", + "futures", + "html-escape", + "indexmap", + "itertools", + "js-sys", + "linear-map", + "next_tuple", + "oco_ref", + "once_cell", + "or_poisoned", + "parking_lot", + "paste", + "reactive_graph", + "reactive_stores", + "rustc-hash", + "send_wrapper", + "slotmap", + "throw_error", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "throw_error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ef8bf264c6ae02a065a4a16553283f0656bd6266fc1fcb09fd2e6b5e91427b" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite 0.26.2", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.29.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.0", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.4", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.4", + "sha1", + "thiserror 2.0.18", +] + +[[package]] +name = "typed-builder" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[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 = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" From bec50fe8d5101d0ebdc563d6cb9db673c9e6156b Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sat, 2 May 2026 17:02:42 +0200 Subject: [PATCH 16/18] feat(server): add automatic TLS with ACME certificate management --- Cargo.lock | 1379 +++++++++++++++-- crates/funnel-client/src/main.rs | 1 + crates/funnel-client/src/tunnel.rs | 13 +- crates/funnel-server/Cargo.toml | 11 + crates/funnel-server/src/api/keys.rs | 8 +- crates/funnel-server/src/app.rs | 12 +- crates/funnel-server/src/main.rs | 137 +- crates/funnel-server/src/proxy/router.rs | 4 +- crates/funnel-server/src/tls/config.rs | 27 + crates/funnel-server/src/tls/manager.rs | 369 +++++ crates/funnel-server/src/tls/mod.rs | 44 + .../src/tls/provider/cloudflare.rs | 117 ++ crates/funnel-server/src/tls/provider/exec.rs | 45 + crates/funnel-server/src/tls/provider/mod.rs | 88 ++ .../funnel-server/src/tls/provider/route53.rs | 112 ++ flake.lock | 21 + 16 files changed, 2242 insertions(+), 146 deletions(-) create mode 100644 crates/funnel-server/src/tls/config.rs create mode 100644 crates/funnel-server/src/tls/manager.rs create mode 100644 crates/funnel-server/src/tls/provider/cloudflare.rs create mode 100644 crates/funnel-server/src/tls/provider/exec.rs create mode 100644 crates/funnel-server/src/tls/provider/mod.rs create mode 100644 crates/funnel-server/src/tls/provider/route53.rs diff --git a/Cargo.lock b/Cargo.lock index 363ce43..285271d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aho-corasick" version = "1.1.4" @@ -99,6 +93,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "argon2" version = "0.5.3" @@ -107,10 +110,49 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -200,6 +242,408 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f156acdd2cf55f5aa53ee416c4ac851cf1222694506c0b1f78c85695e9ca9d" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.0", + "sha1", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcd93c82209ac7413532388067dce79be5a8780c1786e5fae3df22e4dee2864" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 1.4.0", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-route53" +version = "1.111.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f556e78b01bc3b6fe405e2c6c4e538778b9da1cfea59c9f1123a84175305455e" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.98.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69c77aafa20460c68b6b3213c84f6423b6e76dbf89accd3e1789a686ffd9489" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7e7b09346d5ca22a2a08267555843a6a0127fb20d8964cb6ecfb8fdb190225" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2249b81a2e73a8027c41c378463a81ec39b8510f184f2caab87de912af0f49b" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68dc0b907359b120170613b5c09ccc61304eac3998ff6274b97d93ee6490115a" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "percent-encoding", + "sha2 0.11.0", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.9.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.9", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.7.9" @@ -211,9 +655,9 @@ dependencies = [ "bytes", "futures-util", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.9.0", "hyper-util", "itoa", "matchit 0.7.3", @@ -246,9 +690,9 @@ dependencies = [ "form_urlencoded", "futures-util", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.9.0", "hyper-util", "itoa", "matchit 0.8.4", @@ -280,7 +724,7 @@ dependencies = [ "bytes", "futures-util", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -300,7 +744,7 @@ dependencies = [ "bytes", "futures-core", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -310,6 +754,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.9.0", + "hyper-util", + "pin-project-lite", + "rustls 0.23.40", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + [[package]] name = "axum-test" version = "16.4.1" @@ -325,7 +791,7 @@ dependencies = [ "cookie", "http 1.4.0", "http-body-util", - "hyper", + "hyper 1.9.0", "hyper-util", "mime", "pretty_assertions", @@ -346,6 +812,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -367,7 +843,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -379,6 +855,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -395,7 +880,17 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] [[package]] name = "bytesize" @@ -416,6 +911,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -485,6 +982,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "codee" version = "0.3.5" @@ -508,6 +1020,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -536,6 +1058,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const_format" version = "0.2.36" @@ -591,6 +1119,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -606,6 +1144,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -621,15 +1168,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -655,6 +1193,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -681,11 +1237,25 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -718,12 +1288,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "dirs" version = "6.0.0" @@ -768,6 +1350,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -842,20 +1430,16 @@ dependencies = [ ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "fastrand" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] -name = "flate2" -version = "1.1.9" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flume" @@ -880,6 +1464,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -889,19 +1479,33 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funnel-client" version = "0.1.0" dependencies = [ "anyhow", - "axum 0.8.9", "clap", "dirs", - "flate2", "funnel-core", "futures", "http 1.4.0", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "tokio", @@ -928,7 +1532,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", "uuid", ] @@ -939,16 +1543,24 @@ version = "0.1.0" dependencies = [ "anyhow", "argon2", + "aws-config", + "aws-sdk-route53", "axum 0.8.9", + "axum-server", "axum-test", "chrono", "clap", "dashmap", "funnel-core", "futures", + "instant-acme", "ipnetwork", "jsonwebtoken", + "lru", "pretty_assertions", + "reqwest 0.13.3", + "rustls 0.23.40", + "rustls-pemfile", "serde", "serde_json", "sqlx", @@ -960,6 +1572,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "x509-parser", ] [[package]] @@ -1160,6 +1773,44 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1174,7 +1825,18 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -1210,7 +1872,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -1219,7 +1881,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", ] [[package]] @@ -1261,6 +1932,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1280,7 +1962,7 @@ dependencies = [ "bytes", "futures-core", "http 1.4.0", - "http-body", + "http-body 1.0.1", "pin-project-lite", ] @@ -1296,6 +1978,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] + [[package]] name = "hydration_context" version = "0.2.1" @@ -1310,6 +2001,30 @@ dependencies = [ "throw_error", ] +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.9.0" @@ -1320,8 +2035,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2 0.4.13", "http 1.4.0", - "http-body", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1331,6 +2047,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.9" @@ -1338,11 +2069,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.0", - "hyper", + "hyper 1.9.0", "hyper-util", - "rustls", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-platform-verifier", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", "webpki-roots 1.0.7", ] @@ -1358,13 +2091,13 @@ dependencies = [ "futures-channel", "futures-util", "http 1.4.0", - "http-body", - "hyper", + "http-body 1.0.1", + "hyper 1.9.0", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1515,6 +2248,32 @@ dependencies = [ "serde_core", ] +[[package]] +name = "instant-acme" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f05ad37c421b962354c358d347d4a6130151df9407978372d3ad7f0c8f71a64" +dependencies = [ + "async-trait", + "aws-lc-rs", + "base64", + "bytes", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "httpdate", + "hyper 1.9.0", + "hyper-rustls 0.27.9", + "hyper-util", + "rcgen", + "rustls 0.23.40", + "rustls-pki-types", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "interpolator" version = "0.5.0" @@ -1537,35 +2296,94 @@ dependencies = [ ] [[package]] -name = "iri-string" -version = "0.7.12" +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "memchr", - "serde", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] [[package]] -name = "itertools" -version = "0.14.0" +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ - "either", + "quote", + "syn", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] [[package]] name = "js-sys" @@ -1808,6 +2626,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1865,7 +2692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -1891,14 +2718,10 @@ dependencies = [ ] [[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" @@ -1926,6 +2749,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2007,6 +2840,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2019,6 +2861,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2031,6 +2879,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking" version = "2.2.1" @@ -2134,6 +2988,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs1" version = "0.7.5" @@ -2278,8 +3138,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls", - "socket2", + "rustls 0.23.40", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2292,13 +3152,14 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", "rand 0.9.4", "ring", "rustc-hash", - "rustls", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -2316,7 +3177,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] @@ -2423,6 +3284,20 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "aws-lc-rs", + "pem", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "reactive_graph" version = "0.1.8" @@ -2524,6 +3399,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.10" @@ -2540,24 +3421,24 @@ dependencies = [ "bytes", "futures-core", "http 1.4.0", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.40", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower", "tower-http", "tower-service", @@ -2568,6 +3449,44 @@ dependencies = [ "webpki-roots 1.0.7", ] +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.9.0", + "hyper-rustls 0.27.9", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.40", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reserve-port" version = "2.4.0" @@ -2587,7 +3506,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -2597,8 +3516,8 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -2648,20 +3567,73 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -2672,15 +3644,53 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.13", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -2704,12 +3714,54 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -2881,8 +3933,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -2892,8 +3944,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -2927,15 +3990,25 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] -name = "simd-adler32" -version = "0.3.9" +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "simple_asn1" @@ -2973,6 +4046,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.3" @@ -3040,10 +4123,10 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls", + "rustls 0.23.40", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "tokio", @@ -3082,7 +4165,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -3105,7 +4188,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -3115,7 +4198,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", "md-5", @@ -3126,7 +4209,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -3155,7 +4238,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "ipnetwork", "itoa", @@ -3166,7 +4249,7 @@ dependencies = [ "rand 0.8.6", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -3434,7 +4517,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -3450,13 +4533,23 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.40", "tokio", ] @@ -3479,10 +4572,10 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls", + "rustls 0.23.40", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tungstenite 0.26.2", "webpki-roots 0.26.11", ] @@ -3610,7 +4703,7 @@ dependencies = [ "bytes", "futures-util", "http 1.4.0", - "http-body", + "http-body 1.0.1", "iri-string", "pin-project-lite", "tower", @@ -3724,7 +4817,7 @@ dependencies = [ "httparse", "log", "rand 0.9.4", - "rustls", + "rustls 0.23.40", "rustls-pki-types", "sha1", "thiserror 2.0.18", @@ -3818,6 +4911,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -3836,6 +4935,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -3890,6 +4995,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -4061,6 +5172,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -4497,6 +5617,30 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -4509,6 +5653,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/crates/funnel-client/src/main.rs b/crates/funnel-client/src/main.rs index 4602c08..bb2953f 100644 --- a/crates/funnel-client/src/main.rs +++ b/crates/funnel-client/src/main.rs @@ -105,6 +105,7 @@ async fn run_http( let server_url = server_flag .or_else(|| inlet.map(|i| i.server.clone())) .filter(|s| !s.is_empty()) + .map(|s| if s.contains("://") { s } else { format!("http://{s}") }) .ok_or_else(|| { anyhow::anyhow!( "no server configured. use --server or run: funnel config set-server " diff --git a/crates/funnel-client/src/tunnel.rs b/crates/funnel-client/src/tunnel.rs index 2e24ceb..b754a6e 100644 --- a/crates/funnel-client/src/tunnel.rs +++ b/crates/funnel-client/src/tunnel.rs @@ -95,18 +95,17 @@ impl TunnelClient { > { let mut url = Url::parse(&self.server_url)?; - match url.scheme() { - "https" => url.set_scheme("wss").unwrap(), - _ => url.set_scheme("ws").unwrap(), - }; + let ws_scheme = if url.scheme() == "https" { "wss" } else { "ws" }; + url.set_scheme(ws_scheme) + .map_err(|_| anyhow::anyhow!("failed to set websocket scheme on url: {url}"))?; url.set_path("/ws"); url.query_pairs_mut() .append_pair("id", self.tunnel_id.as_ref()); - let mut request = tokio_tungstenite::tungstenite::http::Request::builder() - .uri(url.as_str()) - .body(())?; + use tokio_tungstenite::tungstenite::client::IntoClientRequest; + + let mut request = url.as_str().into_client_request()?; if let Some(token) = &self.token { request.headers_mut().insert( diff --git a/crates/funnel-server/Cargo.toml b/crates/funnel-server/Cargo.toml index 066fdb3..f274d84 100644 --- a/crates/funnel-server/Cargo.toml +++ b/crates/funnel-server/Cargo.toml @@ -27,6 +27,17 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } uuid = { version = "1", features = ["v7", "serde"] } +# tls +aws-config = "1" +aws-sdk-route53 = "1" +axum-server = { version = "0.8", features = ["tls-rustls"] } +instant-acme = { version = "0.8", features = ["rcgen"] } +lru = "0.16" +reqwest = { version = "0.13", default-features = false, features = ["rustls", "json", "query"] } +rustls = "0.23" +rustls-pemfile = "2" +x509-parser = "0.18" + [dev-dependencies] axum-test = "16" pretty_assertions = "1" diff --git a/crates/funnel-server/src/api/keys.rs b/crates/funnel-server/src/api/keys.rs index c612af4..a0f43dd 100644 --- a/crates/funnel-server/src/api/keys.rs +++ b/crates/funnel-server/src/api/keys.rs @@ -26,7 +26,7 @@ pub struct CreateKeyResponse { pub async fn list(State(state): State>) -> Result>, AppError> { // TODO: get user_id from auth middleware // For now, return empty - let _pool = &state.db; + let _pool = state.require_db()?; Ok(Json(vec![])) } @@ -35,7 +35,8 @@ pub async fn create( State(state): State>, Json(req): Json, ) -> Result, AppError> { - let (plaintext, key) = api_keys::create(&state.db, req.user_id, &req.name).await?; + let db = state.require_db()?; + let (plaintext, key) = api_keys::create(db, req.user_id, &req.name).await?; Ok(Json(CreateKeyResponse { key: plaintext, @@ -51,7 +52,8 @@ pub async fn revoke( // TODO: get user_id from auth middleware let user_id = Uuid::nil(); // placeholder - let revoked = api_keys::revoke(&state.db, id, user_id).await?; + let db = state.require_db()?; + let revoked = api_keys::revoke(db, id, user_id).await?; if !revoked { return Err(AppError::NotFound("api key not found".into())); } diff --git a/crates/funnel-server/src/app.rs b/crates/funnel-server/src/app.rs index 95bfcaa..dc70298 100644 --- a/crates/funnel-server/src/app.rs +++ b/crates/funnel-server/src/app.rs @@ -13,19 +13,27 @@ use crate::ws; use crate::proxy; pub struct AppState { - pub db: PgPool, + pub db: Option, pub tunnels: TunnelManager, pub start_time: Instant, + pub is_tls: bool, } impl AppState { - pub fn new(db: PgPool) -> Self { + pub fn new(db: Option, is_tls: bool) -> Self { Self { db, tunnels: TunnelManager::new(), start_time: Instant::now(), + is_tls, } } + + pub fn require_db(&self) -> Result<&PgPool, crate::error::AppError> { + self.db + .as_ref() + .ok_or_else(|| crate::error::AppError::BadRequest("database not configured".into())) + } } pub fn build_router(state: Arc) -> Router { diff --git a/crates/funnel-server/src/main.rs b/crates/funnel-server/src/main.rs index c60dd8e..642162f 100644 --- a/crates/funnel-server/src/main.rs +++ b/crates/funnel-server/src/main.rs @@ -8,8 +8,13 @@ mod tls; mod tunnel; mod ws; +use std::net::SocketAddr; +use std::path::Path; use std::sync::Arc; +use axum::extract::Request; +use axum::response::Redirect; +use axum::Router; use clap::Parser; use sqlx::postgres::PgPoolOptions; use tracing_subscriber::EnvFilter; @@ -27,11 +32,35 @@ struct Cli { /// PostgreSQL connection URL #[arg(long, env = "DATABASE_URL")] - database_url: String, + database_url: Option, /// Maximum database connections #[arg(long, default_value_t = 10)] db_max_connections: u32, + + /// Enable TLS/HTTPS + #[arg(long)] + enable_tls: bool, + + /// TLS port + #[arg(long, default_value_t = 8443)] + tls_port: u16, + + /// Certificate storage directory + #[arg(long, default_value = "./certs")] + cert_dir: String, + + /// Let's Encrypt email for ACME registration + #[arg(long, env = "LETSENCRYPT_EMAIL")] + letsencrypt_email: Option, + + /// Path to DNS providers config JSON + #[arg(long, env = "DNS_PROVIDERS_CONFIG")] + dns_providers_config: Option, + + /// Use Let's Encrypt staging environment + #[arg(long)] + acme_staging: bool, } #[tokio::main] @@ -47,32 +76,102 @@ async fn main() -> anyhow::Result<()> { tracing::info!(host = %cli.host, port = cli.port, "starting funnel server"); - let pool = PgPoolOptions::new() - .max_connections(cli.db_max_connections) - .connect(&cli.database_url) - .await?; + let pool = if let Some(ref database_url) = cli.database_url { + let pool = PgPoolOptions::new() + .max_connections(cli.db_max_connections) + .connect(database_url) + .await?; + + tracing::info!("connected to database"); + + sqlx::migrate!("../../migrations") + .run(&pool) + .await?; + + tracing::info!("database migrations applied"); + Some(pool) + } else { + tracing::info!("no database configured, running in memory-only mode"); + None + }; + + if cli.enable_tls { + run_with_tls(cli, pool).await + } else { + run_plain(cli, pool).await + } +} + +async fn run_plain(cli: Cli, pool: Option) -> anyhow::Result<()> { + let state = Arc::new(app::AppState::new(pool, false)); + let router = app::build_router(state); + + let addr: SocketAddr = format!("{}:{}", cli.host, cli.port).parse()?; + let listener = tokio::net::TcpListener::bind(addr).await?; - tracing::info!("connected to database"); + tracing::info!(addr = %addr, "server listening"); - sqlx::migrate!("../../migrations") - .run(&pool) - .await?; + axum::serve(listener, router.into_make_service_with_connect_info::()).await?; - tracing::info!("database migrations applied"); + Ok(()) +} - let state = Arc::new(app::AppState::new(pool)); +async fn run_with_tls(cli: Cli, pool: Option) -> anyhow::Result<()> { + let email = cli + .letsencrypt_email + .as_deref() + .ok_or_else(|| anyhow::anyhow!("--letsencrypt-email required when TLS is enabled"))?; + let config_path = cli + .dns_providers_config + .as_deref() + .ok_or_else(|| anyhow::anyhow!("--dns-providers-config required when TLS is enabled"))?; + + let rustls_config = + tls::setup(Path::new(&cli.cert_dir), Path::new(config_path), email, cli.acme_staging) + .await?; + + let state = Arc::new(app::AppState::new(pool, true)); let router = app::build_router(state); - let addr = format!("{}:{}", cli.host, cli.port); - let listener = tokio::net::TcpListener::bind(&addr).await?; + let tls_addr: SocketAddr = format!("{}:{}", cli.host, cli.tls_port).parse()?; + let http_addr: SocketAddr = format!("{}:{}", cli.host, cli.port).parse()?; - tracing::info!(addr = %addr, "server listening"); + tracing::info!(tls_addr = %tls_addr, http_addr = %http_addr, "server listening with TLS"); - axum::serve( - listener, - router.into_make_service_with_connect_info::(), - ) - .await?; + let https_server = axum_server::bind_rustls(tls_addr, rustls_config) + .serve(router.into_make_service_with_connect_info::()); + + let redirect_router = https_redirect_router(cli.tls_port); + let http_listener = tokio::net::TcpListener::bind(http_addr).await?; + let http_server = axum::serve(http_listener, redirect_router.into_make_service()); + + tokio::select! { + res = https_server => { res?; } + res = http_server => { res?; } + }; Ok(()) } + +fn https_redirect_router(tls_port: u16) -> Router { + Router::new().fallback(move |request: Request| async move { + let host = request + .headers() + .get("host") + .and_then(|v| v.to_str().ok()) + .unwrap_or("localhost"); + let host_without_port = host.split(':').next().unwrap_or(host); + let path = request + .uri() + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + + let location = if tls_port == 443 { + format!("https://{host_without_port}{path}") + } else { + format!("https://{host_without_port}:{tls_port}{path}") + }; + Redirect::permanent(&location) + }) +} diff --git a/crates/funnel-server/src/proxy/router.rs b/crates/funnel-server/src/proxy/router.rs index e970358..3fce6fc 100644 --- a/crates/funnel-server/src/proxy/router.rs +++ b/crates/funnel-server/src/proxy/router.rs @@ -48,12 +48,12 @@ pub async fn handle_tunnel_request( .map(|ci| ci.0); let headers = match remote_addr { - Some(addr) => prepare_forwarding_headers(request.headers(), host, addr, false), + Some(addr) => prepare_forwarding_headers(request.headers(), host, addr, state.is_tls), None => prepare_forwarding_headers( request.headers(), host, std::net::SocketAddr::from(([0, 0, 0, 0], 0)), - false, + state.is_tls, ), }; diff --git a/crates/funnel-server/src/tls/config.rs b/crates/funnel-server/src/tls/config.rs new file mode 100644 index 0000000..0e0ac6d --- /dev/null +++ b/crates/funnel-server/src/tls/config.rs @@ -0,0 +1,27 @@ +use std::collections::HashMap; +use std::path::Path; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct ProviderConfig { + pub providers: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct DnsProviderEntry { + #[allow(dead_code)] + pub name: String, + #[serde(rename = "type")] + pub provider_type: String, + pub domains: Vec, + pub config: HashMap, +} + +impl ProviderConfig { + pub async fn load(path: &Path) -> anyhow::Result { + let contents = tokio::fs::read_to_string(path).await?; + let config: ProviderConfig = serde_json::from_str(&contents)?; + Ok(config) + } +} diff --git a/crates/funnel-server/src/tls/manager.rs b/crates/funnel-server/src/tls/manager.rs new file mode 100644 index 0000000..89be9c7 --- /dev/null +++ b/crates/funnel-server/src/tls/manager.rs @@ -0,0 +1,369 @@ +use std::io::BufReader; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; + +use anyhow::{Context, Result}; +use instant_acme::{ + Account, AccountCredentials, AuthorizationStatus, ChallengeType, Identifier, NewAccount, + NewOrder, RetryPolicy, +}; +use lru::LruCache; +use rustls::server::ResolvesServerCert; +use rustls::sign::CertifiedKey; +use x509_parser::prelude::*; + +use super::provider::ProviderMux; + +const LRU_CACHE_SIZE: usize = 512; +const RENEWAL_CHECK_INTERVAL: Duration = Duration::from_secs(3600); +const RENEWAL_WINDOW: Duration = Duration::from_secs(30 * 24 * 3600); +const ACME_POLL_TIMEOUT: Duration = Duration::from_secs(300); +const DNS_PROPAGATION_DELAY: Duration = Duration::from_secs(10); + +struct CachedCert { + certified_key: Arc, + not_after: SystemTime, +} + +pub struct CertificateManager { + cache: Mutex>, + obtain_lock: tokio::sync::Mutex<()>, + cert_dir: PathBuf, + provider_mux: Arc, + account: Account, +} + +impl std::fmt::Debug for CertificateManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CertificateManager") + .field("cert_dir", &self.cert_dir) + .finish_non_exhaustive() + } +} + +impl CertificateManager { + pub async fn new( + cert_dir: PathBuf, + provider_mux: ProviderMux, + email: &str, + staging: bool, + ) -> Result { + tokio::fs::create_dir_all(&cert_dir) + .await + .context("failed to create certificate directory")?; + + let account = load_or_create_account(&cert_dir, email, staging).await?; + let cache = Mutex::new(LruCache::new( + std::num::NonZeroUsize::new(LRU_CACHE_SIZE).unwrap(), + )); + + Ok(Self { + cache, + obtain_lock: tokio::sync::Mutex::new(()), + cert_dir, + provider_mux: Arc::new(provider_mux), + account, + }) + } + + pub async fn preload_certificates(&self) -> Result<()> { + for domain in self.provider_mux.domains() { + match self.load_from_disk(&domain).await { + Ok(Some((certified_key, not_after))) => { + tracing::info!(domain = %domain, "loaded certificate from disk"); + let mut cache = self.cache.lock().unwrap(); + cache.put( + domain, + CachedCert { + certified_key: Arc::new(certified_key), + not_after, + }, + ); + } + Ok(None) => { + tracing::info!(domain = %domain, "no cached certificate, obtaining from ACME"); + if let Err(e) = self.obtain_certificate(&domain).await { + tracing::error!(domain = %domain, error = %e, "failed to obtain certificate"); + } + } + Err(e) => { + tracing::warn!(domain = %domain, error = %e, "failed to load cert from disk"); + if let Err(e) = self.obtain_certificate(&domain).await { + tracing::error!(domain = %domain, error = %e, "failed to obtain certificate"); + } + } + } + } + Ok(()) + } + + pub async fn obtain_certificate(&self, domain: &str) -> Result<()> { + let _guard = self.obtain_lock.lock().await; + + // double check cache after acquiring lock + { + let cache = self.cache.lock().unwrap(); + if let Some(cached) = cache.peek(domain) { + if cached.not_after > SystemTime::now() + RENEWAL_WINDOW { + return Ok(()); + } + } + } + + let provider = self + .provider_mux + .find(domain) + .context("no DNS provider configured for domain")?; + + tracing::info!(domain = %domain, "ordering certificate from ACME"); + + let identifiers = vec![Identifier::Dns(domain.to_string())]; + let mut order = self + .account + .new_order(&NewOrder::new(&identifiers)) + .await + .context("failed to create ACME order")?; + + // process authorizations: present DNS challenges and mark them ready + struct PendingCleanup { + record_name: String, + dns_value: String, + } + let mut cleanups = Vec::new(); + + { + let mut authz_stream = order.authorizations(); + while let Some(result) = authz_stream.next().await { + let mut authz = result.context("failed to get authorization")?; + + if matches!(authz.status, AuthorizationStatus::Valid) { + continue; + } + + let challenge_domain = match authz.identifier().identifier { + Identifier::Dns(d) => d.clone(), + _ => continue, + }; + + let mut challenge = authz + .challenge(ChallengeType::Dns01) + .context("no DNS-01 challenge found")?; + + let dns_value = challenge.key_authorization().dns_value(); + + let record_name = format!("_acme-challenge.{challenge_domain}"); + provider + .present(&record_name, &dns_value) + .await + .context("failed to present DNS challenge")?; + + tokio::time::sleep(DNS_PROPAGATION_DELAY).await; + + challenge + .set_ready() + .await + .context("failed to set challenge ready")?; + + cleanups.push(PendingCleanup { + record_name, + dns_value, + }); + } + } + + let retries = RetryPolicy::new().timeout(ACME_POLL_TIMEOUT); + + order + .poll_ready(&retries) + .await + .context("failed waiting for order to become ready")?; + + // finalize: generates ECDSA key + CSR internally, returns private key PEM + let private_key_pem = order + .finalize() + .await + .context("failed to finalize ACME order")?; + + let cert_pem = order + .poll_certificate(&retries) + .await + .context("failed to get certificate")?; + + // save to disk + let cert_path = self.cert_dir.join(format!("{domain}.crt")); + let key_path = self.cert_dir.join(format!("{domain}.key")); + tokio::fs::write(&cert_path, &cert_pem).await?; + tokio::fs::write(&key_path, &private_key_pem).await?; + + // parse and cache + let certified_key = load_certified_key(&cert_pem, &private_key_pem)?; + let not_after = parse_cert_expiry(&cert_pem)?; + + { + let mut cache = self.cache.lock().unwrap(); + cache.put( + domain.to_string(), + CachedCert { + certified_key: Arc::new(certified_key), + not_after, + }, + ); + } + + tracing::info!(domain = %domain, "certificate obtained and cached"); + + // clean up DNS records (best effort, after everything succeeded) + for cleanup in &cleanups { + if let Err(e) = provider.cleanup(&cleanup.record_name, &cleanup.dns_value).await { + tracing::warn!( + record = %cleanup.record_name, + error = %e, + "failed to clean up DNS record" + ); + } + } + + Ok(()) + } + + async fn load_from_disk(&self, domain: &str) -> Result> { + let cert_path = self.cert_dir.join(format!("{domain}.crt")); + let key_path = self.cert_dir.join(format!("{domain}.key")); + + if !cert_path.exists() || !key_path.exists() { + return Ok(None); + } + + let cert_pem = tokio::fs::read_to_string(&cert_path).await?; + let key_pem = tokio::fs::read_to_string(&key_path).await?; + + let certified_key = load_certified_key(&cert_pem, &key_pem)?; + let not_after = parse_cert_expiry(&cert_pem)?; + + Ok(Some((certified_key, not_after))) + } + + pub fn spawn_renewal_task(self: &Arc) { + let manager = Arc::clone(self); + tokio::spawn(async move { + let mut interval = tokio::time::interval(RENEWAL_CHECK_INTERVAL); + loop { + interval.tick().await; + manager.check_renewals().await; + } + }); + } + + async fn check_renewals(&self) { + let domains_to_renew: Vec = { + let cache = self.cache.lock().unwrap(); + cache + .iter() + .filter(|(_, cert)| { + cert.not_after + .duration_since(SystemTime::now()) + .unwrap_or(Duration::ZERO) + < RENEWAL_WINDOW + }) + .map(|(domain, _)| domain.clone()) + .collect() + }; + + for domain in domains_to_renew { + tracing::info!(domain = %domain, "renewing certificate"); + if let Err(e) = self.obtain_certificate(&domain).await { + tracing::error!(domain = %domain, error = %e, "failed to renew certificate"); + } + } + } +} + +impl ResolvesServerCert for CertificateManager { + fn resolve( + &self, + client_hello: rustls::server::ClientHello<'_>, + ) -> Option> { + let sni = client_hello.server_name()?; + let cache = self.cache.lock().unwrap(); + let cached = cache.peek(sni)?; + + if cached.not_after > SystemTime::now() { + Some(Arc::clone(&cached.certified_key)) + } else { + None + } + } +} + +fn load_certified_key(cert_pem: &str, key_pem: &str) -> Result { + let certs = rustls_pemfile::certs(&mut BufReader::new(cert_pem.as_bytes())) + .collect::, _>>() + .context("failed to parse certificate PEM")?; + + let key = rustls_pemfile::private_key(&mut BufReader::new(key_pem.as_bytes())) + .context("failed to parse private key PEM")? + .context("no private key found")?; + + let signing_key = rustls::crypto::ring::sign::any_supported_type(&key) + .map_err(|e| anyhow::anyhow!("unsupported private key type: {e}"))?; + + Ok(CertifiedKey::new(certs, signing_key)) +} + +fn parse_cert_expiry(cert_pem: &str) -> Result { + let certs: Vec<_> = rustls_pemfile::certs(&mut BufReader::new(cert_pem.as_bytes())) + .collect::, _>>()?; + let cert = certs.first().context("no certificate found")?; + let (_, parsed) = X509Certificate::from_der(cert.as_ref()) + .map_err(|e| anyhow::anyhow!("failed to parse x509 certificate: {e}"))?; + + let not_after = parsed.validity().not_after; + Ok(not_after.to_datetime().into()) +} + +async fn load_or_create_account( + cert_dir: &Path, + email: &str, + staging: bool, +) -> Result { + let account_json_path = cert_dir.join("account.json"); + + if account_json_path.exists() { + let json = tokio::fs::read_to_string(&account_json_path).await?; + let credentials: AccountCredentials = serde_json::from_str(&json)?; + let account = Account::builder() + .context("failed to create account builder")? + .from_credentials(credentials) + .await + .context("failed to load ACME account from credentials")?; + tracing::info!("loaded existing ACME account"); + return Ok(account); + } + + let directory_url = if staging { + instant_acme::LetsEncrypt::Staging.url() + } else { + instant_acme::LetsEncrypt::Production.url() + }; + + let (account, credentials) = Account::builder() + .context("failed to create account builder")? + .create( + &NewAccount { + contact: &[&format!("mailto:{email}")], + terms_of_service_agreed: true, + only_return_existing: false, + }, + directory_url.to_string(), + None, + ) + .await + .context("failed to create ACME account")?; + + let json = serde_json::to_string_pretty(&credentials)?; + tokio::fs::write(&account_json_path, &json).await?; + + tracing::info!("created new ACME account"); + Ok(account) +} diff --git a/crates/funnel-server/src/tls/mod.rs b/crates/funnel-server/src/tls/mod.rs index e69de29..0aad9a8 100644 --- a/crates/funnel-server/src/tls/mod.rs +++ b/crates/funnel-server/src/tls/mod.rs @@ -0,0 +1,44 @@ +pub mod config; +pub mod manager; +pub mod provider; + +use std::path::Path; +use std::sync::Arc; + +use anyhow::{Context, Result}; + +pub async fn setup( + cert_dir: &Path, + providers_config_path: &Path, + email: &str, + acme_staging: bool, +) -> Result { + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install default crypto provider"); + + let provider_config = config::ProviderConfig::load(providers_config_path).await?; + let provider_mux = provider::ProviderMux::from_config(&provider_config).await?; + + let cert_manager = Arc::new( + manager::CertificateManager::new( + cert_dir.to_path_buf(), + provider_mux, + email, + acme_staging, + ) + .await + .context("failed to create certificate manager")?, + ); + + cert_manager.preload_certificates().await?; + cert_manager.spawn_renewal_task(); + + let server_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_cert_resolver(cert_manager as Arc); + + Ok(axum_server::tls_rustls::RustlsConfig::from_config( + Arc::new(server_config), + )) +} diff --git a/crates/funnel-server/src/tls/provider/cloudflare.rs b/crates/funnel-server/src/tls/provider/cloudflare.rs new file mode 100644 index 0000000..36812ed --- /dev/null +++ b/crates/funnel-server/src/tls/provider/cloudflare.rs @@ -0,0 +1,117 @@ +use anyhow::Result; + +use super::{BoxFuture, DnsChallenger}; + +const API_BASE: &str = "https://api.cloudflare.com/client/v4"; +const TXT_TTL: u32 = 120; + +pub struct CloudflareProvider { + client: reqwest::Client, + api_token: String, +} + +impl CloudflareProvider { + pub fn new(api_token: String) -> Self { + Self { + client: reqwest::Client::new(), + api_token, + } + } + + async fn find_zone_id(&self, domain: &str) -> Result { + let parts: Vec<&str> = domain.split('.').collect(); + for i in 0..parts.len().saturating_sub(1) { + let zone_name = parts[i..].join("."); + let resp: serde_json::Value = self + .client + .get(format!("{API_BASE}/zones")) + .bearer_auth(&self.api_token) + .query(&[("name", &zone_name)]) + .send() + .await? + .json() + .await?; + + let zone_id = resp["result"] + .as_array() + .and_then(|zones| zones.first()) + .and_then(|zone| zone["id"].as_str()); + + if let Some(id) = zone_id { + return Ok(id.to_string()); + } + } + anyhow::bail!("no cloudflare zone found for domain: {domain}") + } + + async fn create_txt_record(&self, zone_id: &str, name: &str, value: &str) -> Result<()> { + let resp = self + .client + .post(format!("{API_BASE}/zones/{zone_id}/dns_records")) + .bearer_auth(&self.api_token) + .json(&serde_json::json!({ + "type": "TXT", + "name": name, + "content": value, + "ttl": TXT_TTL + })) + .send() + .await?; + + if !resp.status().is_success() { + let body = resp.text().await?; + anyhow::bail!("failed to create TXT record: {body}"); + } + Ok(()) + } + + async fn delete_txt_records(&self, zone_id: &str, name: &str, value: &str) -> Result<()> { + let resp: serde_json::Value = self + .client + .get(format!("{API_BASE}/zones/{zone_id}/dns_records")) + .bearer_auth(&self.api_token) + .query(&[("type", "TXT"), ("name", name)]) + .send() + .await? + .json() + .await?; + + let matching_records = resp["result"] + .as_array() + .into_iter() + .flatten() + .filter(|r| r["content"].as_str() == Some(value)) + .filter_map(|r| r["id"].as_str()); + + for record_id in matching_records { + self.client + .delete(format!("{API_BASE}/zones/{zone_id}/dns_records/{record_id}")) + .bearer_auth(&self.api_token) + .send() + .await?; + } + Ok(()) + } +} + +impl DnsChallenger for CloudflareProvider { + fn present(&self, record_name: &str, value: &str) -> BoxFuture<'_, Result<()>> { + let record_name = record_name.to_string(); + let value = value.to_string(); + Box::pin(async move { + let zone_id = self.find_zone_id(&record_name).await?; + self.create_txt_record(&zone_id, &record_name, &value) + .await + }) + } + + fn cleanup(&self, record_name: &str, value: &str) -> BoxFuture<'_, Result<()>> { + let record_name = record_name.to_string(); + let value = value.to_string(); + Box::pin(async move { + let zone_id = self.find_zone_id(&record_name).await?; + self.delete_txt_records(&zone_id, &record_name, &value) + .await + }) + } +} diff --git a/crates/funnel-server/src/tls/provider/exec.rs b/crates/funnel-server/src/tls/provider/exec.rs new file mode 100644 index 0000000..89c0854 --- /dev/null +++ b/crates/funnel-server/src/tls/provider/exec.rs @@ -0,0 +1,45 @@ +use anyhow::{Context, Result}; + +use super::{BoxFuture, DnsChallenger}; + +pub struct ExecProvider { + command: String, +} + +impl ExecProvider { + pub fn new(command: String) -> Self { + Self { command } + } + + async fn run(&self, action: &str, record_name: &str, value: &str) -> Result<()> { + let output = tokio::process::Command::new(&self.command) + .args([action, record_name, value]) + .output() + .await + .with_context(|| format!("failed to execute command: {}", self.command))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!( + "command exited with {}: {}", + output.status, + stderr.trim() + ); + } + Ok(()) + } +} + +impl DnsChallenger for ExecProvider { + fn present(&self, record_name: &str, value: &str) -> BoxFuture<'_, Result<()>> { + let record_name = record_name.to_string(); + let value = value.to_string(); + Box::pin(async move { self.run("present", &record_name, &value).await }) + } + + fn cleanup(&self, record_name: &str, value: &str) -> BoxFuture<'_, Result<()>> { + let record_name = record_name.to_string(); + let value = value.to_string(); + Box::pin(async move { self.run("cleanup", &record_name, &value).await }) + } +} diff --git a/crates/funnel-server/src/tls/provider/mod.rs b/crates/funnel-server/src/tls/provider/mod.rs new file mode 100644 index 0000000..6c72893 --- /dev/null +++ b/crates/funnel-server/src/tls/provider/mod.rs @@ -0,0 +1,88 @@ +mod cloudflare; +mod exec; +mod route53; + +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use anyhow::{Context, Result}; + +use self::cloudflare::CloudflareProvider; +use self::exec::ExecProvider; +use self::route53::Route53Provider; +use super::config::ProviderConfig; + +pub type BoxFuture<'a, T> = Pin + Send + 'a>>; + +pub trait DnsChallenger: Send + Sync { + fn present(&self, record_name: &str, value: &str) -> BoxFuture<'_, Result<()>>; + fn cleanup(&self, record_name: &str, value: &str) -> BoxFuture<'_, Result<()>>; +} + +/// maps domains to their DNS challenge providers, walking the domain hierarchy for lookups +pub struct ProviderMux { + providers: HashMap>, +} + +impl ProviderMux { + pub fn new() -> Self { + Self { + providers: HashMap::new(), + } + } + + pub async fn from_config(config: &ProviderConfig) -> Result { + let mut mux = Self::new(); + + for entry in &config.providers { + let provider: Arc = match entry.provider_type.as_str() { + "cloudflare" => { + let api_token = entry + .config + .get("api_token") + .context("cloudflare provider requires 'api_token' in config")?; + Arc::new(CloudflareProvider::new(api_token.clone())) + } + "route53" => { + let region = entry.config.get("region").cloned(); + Arc::new(Route53Provider::new(region).await) + } + "exec" => { + let command = entry + .config + .get("command") + .context("exec provider requires 'command' in config")?; + Arc::new(ExecProvider::new(command.clone())) + } + other => anyhow::bail!("unsupported DNS provider type: {other}"), + }; + + for domain in &entry.domains { + mux.add(domain.clone(), Arc::clone(&provider)); + } + } + + Ok(mux) + } + + pub fn add(&mut self, domain: String, provider: Arc) { + self.providers.insert(domain, provider); + } + + pub fn find(&self, domain: &str) -> Option> { + let parts: Vec<&str> = domain.split('.').collect(); + for i in 0..parts.len() { + let candidate = parts[i..].join("."); + if let Some(provider) = self.providers.get(&candidate) { + return Some(Arc::clone(provider)); + } + } + None + } + + pub fn domains(&self) -> Vec { + self.providers.keys().cloned().collect() + } +} diff --git a/crates/funnel-server/src/tls/provider/route53.rs b/crates/funnel-server/src/tls/provider/route53.rs new file mode 100644 index 0000000..0122bf6 --- /dev/null +++ b/crates/funnel-server/src/tls/provider/route53.rs @@ -0,0 +1,112 @@ +use anyhow::{Context, Result}; +use aws_sdk_route53::types::{ + Change, ChangeAction, ChangeBatch, ResourceRecord, ResourceRecordSet, RrType, +}; + +use super::{BoxFuture, DnsChallenger}; + +const TXT_TTL: i64 = 60; + +pub struct Route53Provider { + client: aws_sdk_route53::Client, +} + +impl Route53Provider { + pub async fn new(region: Option) -> Self { + let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()); + if let Some(region) = region { + loader = loader.region(aws_config::Region::new(region)); + } + let config = loader.load().await; + Self { + client: aws_sdk_route53::Client::new(&config), + } + } + + async fn find_hosted_zone(&self, domain: &str) -> Result { + let parts: Vec<&str> = domain.split('.').collect(); + for i in 0..parts.len().saturating_sub(1) { + let zone_name = parts[i..].join("."); + let resp = self + .client + .list_hosted_zones_by_name() + .dns_name(&zone_name) + .max_items(1) + .send() + .await + .context("failed to list hosted zones")?; + + if let Some(zone) = resp.hosted_zones().first() { + let normalized_name = zone.name().trim_end_matches('.'); + if normalized_name == zone_name { + let zone_id = zone.id().trim_start_matches("/hostedzone/"); + return Ok(zone_id.to_string()); + } + } + } + anyhow::bail!("no route53 hosted zone found for domain: {domain}") + } + + async fn change_record( + &self, + action: ChangeAction, + record_name: &str, + value: &str, + ) -> Result<()> { + let zone_id = self.find_hosted_zone(record_name).await?; + + let record = ResourceRecord::builder() + .value(format!("\"{value}\"")) + .build() + .context("failed to build resource record")?; + + let record_set = ResourceRecordSet::builder() + .name(record_name) + .r#type(RrType::Txt) + .ttl(TXT_TTL) + .resource_records(record) + .build() + .context("failed to build resource record set")?; + + let change = Change::builder() + .action(action) + .resource_record_set(record_set) + .build() + .context("failed to build change")?; + + let batch = ChangeBatch::builder() + .changes(change) + .build() + .context("failed to build change batch")?; + + self.client + .change_resource_record_sets() + .hosted_zone_id(&zone_id) + .change_batch(batch) + .send() + .await + .context("failed to change route53 record")?; + + Ok(()) + } +} + +impl DnsChallenger for Route53Provider { + fn present(&self, record_name: &str, value: &str) -> BoxFuture<'_, Result<()>> { + let record_name = record_name.to_string(); + let value = value.to_string(); + Box::pin(async move { + self.change_record(ChangeAction::Upsert, &record_name, &value) + .await + }) + } + + fn cleanup(&self, record_name: &str, value: &str) -> BoxFuture<'_, Result<()>> { + let record_name = record_name.to_string(); + let value = value.to_string(); + Box::pin(async move { + self.change_record(ChangeAction::Delete, &record_name, &value) + .await + }) + } +} diff --git a/flake.lock b/flake.lock index 881f35a..53d7077 100644 --- a/flake.lock +++ b/flake.lock @@ -19,9 +19,30 @@ "root": { "inputs": { "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", "systems": "systems" } }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777691680, + "narHash": "sha256-sdCAzrPAaKu+yo7L2pWddy5PN6U9bO++WEWc1zcr7aQ=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "4757db4358c77c1cbe878fa5990e6ea88d82f6b5", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, "systems": { "locked": { "lastModified": 1681028828, From d9d9481d3c07279cd83469d725f6ec31884eb4c7 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 3 May 2026 20:55:39 +0200 Subject: [PATCH 17/18] feat(protocol): replace websocket tunneling with quic --- Cargo.lock | 420 ++++++++++++++---- Cargo.toml | 1 + crates/funnel-client/Cargo.toml | 11 +- crates/funnel-client/src/forwarder.rs | 56 +-- crates/funnel-client/src/main.rs | 29 +- crates/funnel-client/src/runner.rs | 21 +- crates/funnel-client/src/tunnel.rs | 416 ++++++++--------- crates/funnel-core/Cargo.toml | 4 +- crates/funnel-core/src/error.rs | 2 +- crates/funnel-core/src/protocol/frame.rs | 110 +++++ crates/funnel-core/src/protocol/handshake.rs | 47 ++ crates/funnel-core/src/protocol/message.rs | 179 -------- crates/funnel-core/src/protocol/mod.rs | 11 +- crates/funnel-core/src/protocol/request.rs | 51 +++ crates/funnel-core/src/tunnel/id.rs | 43 +- crates/funnel-e2e/Cargo.toml | 9 + crates/funnel-e2e/src/lib.rs | 0 crates/funnel-e2e/tests/e2e/harness.rs | 207 +++++++++ crates/funnel-e2e/tests/e2e/main.rs | 136 ++++++ crates/funnel-server/Cargo.toml | 10 +- crates/funnel-server/src/app.rs | 19 +- crates/funnel-server/src/error.rs | 25 +- crates/funnel-server/src/main.rs | 112 ++++- crates/funnel-server/src/metrics.rs | 21 + crates/funnel-server/src/proxy/router.rs | 71 +-- crates/funnel-server/src/quic/handler.rs | 76 ++++ crates/funnel-server/src/quic/listener.rs | 30 ++ crates/funnel-server/src/quic/mod.rs | 2 + crates/funnel-server/src/tls/manager.rs | 22 +- crates/funnel-server/src/tls/mod.rs | 4 - crates/funnel-server/src/tunnel/connection.rs | 300 ++++++------- crates/funnel-server/src/tunnel/manager.rs | 11 - crates/funnel-server/src/tunnel/stats.rs | 1 - crates/funnel-server/src/ws/handler.rs | 68 --- crates/funnel-server/src/ws/mod.rs | 1 - scripts/e2e.sh | 4 + 36 files changed, 1619 insertions(+), 911 deletions(-) create mode 100644 crates/funnel-core/src/protocol/frame.rs create mode 100644 crates/funnel-core/src/protocol/handshake.rs delete mode 100644 crates/funnel-core/src/protocol/message.rs create mode 100644 crates/funnel-core/src/protocol/request.rs create mode 100644 crates/funnel-e2e/Cargo.toml create mode 100644 crates/funnel-e2e/src/lib.rs create mode 100644 crates/funnel-e2e/tests/e2e/harness.rs create mode 100644 crates/funnel-e2e/tests/e2e/main.rs create mode 100644 crates/funnel-server/src/metrics.rs create mode 100644 crates/funnel-server/src/quic/handler.rs create mode 100644 crates/funnel-server/src/quic/listener.rs create mode 100644 crates/funnel-server/src/quic/mod.rs delete mode 100644 crates/funnel-server/src/ws/handler.rs delete mode 100644 crates/funnel-server/src/ws/mod.rs create mode 100755 scripts/e2e.sh diff --git a/Cargo.lock b/Cargo.lock index 285271d..57b6302 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,7 +685,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core 0.5.6", - "base64", "bytes", "form_urlencoded", "futures-util", @@ -704,10 +703,8 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -916,6 +913,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -1168,6 +1171,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1296,9 +1308,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", @@ -1429,6 +1441,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.4", + "siphasher", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1503,19 +1527,18 @@ dependencies = [ "clap", "dirs", "funnel-core", - "futures", "http 1.4.0", + "quinn", "reqwest 0.12.28", + "rustls 0.23.40", "serde", - "serde_json", "tokio", - "tokio-tungstenite 0.26.2", "tokio-util", "toml 0.8.23", "tracing", "tracing-subscriber", "url", - "uuid", + "webpki-roots 1.0.7", ] [[package]] @@ -1530,11 +1553,21 @@ dependencies = [ "nanoid", "pretty_assertions", "regex", + "rmp-serde", "serde", "serde_json", "sha2 0.10.9", "thiserror 2.0.18", - "uuid", + "tokio", +] + +[[package]] +name = "funnel-e2e" +version = "0.1.0" +dependencies = [ + "axum 0.8.9", + "reqwest 0.12.28", + "tokio", ] [[package]] @@ -1552,12 +1585,16 @@ dependencies = [ "clap", "dashmap", "funnel-core", - "futures", + "http-body-util", "instant-acme", "ipnetwork", "jsonwebtoken", "lru", + "metrics", + "metrics-exporter-prometheus", "pretty_assertions", + "quinn", + "rcgen 0.13.2", "reqwest 0.13.3", "rustls 0.23.40", "rustls-pemfile", @@ -1890,7 +1927,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -2073,7 +2110,7 @@ dependencies = [ "hyper-util", "rustls 0.23.40", "rustls-native-certs", - "rustls-platform-verifier", + "rustls-platform-verifier 0.7.0", "tokio", "tokio-rustls 0.26.4", "tower-service", @@ -2265,7 +2302,7 @@ dependencies = [ "hyper 1.9.0", "hyper-rustls 0.27.9", "hyper-util", - "rcgen", + "rcgen 0.14.7", "rustls 0.23.40", "rustls-pki-types", "serde", @@ -2326,6 +2363,22 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -2335,7 +2388,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.18", @@ -2356,6 +2409,15 @@ dependencies = [ "syn", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -2701,6 +2763,53 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "metrics" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071" +dependencies = [ + "portable-atomic", + "rapidhash", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +dependencies = [ + "base64", + "http-body-util", + "hyper 1.9.0", + "hyper-rustls 0.27.9", + "hyper-util", + "indexmap", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.5", + "metrics", + "quanta", + "rand 0.9.4", + "rand_xoshiro", + "sketches-ddsketch", +] + [[package]] name = "mime" version = "0.3.17" @@ -3027,6 +3136,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.5" @@ -3126,6 +3241,21 @@ dependencies = [ "yansi", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quinn" version = "0.11.9" @@ -3154,6 +3284,7 @@ checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", + "fastbloom", "getrandom 0.3.4", "lru-slab", "rand 0.9.4", @@ -3161,6 +3292,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.40", "rustls-pki-types", + "rustls-platform-verifier 0.6.2", "slab", "thiserror 2.0.18", "tinyvec", @@ -3284,6 +3416,46 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "rcgen" version = "0.14.7" @@ -3420,6 +3592,7 @@ dependencies = [ "base64", "bytes", "futures-core", + "futures-util", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -3439,12 +3612,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls 0.26.4", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots 1.0.7", ] @@ -3471,7 +3646,7 @@ dependencies = [ "quinn", "rustls 0.23.40", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.7.0", "serde", "serde_json", "serde_urlencoded", @@ -3510,6 +3685,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "rsa" version = "0.9.10" @@ -3644,6 +3838,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.21.1", + "log", + "once_cell", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.13", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + [[package]] name = "rustls-platform-verifier" version = "0.7.0" @@ -3652,7 +3867,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls 0.23.40", @@ -3956,7 +4171,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -4022,6 +4237,18 @@ dependencies = [ "time", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "sketches-ddsketch" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" + [[package]] name = "slab" version = "0.4.12" @@ -4564,34 +4791,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" -dependencies = [ - "futures-util", - "log", - "rustls 0.23.40", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tungstenite 0.26.2", - "webpki-roots 0.26.11", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.29.0", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -4805,41 +5004,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" -dependencies = [ - "bytes", - "data-encoding", - "http 1.4.0", - "httparse", - "log", - "rand 0.9.4", - "rustls 0.23.40", - "rustls-pki-types", - "sha1", - "thiserror 2.0.18", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" -dependencies = [ - "bytes", - "data-encoding", - "http 1.4.0", - "httparse", - "log", - "rand 0.9.4", - "sha1", - "thiserror 2.0.18", -] - [[package]] name = "typed-builder" version = "0.20.1" @@ -4941,12 +5105,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8-width" version = "0.1.8" @@ -5209,6 +5367,22 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -5218,6 +5392,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -5277,6 +5457,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -5313,6 +5502,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -5361,6 +5565,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5379,6 +5589,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -5397,6 +5613,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -5427,6 +5649,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -5445,6 +5673,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -5463,6 +5697,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -5481,6 +5721,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 7a6cf66..6a93871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/funnel-server", "crates/funnel-client", "crates/funnel-web", + "crates/funnel-e2e", ] [workspace.package] diff --git a/crates/funnel-client/Cargo.toml b/crates/funnel-client/Cargo.toml index a87bb2c..8c5b1dd 100644 --- a/crates/funnel-client/Cargo.toml +++ b/crates/funnel-client/Cargo.toml @@ -9,16 +9,15 @@ funnel-core = { path = "../funnel-core" } anyhow = "1" clap = { version = "4", features = ["derive"] } dirs = "6" -futures = "0.3" http = "1" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +quinn = "0.11" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } serde = { version = "1", features = ["derive"] } -serde_json = "1" tokio = { version = "1", features = ["full"] } -tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] } -tokio-util = "0.7" +tokio-util = { version = "0.7", features = ["io"] } toml = "0.8" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = "2" -uuid = { version = "1", features = ["v7"] } +webpki-roots = "1" diff --git a/crates/funnel-client/src/forwarder.rs b/crates/funnel-client/src/forwarder.rs index d9f9f87..552008e 100644 --- a/crates/funnel-client/src/forwarder.rs +++ b/crates/funnel-client/src/forwarder.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use funnel_core::protocol::{RequestPayload, ResponsePayload}; +use funnel_core::protocol::{RequestMeta, ResponseMeta}; const HOP_BY_HOP_HEADERS: &[&str] = &[ "connection", @@ -31,23 +31,31 @@ impl Forwarder { Self { client, local_addr } } - pub async fn forward(&self, req: RequestPayload) -> ResponsePayload { - match self.try_forward(&req).await { + pub async fn forward( + &self, + meta: RequestMeta, + body: reqwest::Body, + ) -> (ResponseMeta, Vec) { + match self.try_forward(&meta, body).await { Ok(resp) => resp, Err(e) => { - tracing::error!(error = %e, method = %req.method, path = %req.path, "forwarding failed"); + tracing::error!(error = %e, method = %meta.method, path = %meta.path, "forwarding failed"); error_response(502, &format!("local service error: {e}")) } } } - async fn try_forward(&self, req: &RequestPayload) -> anyhow::Result { - let url = format!("http://{}{}", self.local_addr, req.path); + async fn try_forward( + &self, + meta: &RequestMeta, + body: reqwest::Body, + ) -> anyhow::Result<(ResponseMeta, Vec)> { + let url = format!("http://{}{}", self.local_addr, meta.path); - let method: http::Method = req.method.parse()?; + let method: http::Method = meta.method.parse()?; let mut builder = self.client.request(method, &url); - for (name, values) in &req.headers { + for (name, values) in &meta.headers { if is_hop_by_hop(name) || name.eq_ignore_ascii_case("host") { continue; } @@ -57,7 +65,7 @@ impl Forwarder { } builder = builder.header("host", &self.local_addr); - builder = builder.body(req.body.clone()); + builder = builder.body(body); let resp = builder.send().await?; @@ -65,17 +73,14 @@ impl Forwarder { let headers = collect_response_headers(resp.headers()); let body = resp.bytes().await?.to_vec(); - Ok(ResponsePayload { - status, - headers, - body, - }) + Ok((ResponseMeta { status, headers }, body)) } } fn is_hop_by_hop(name: &str) -> bool { - let lower = name.to_ascii_lowercase(); - HOP_BY_HOP_HEADERS.iter().any(|h| *h == lower) + HOP_BY_HOP_HEADERS + .iter() + .any(|h| h.eq_ignore_ascii_case(name)) } fn collect_response_headers(headers: &reqwest::header::HeaderMap) -> HashMap> { @@ -90,14 +95,13 @@ fn collect_response_headers(headers: &reqwest::header::HeaderMap) -> HashMap ResponsePayload { +fn error_response(status: u16, msg: &str) -> (ResponseMeta, Vec) { let mut headers = HashMap::new(); headers.insert("content-type".to_string(), vec!["text/plain".to_string()]); - ResponsePayload { - status, - headers, - body: msg.as_bytes().to_vec(), - } + ( + ResponseMeta { status, headers }, + msg.as_bytes().to_vec(), + ) } #[cfg(test)] @@ -115,9 +119,9 @@ mod tests { #[test] fn error_response_has_correct_status() { - let resp = error_response(502, "test error"); - assert_eq!(resp.status, 502); - assert_eq!(resp.body, b"test error"); - assert!(resp.headers.contains_key("content-type")); + let (meta, body) = error_response(502, "test error"); + assert_eq!(meta.status, 502); + assert_eq!(body, b"test error"); + assert!(meta.headers.contains_key("content-type")); } } diff --git a/crates/funnel-client/src/main.rs b/crates/funnel-client/src/main.rs index bb2953f..11ced8b 100644 --- a/crates/funnel-client/src/main.rs +++ b/crates/funnel-client/src/main.rs @@ -38,6 +38,14 @@ enum Command { /// authentication token (overrides config) #[arg(short, long)] token: Option, + + /// quic port on the server + #[arg(long, default_value_t = 4433)] + quic_port: u16, + + /// skip tls certificate verification (for development with self signed certs) + #[arg(long)] + insecure: bool, }, /// manage client configuration Config { @@ -68,6 +76,10 @@ enum ConfigAction { #[tokio::main] async fn main() -> anyhow::Result<()> { + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install default crypto provider"); + tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), @@ -83,7 +95,9 @@ async fn main() -> anyhow::Result<()> { id, inlet, token, - } => run_http(address, server, id, inlet, token).await?, + quic_port, + insecure, + } => run_http(address, server, id, inlet, token, quic_port, insecure).await?, Command::Config { action } => run_config(action)?, } @@ -96,6 +110,8 @@ async fn run_http( id_flag: Option, inlet_name: String, token_flag: Option, + quic_port: u16, + insecure: bool, ) -> anyhow::Result<()> { let local_addr = normalize_address(&address); @@ -128,6 +144,15 @@ async fn run_http( ); } + let client = tunnel::TunnelClient::new( + tunnel_id, + server_url, + local_addr, + token, + quic_port, + insecure, + )?; + let shutdown = CancellationToken::new(); let shutdown_signal = shutdown.clone(); @@ -137,7 +162,7 @@ async fn run_http( shutdown_signal.cancel(); }); - runner::run(tunnel_id, server_url, local_addr, token, shutdown).await; + runner::run(&client, shutdown).await; Ok(()) } diff --git a/crates/funnel-client/src/runner.rs b/crates/funnel-client/src/runner.rs index a6ab15a..305849f 100644 --- a/crates/funnel-client/src/runner.rs +++ b/crates/funnel-client/src/runner.rs @@ -10,13 +10,7 @@ use crate::tunnel::TunnelClient; const INITIAL_BACKOFF: Duration = Duration::from_secs(1); const MAX_BACKOFF: Duration = Duration::from_secs(30); -pub async fn run( - tunnel_id: TunnelId, - server_url: String, - local_addr: String, - token: Option, - shutdown: CancellationToken, -) { +pub async fn run(client: &TunnelClient, shutdown: CancellationToken) { let mut attempt: u32 = 0; loop { @@ -24,13 +18,6 @@ pub async fn run( break; } - let client = TunnelClient::new( - tunnel_id.clone(), - server_url.clone(), - local_addr.clone(), - token.clone(), - ); - tracing::info!(attempt = attempt + 1, "connecting to server"); let run_cancel = shutdown.child_token(); @@ -95,21 +82,21 @@ mod tests { #[test] fn public_url_http() { - let id = TunnelId::new("my-tunnel").unwrap(); + let id = TunnelId::new("my-tunnel").expect("valid tunnel id"); let url = build_public_url("http://tunnel.example.com", &id); assert_eq!(url.as_deref(), Some("http://my-tunnel.tunnel.example.com")); } #[test] fn public_url_https() { - let id = TunnelId::new("my-tunnel").unwrap(); + let id = TunnelId::new("my-tunnel").expect("valid tunnel id"); let url = build_public_url("https://tunnel.example.com", &id); assert_eq!(url.as_deref(), Some("https://my-tunnel.tunnel.example.com")); } #[test] fn public_url_with_port() { - let id = TunnelId::new("abc").unwrap(); + let id = TunnelId::new("abc").expect("valid tunnel id"); let url = build_public_url("http://localhost:8080", &id); assert_eq!(url.as_deref(), Some("http://abc.localhost:8080")); } diff --git a/crates/funnel-client/src/tunnel.rs b/crates/funnel-client/src/tunnel.rs index b754a6e..ce1c397 100644 --- a/crates/funnel-client/src/tunnel.rs +++ b/crates/funnel-client/src/tunnel.rs @@ -1,27 +1,26 @@ +use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; -use futures::{SinkExt, StreamExt}; -use tokio::sync::mpsc; -use tokio_tungstenite::tungstenite::Message; +use tokio_util::io::ReaderStream; use tokio_util::sync::CancellationToken; use url::Url; -use funnel_core::protocol::TunnelMessage; +use funnel_core::protocol::{self, Handshake, HandshakeResponse, RequestMeta, QUIC_ALPN}; use funnel_core::tunnel::TunnelId; use crate::forwarder::Forwarder; -const CHANNEL_BUFFER: usize = 128; -const PING_INTERVAL: Duration = Duration::from_secs(30); const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(15); const MAX_CONCURRENT_REQUESTS: usize = 128; pub struct TunnelClient { pub tunnel_id: TunnelId, - server_url: String, local_addr: String, token: Option, + quic_addr: SocketAddr, + host: String, + endpoint: quinn::Endpoint, } impl TunnelClient { @@ -30,274 +29,211 @@ impl TunnelClient { server_url: String, local_addr: String, token: Option, - ) -> Self { - Self { + quic_port: u16, + insecure: bool, + ) -> anyhow::Result { + let url = Url::parse(&server_url)?; + let host = url + .host_str() + .ok_or_else(|| anyhow::anyhow!("no host in server url"))? + .to_string(); + + let quic_addr = resolve_quic_addr(&host, quic_port)?; + let client_config = build_client_config(insecure)?; + + // bind address must match the remote address family + let bind_addr: SocketAddr = if quic_addr.is_ipv6() { + SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, 0)) + } else { + SocketAddr::from((std::net::Ipv4Addr::UNSPECIFIED, 0)) + }; + + let mut endpoint = quinn::Endpoint::client(bind_addr)?; + endpoint.set_default_client_config(client_config); + + Ok(Self { tunnel_id, - server_url, local_addr, token, - } + quic_addr, + host, + endpoint, + }) } - /// connect to the server and run the tunnel until it disconnects. + /// connect to the server via quic and run the tunnel until it disconnects. pub async fn run(&self, cancel: CancellationToken) -> anyhow::Result<()> { - let ws = self.connect().await?; - let (ws_sink, ws_stream) = ws.split(); - - let (outgoing_tx, outgoing_rx) = mpsc::channel::(CHANNEL_BUFFER); + let conn = self.connect().await?; let forwarder = Arc::new(Forwarder::new(self.local_addr.clone())); let semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_REQUESTS)); - let read_cancel = cancel.clone(); - let read_tx = outgoing_tx.clone(); - let read_forwarder = Arc::clone(&forwarder); - let read_semaphore = Arc::clone(&semaphore); - let read_tunnel_id = self.tunnel_id.clone(); - let read_handle = tokio::spawn(async move { - read_loop( - ws_stream, - read_tx, - read_forwarder, - read_semaphore, - read_tunnel_id, - read_cancel, - ) - .await; - }); - - let write_cancel = cancel.clone(); - let write_handle = tokio::spawn(async move { - write_loop(ws_sink, outgoing_rx, write_cancel).await; - }); - - let ping_cancel = cancel.clone(); - let ping_tx = outgoing_tx.clone(); - let ping_handle = tokio::spawn(async move { - heartbeat_loop(ping_tx, ping_cancel).await; - }); - - tokio::select! { - _ = read_handle => {}, - _ = write_handle => {}, - _ = ping_handle => {}, - _ = cancel.cancelled() => {}, + loop { + tokio::select! { + bi = conn.accept_bi() => { + let (send, recv) = match bi { + Ok(pair) => pair, + Err(e) => { + tracing::debug!(error = %e, "connection closed"); + break; + } + }; + + let fwd = Arc::clone(&forwarder); + let sem = Arc::clone(&semaphore); + let cancel = cancel.clone(); + + tokio::spawn(async move { + let _permit = tokio::select! { + permit = sem.acquire() => match permit { + Ok(p) => p, + Err(_) => return, + }, + _ = cancel.cancelled() => return, + }; + + if let Err(e) = handle_stream(send, recv, &fwd).await { + tracing::debug!(error = %e, "stream handler error"); + } + }); + } + _ = cancel.cancelled() => break, + } } + conn.close(quinn::VarInt::from_u32(0), b"client shutdown"); Ok(()) } - async fn connect( - &self, - ) -> anyhow::Result< - tokio_tungstenite::WebSocketStream< - tokio_tungstenite::MaybeTlsStream, - >, - > { - let mut url = Url::parse(&self.server_url)?; - - let ws_scheme = if url.scheme() == "https" { "wss" } else { "ws" }; - url.set_scheme(ws_scheme) - .map_err(|_| anyhow::anyhow!("failed to set websocket scheme on url: {url}"))?; - - url.set_path("/ws"); - url.query_pairs_mut() - .append_pair("id", self.tunnel_id.as_ref()); - - use tokio_tungstenite::tungstenite::client::IntoClientRequest; - - let mut request = url.as_str().into_client_request()?; - - if let Some(token) = &self.token { - request.headers_mut().insert( - "Authorization", - format!("Bearer {token}").parse()?, - ); - } + async fn connect(&self) -> anyhow::Result { + tracing::debug!(addr = %self.quic_addr, host = %self.host, "connecting via quic"); - tracing::debug!(url = %url, "connecting to server"); + let connecting = self.endpoint.connect(self.quic_addr, &self.host)?; - let (ws, _response) = - tokio::time::timeout(HANDSHAKE_TIMEOUT, tokio_tungstenite::connect_async(request)) - .await - .map_err(|_| anyhow::anyhow!("connection timed out"))??; + let conn = tokio::time::timeout(HANDSHAKE_TIMEOUT, connecting) + .await + .map_err(|_| anyhow::anyhow!("connection timed out")) + .and_then(|r| r.map_err(Into::into))?; - tracing::info!(tunnel_id = %self.tunnel_id, "websocket connected"); - Ok(ws) - } -} + let (mut send, mut recv) = conn.open_bi().await?; -async fn read_loop( - mut stream: futures::stream::SplitStream< - tokio_tungstenite::WebSocketStream< - tokio_tungstenite::MaybeTlsStream, - >, - >, - outgoing_tx: mpsc::Sender, - forwarder: Arc, - semaphore: Arc, - tunnel_id: TunnelId, - cancel: CancellationToken, -) { - loop { - let msg = tokio::select! { - msg = stream.next() => msg, - _ = cancel.cancelled() => break, + let handshake = Handshake { + tunnel_id: self.tunnel_id.clone(), + token: self.token.clone(), }; - let Some(result) = msg else { break }; + protocol::write_meta(&mut send, &handshake).await?; - let frame = match result { - Ok(f) => f, - Err(e) => { - tracing::debug!(error = %e, "websocket read error"); - break; - } - }; - - match frame { - Message::Text(text) => { - handle_text_message( - &text, - &outgoing_tx, - &forwarder, - &semaphore, - &tunnel_id, - &cancel, - ); - } - Message::Close(_) => break, - Message::Ping(_) => { - // tungstenite handles pong automatically - } - _ => {} - } - } + let resp: HandshakeResponse = protocol::read_meta(&mut recv).await?; + resp.into_result()?; - tracing::debug!("read loop exiting"); - cancel.cancel(); -} + tracing::info!(tunnel_id = %self.tunnel_id, "quic tunnel connected"); -fn handle_text_message( - text: &str, - outgoing_tx: &mpsc::Sender, - forwarder: &Arc, - semaphore: &Arc, - tunnel_id: &TunnelId, - cancel: &CancellationToken, -) { - let msg: TunnelMessage = match serde_json::from_str(text) { - Ok(m) => m, - Err(e) => { - tracing::warn!(error = %e, "malformed message from server"); - return; - } - }; + // keep the control stream alive in the background; dropping send + // would finish the stream, which the server interprets as disconnect + tokio::spawn(async move { + let _send = send; + let mut buf = [0u8; 1]; + let _ = recv.read(&mut buf).await; + }); - match msg { - TunnelMessage::Request { - request_id, - payload, - .. - } => { - tracing::debug!( - request_id = %request_id, - method = %payload.method, - path = %payload.path, - "received request" - ); - - let tx = outgoing_tx.clone(); - let fwd = Arc::clone(forwarder); - let sem = Arc::clone(semaphore); - let cancel = cancel.clone(); - let tid = tunnel_id.clone(); - - tokio::spawn(async move { - let _permit = tokio::select! { - permit = sem.acquire() => match permit { - Ok(p) => p, - Err(_) => return, - }, - _ = cancel.cancelled() => return, - }; - - let response = fwd.forward(payload).await; - - let msg = TunnelMessage::Response { - request_id, - payload: response, - }; - - if tx.send(msg).await.is_err() { - tracing::debug!(tunnel_id = %tid, "outgoing channel closed"); - } - }); - } - TunnelMessage::RequestCancel { request_id } => { - tracing::debug!(request_id = %request_id, "request cancelled by server"); - // cancellation of individual in flight requests would require - // tracking cancel tokens per request. for now we just log it; - // the forwarded request will complete and its response will be - // silently dropped by the server since the pending entry is gone. - } - TunnelMessage::Pong => { - tracing::debug!("pong received"); - } - other => { - tracing::debug!(msg_type = ?other, "unhandled message type"); - } + Ok(conn) } } -async fn write_loop( - mut sink: futures::stream::SplitSink< - tokio_tungstenite::WebSocketStream< - tokio_tungstenite::MaybeTlsStream, - >, - Message, - >, - mut rx: mpsc::Receiver, - cancel: CancellationToken, -) { - loop { - let msg = tokio::select! { - msg = rx.recv() => msg, - _ = cancel.cancelled() => break, - }; +async fn handle_stream( + mut send: quinn::SendStream, + mut recv: quinn::RecvStream, + forwarder: &Forwarder, +) -> anyhow::Result<()> { + let meta: RequestMeta = protocol::read_meta(&mut recv).await?; - let Some(msg) = msg else { break }; + tracing::debug!( + method = %meta.method, + path = %meta.path, + "received request" + ); - let json = match serde_json::to_string(&msg) { - Ok(j) => j, - Err(e) => { - tracing::error!(error = %e, "failed to serialize message"); - continue; - } - }; + // stream body from quic directly to local service (no buffer cap) + let body_stream = ReaderStream::new(recv); + let body = reqwest::Body::wrap_stream(body_stream); - if sink.send(Message::text(json)).await.is_err() { - break; - } - } + let (resp_meta, resp_body) = forwarder.forward(meta, body).await; + + protocol::write_meta(&mut send, &resp_meta).await?; + send.write_all(&resp_body).await?; + send.finish()?; - tracing::debug!("write loop exiting"); - cancel.cancel(); + Ok(()) } -async fn heartbeat_loop(tx: mpsc::Sender, cancel: CancellationToken) { - let mut interval = tokio::time::interval(PING_INTERVAL); - interval.tick().await; // skip first immediate tick +fn resolve_quic_addr(host: &str, port: u16) -> anyhow::Result { + let addr_str = format!("{host}:{port}"); + addr_str.parse().or_else(|_| { + use std::net::ToSocketAddrs; + addr_str + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow::anyhow!("failed to resolve host: {host}")) + }) +} - loop { - tokio::select! { - _ = interval.tick() => {}, - _ = cancel.cancelled() => break, - } +fn build_client_config(insecure: bool) -> anyhow::Result { + let mut crypto = if insecure { + rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(SkipVerification)) + .with_no_client_auth() + } else { + let mut roots = rustls::RootCertStore::empty(); + roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth() + }; - if tx.send(TunnelMessage::Ping).await.is_err() { - break; - } + crypto.alpn_protocols = vec![QUIC_ALPN.to_vec()]; + + let quic_config = quinn::crypto::rustls::QuicClientConfig::try_from(crypto)?; + Ok(quinn::ClientConfig::new(Arc::new(quic_config))) +} + +/// certificate verifier that accepts any certificate (for dev/insecure mode) +#[derive(Debug)] +struct SkipVerification; + +impl rustls::client::danger::ServerCertVerifier for SkipVerification { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) } - tracing::debug!("heartbeat loop exiting"); + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } } diff --git a/crates/funnel-core/Cargo.toml b/crates/funnel-core/Cargo.toml index 18a0fcc..5d17dd6 100644 --- a/crates/funnel-core/Cargo.toml +++ b/crates/funnel-core/Cargo.toml @@ -11,11 +11,13 @@ hex = "0.4" http = "1" nanoid = "0.4" regex = "1" +rmp-serde = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" thiserror = "2" -uuid = { version = "1", features = ["v7", "serde"] } +tokio = { version = "1", features = ["io-util"] } [dev-dependencies] pretty_assertions = "1" +tokio = { version = "1", features = ["io-util", "rt", "macros"] } diff --git a/crates/funnel-core/src/error.rs b/crates/funnel-core/src/error.rs index 3876b98..b6b979c 100644 --- a/crates/funnel-core/src/error.rs +++ b/crates/funnel-core/src/error.rs @@ -1,7 +1,7 @@ use thiserror::Error; #[derive(Debug, Error)] -pub enum ValidationError { +pub enum TunnelIdError { #[error("tunnel ID cannot be empty")] Empty, diff --git a/crates/funnel-core/src/protocol/frame.rs b/crates/funnel-core/src/protocol/frame.rs new file mode 100644 index 0000000..092099c --- /dev/null +++ b/crates/funnel-core/src/protocol/frame.rs @@ -0,0 +1,110 @@ +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +const MAX_META_SIZE: u32 = 1_048_576; // 1mb + +#[derive(Debug, thiserror::Error)] +pub enum FrameError { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + + #[error("frame too large: {size} bytes (max {max})")] + TooLarge { size: u32, max: u32 }, + + #[error("encode error: {0}")] + Encode(#[from] rmp_serde::encode::Error), + + #[error("decode error: {0}")] + Decode(#[from] rmp_serde::decode::Error), +} + +/// write a length prefixed frame: [4 bytes big endian u32 length][data] +pub async fn write_frame( + writer: &mut W, + data: &[u8], +) -> Result<(), FrameError> { + let len = data.len() as u32; + writer.write_all(&len.to_be_bytes()).await?; + writer.write_all(data).await?; + Ok(()) +} + +/// read a length prefixed frame, rejecting frames larger than max_size +pub async fn read_frame( + reader: &mut R, + max_size: u32, +) -> Result, FrameError> { + let mut len_buf = [0u8; 4]; + reader.read_exact(&mut len_buf).await?; + let len = u32::from_be_bytes(len_buf); + + if len > max_size { + return Err(FrameError::TooLarge { + size: len, + max: max_size, + }); + } + + let mut buf = vec![0u8; len as usize]; + reader.read_exact(&mut buf).await?; + Ok(buf) +} + +/// serialize T as msgpack and write as a length prefixed frame +pub async fn write_meta( + writer: &mut W, + meta: &T, +) -> Result<(), FrameError> { + let data = rmp_serde::to_vec_named(meta)?; + write_frame(writer, &data).await +} + +/// read a length prefixed frame and deserialize as msgpack +pub async fn read_meta( + reader: &mut R, +) -> Result { + let data = read_frame(reader, MAX_META_SIZE).await?; + let value = rmp_serde::from_slice(&data)?; + Ok(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn frame_roundtrip() { + let data = b"hello world"; + let mut buf = Vec::new(); + write_frame(&mut buf, data).await.unwrap(); + + let mut cursor = std::io::Cursor::new(buf); + let result = read_frame(&mut cursor, 1024).await.unwrap(); + assert_eq!(result, data); + } + + #[tokio::test] + async fn frame_too_large() { + let data = vec![0u8; 100]; + let mut buf = Vec::new(); + write_frame(&mut buf, &data).await.unwrap(); + + let mut cursor = std::io::Cursor::new(buf); + let result = read_frame(&mut cursor, 50).await; + assert!(matches!(result, Err(FrameError::TooLarge { size: 100, max: 50 }))); + } + + #[tokio::test] + async fn meta_roundtrip() { + use std::collections::HashMap; + + let original: HashMap = + [("key".into(), "value".into())].into_iter().collect(); + + let mut buf = Vec::new(); + write_meta(&mut buf, &original).await.unwrap(); + + let mut cursor = std::io::Cursor::new(buf); + let decoded: HashMap = read_meta(&mut cursor).await.unwrap(); + assert_eq!(decoded, original); + } +} diff --git a/crates/funnel-core/src/protocol/handshake.rs b/crates/funnel-core/src/protocol/handshake.rs new file mode 100644 index 0000000..f9be233 --- /dev/null +++ b/crates/funnel-core/src/protocol/handshake.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; + +use crate::tunnel::TunnelId; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Handshake { + pub tunnel_id: TunnelId, + pub token: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HandshakeResponse { + success: bool, + error: Option, +} + +impl HandshakeResponse { + pub fn ok() -> Self { + Self { + success: true, + error: None, + } + } + + pub fn rejected(reason: impl Into) -> Self { + Self { + success: false, + error: Some(reason.into()), + } + } + + pub fn into_result(self) -> Result<(), HandshakeRejected> { + if self.success { + Ok(()) + } else { + Err(HandshakeRejected { + reason: self.error.unwrap_or_else(|| "unknown error".to_string()), + }) + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("handshake rejected: {reason}")] +pub struct HandshakeRejected { + pub reason: String, +} diff --git a/crates/funnel-core/src/protocol/message.rs b/crates/funnel-core/src/protocol/message.rs deleted file mode 100644 index a022cb7..0000000 --- a/crates/funnel-core/src/protocol/message.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::tunnel::TunnelId; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RequestPayload { - pub method: String, - pub path: String, - pub headers: HashMap>, - #[serde(with = "base64_bytes")] - pub body: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResponsePayload { - pub status: u16, - pub headers: HashMap>, - #[serde(with = "base64_bytes")] - pub body: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum TunnelMessage { - Request { - tunnel_id: TunnelId, - request_id: Uuid, - payload: RequestPayload, - }, - Response { - request_id: Uuid, - payload: ResponsePayload, - }, - Ping, - Pong, - RequestCancel { - request_id: Uuid, - }, -} - -mod base64_bytes { - use base64::prelude::*; - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn serialize(bytes: &Vec, s: S) -> Result { - s.serialize_str(&BASE64_STANDARD.encode(bytes)) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { - let s = String::deserialize(d)?; - BASE64_STANDARD.decode(&s).map_err(serde::de::Error::custom) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn serialize_request() { - let msg = TunnelMessage::Request { - tunnel_id: TunnelId::new("my-tunnel").unwrap(), - request_id: Uuid::nil(), - payload: RequestPayload { - method: "GET".into(), - path: "/hello".into(), - headers: HashMap::new(), - body: vec![], - }, - }; - - let json = serde_json::to_value(&msg).unwrap(); - assert_eq!(json["type"], "request"); - assert_eq!(json["tunnel_id"], "my-tunnel"); - assert_eq!(json["payload"]["method"], "GET"); - } - - #[test] - fn serialize_response() { - let msg = TunnelMessage::Response { - request_id: Uuid::nil(), - payload: ResponsePayload { - status: 200, - headers: HashMap::new(), - body: b"hello world".to_vec(), - }, - }; - - let json = serde_json::to_value(&msg).unwrap(); - assert_eq!(json["type"], "response"); - assert_eq!(json["payload"]["status"], 200); - } - - #[test] - fn serialize_ping_pong() { - let ping_json = serde_json::to_value(&TunnelMessage::Ping).unwrap(); - assert_eq!(ping_json["type"], "ping"); - - let pong_json = serde_json::to_value(&TunnelMessage::Pong).unwrap(); - assert_eq!(pong_json["type"], "pong"); - } - - #[test] - fn serialize_request_cancel() { - let id = Uuid::nil(); - let msg = TunnelMessage::RequestCancel { request_id: id }; - let json = serde_json::to_value(&msg).unwrap(); - assert_eq!(json["type"], "request_cancel"); - assert_eq!(json["request_id"], id.to_string()); - } - - #[test] - fn roundtrip_request() { - let msg = TunnelMessage::Request { - tunnel_id: TunnelId::new("test-abc").unwrap(), - request_id: Uuid::now_v7(), - payload: RequestPayload { - method: "POST".into(), - path: "/api/data".into(), - headers: { - let mut h = HashMap::new(); - h.insert("Content-Type".into(), vec!["application/json".into()]); - h - }, - body: b"{\"key\": \"value\"}".to_vec(), - }, - }; - - let json = serde_json::to_string(&msg).unwrap(); - let parsed: TunnelMessage = serde_json::from_str(&json).unwrap(); - - match parsed { - TunnelMessage::Request { - tunnel_id, - payload, - .. - } => { - assert_eq!(tunnel_id.as_ref(), "test-abc"); - assert_eq!(payload.method, "POST"); - assert_eq!(payload.body, b"{\"key\": \"value\"}"); - } - _ => panic!("expected Request variant"), - } - } - - #[test] - fn roundtrip_response_with_body() { - let body = vec![0u8, 1, 2, 255, 128]; - let msg = TunnelMessage::Response { - request_id: Uuid::now_v7(), - payload: ResponsePayload { - status: 201, - headers: HashMap::new(), - body: body.clone(), - }, - }; - - let json = serde_json::to_string(&msg).unwrap(); - let parsed: TunnelMessage = serde_json::from_str(&json).unwrap(); - - match parsed { - TunnelMessage::Response { payload, .. } => { - assert_eq!(payload.status, 201); - assert_eq!(payload.body, body); - } - _ => panic!("expected Response variant"), - } - } - - #[test] - fn deserialize_unknown_type_fails() { - let json = r#"{"type": "unknown_msg"}"#; - let result: Result = serde_json::from_str(json); - assert!(result.is_err()); - } -} diff --git a/crates/funnel-core/src/protocol/mod.rs b/crates/funnel-core/src/protocol/mod.rs index 8805705..8542261 100644 --- a/crates/funnel-core/src/protocol/mod.rs +++ b/crates/funnel-core/src/protocol/mod.rs @@ -1,3 +1,10 @@ -mod message; +pub mod frame; +pub mod handshake; +pub mod request; -pub use message::{RequestPayload, ResponsePayload, TunnelMessage}; +pub use frame::{read_frame, read_meta, write_frame, write_meta, FrameError}; +pub use handshake::{Handshake, HandshakeRejected, HandshakeResponse}; +pub use request::{RequestMeta, ResponseMeta}; + +/// application level protocol name used for QUIC ALPN negotiation +pub const QUIC_ALPN: &[u8] = b"funnel/1"; diff --git a/crates/funnel-core/src/protocol/request.rs b/crates/funnel-core/src/protocol/request.rs new file mode 100644 index 0000000..b437149 --- /dev/null +++ b/crates/funnel-core/src/protocol/request.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequestMeta { + pub method: String, + pub path: String, + pub headers: HashMap>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResponseMeta { + pub status: u16, + pub headers: HashMap>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_meta_roundtrip() { + let meta = RequestMeta { + method: "POST".into(), + path: "/api/data".into(), + headers: { + let mut h = HashMap::new(); + h.insert("content-type".into(), vec!["application/json".into()]); + h + }, + }; + + let encoded = rmp_serde::to_vec_named(&meta).unwrap(); + let decoded: RequestMeta = rmp_serde::from_slice(&encoded).unwrap(); + assert_eq!(decoded.method, "POST"); + assert_eq!(decoded.path, "/api/data"); + } + + #[test] + fn response_meta_roundtrip() { + let meta = ResponseMeta { + status: 200, + headers: HashMap::new(), + }; + + let encoded = rmp_serde::to_vec_named(&meta).unwrap(); + let decoded: ResponseMeta = rmp_serde::from_slice(&encoded).unwrap(); + assert_eq!(decoded.status, 200); + } +} diff --git a/crates/funnel-core/src/tunnel/id.rs b/crates/funnel-core/src/tunnel/id.rs index 821b9be..bc4fd10 100644 --- a/crates/funnel-core/src/tunnel/id.rs +++ b/crates/funnel-core/src/tunnel/id.rs @@ -1,9 +1,13 @@ use std::fmt; +use std::sync::LazyLock; use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::error::ValidationError; +use crate::error::TunnelIdError; + +static TUNNEL_ID_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$").expect("valid regex")); const DOMAIN_SAFE_ALPHABET: &[char] = &[ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', @@ -20,7 +24,7 @@ const MAX_LENGTH: usize = 63; pub struct TunnelId(String); impl TunnelId { - pub fn new(raw: impl Into) -> Result { + pub fn new(raw: impl Into) -> Result { let s = raw.into(); Self::validate(&s)?; Ok(Self(s)) @@ -31,23 +35,22 @@ impl TunnelId { Self(id) } - fn validate(s: &str) -> Result<(), ValidationError> { + fn validate(s: &str) -> Result<(), TunnelIdError> { if s.is_empty() { - return Err(ValidationError::Empty); + return Err(TunnelIdError::Empty); } if s.len() < MIN_LENGTH { - return Err(ValidationError::TooShort(s.len())); + return Err(TunnelIdError::TooShort(s.len())); } if s.len() > MAX_LENGTH { - return Err(ValidationError::TooLong(s.len())); + return Err(TunnelIdError::TooLong(s.len())); } if s != s.to_lowercase() { - return Err(ValidationError::NotLowercase); + return Err(TunnelIdError::NotLowercase); } - let re = Regex::new(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$").expect("valid regex"); - if !re.is_match(s) { - return Err(ValidationError::InvalidFormat); + if !TUNNEL_ID_REGEX.is_match(s) { + return Err(TunnelIdError::InvalidFormat); } Ok(()) @@ -73,7 +76,7 @@ impl From for String { } impl TryFrom for TunnelId { - type Error = ValidationError; + type Error = TunnelIdError; fn try_from(s: String) -> Result { Self::new(s) @@ -95,13 +98,13 @@ mod tests { #[test] fn empty_id() { - assert!(matches!(TunnelId::new(""), Err(ValidationError::Empty))); + assert!(matches!(TunnelId::new(""), Err(TunnelIdError::Empty))); } #[test] fn too_short() { - assert!(matches!(TunnelId::new("ab"), Err(ValidationError::TooShort(2)))); - assert!(matches!(TunnelId::new("a"), Err(ValidationError::TooShort(1)))); + assert!(matches!(TunnelId::new("ab"), Err(TunnelIdError::TooShort(2)))); + assert!(matches!(TunnelId::new("a"), Err(TunnelIdError::TooShort(1)))); } #[test] @@ -109,7 +112,7 @@ mod tests { let long = "a".repeat(MAX_LENGTH + 1); assert!(matches!( TunnelId::new(long), - Err(ValidationError::TooLong(64)) + Err(TunnelIdError::TooLong(64)) )); } @@ -117,16 +120,16 @@ mod tests { fn uppercase_rejected() { assert!(matches!( TunnelId::new("MyTunnel"), - Err(ValidationError::NotLowercase) + Err(TunnelIdError::NotLowercase) )); } #[test] fn invalid_format() { - assert!(matches!(TunnelId::new("-abc"), Err(ValidationError::InvalidFormat))); - assert!(matches!(TunnelId::new("abc-"), Err(ValidationError::InvalidFormat))); - assert!(matches!(TunnelId::new("ab_c"), Err(ValidationError::InvalidFormat))); - assert!(matches!(TunnelId::new("ab c"), Err(ValidationError::InvalidFormat))); + assert!(matches!(TunnelId::new("-abc"), Err(TunnelIdError::InvalidFormat))); + assert!(matches!(TunnelId::new("abc-"), Err(TunnelIdError::InvalidFormat))); + assert!(matches!(TunnelId::new("ab_c"), Err(TunnelIdError::InvalidFormat))); + assert!(matches!(TunnelId::new("ab c"), Err(TunnelIdError::InvalidFormat))); } #[test] diff --git a/crates/funnel-e2e/Cargo.toml b/crates/funnel-e2e/Cargo.toml new file mode 100644 index 0000000..128f369 --- /dev/null +++ b/crates/funnel-e2e/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "funnel-e2e" +version = "0.1.0" +edition = "2024" + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +axum = "0.8" diff --git a/crates/funnel-e2e/src/lib.rs b/crates/funnel-e2e/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/funnel-e2e/tests/e2e/harness.rs b/crates/funnel-e2e/tests/e2e/harness.rs new file mode 100644 index 0000000..de9ea9d --- /dev/null +++ b/crates/funnel-e2e/tests/e2e/harness.rs @@ -0,0 +1,207 @@ +use std::net::TcpListener; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; + +use axum::extract::DefaultBodyLimit; +use axum::routing::{get, post}; +use axum::Router; +use tokio::task::JoinHandle; + +const TUNNEL_ID: &str = "e2etest"; +const READY_TIMEOUT: Duration = Duration::from_secs(30); +const POLL_INTERVAL: Duration = Duration::from_millis(100); +const MOCK_BODY_LIMIT: usize = 4 * 1024 * 1024; + +pub struct TestEnv { + server_process: Child, + client_process: Child, + local_server_handle: JoinHandle<()>, + pub http_port: u16, + pub host_header: String, + pub client: reqwest::Client, +} + +impl TestEnv { + pub async fn start() -> Self { + let (local_handle, local_port) = start_mock_server(); + let http_port = free_port(); + let quic_port = free_port(); + let host_header = format!("{TUNNEL_ID}.localhost:{http_port}"); + + let server_process = start_server_process(http_port, quic_port); + wait_for_tcp(http_port).await; + + let client_process = start_client_process(local_port, http_port, quic_port, TUNNEL_ID); + + let client = reqwest::Client::new(); + wait_for_tunnel(&client, http_port, &host_header).await; + + Self { + server_process, + client_process, + local_server_handle: local_handle, + http_port, + host_header, + client, + } + } + + pub fn tunnel_request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder { + let url = format!("http://127.0.0.1:{}{}", self.http_port, path); + self.client + .request(method, &url) + .header("host", &self.host_header) + } + + pub fn server_url(&self, path: &str) -> String { + format!("http://127.0.0.1:{}{}", self.http_port, path) + } +} + +impl Drop for TestEnv { + fn drop(&mut self) { + let _ = self.server_process.kill(); + let _ = self.client_process.kill(); + self.local_server_handle.abort(); + let _ = self.server_process.wait(); + let _ = self.client_process.wait(); + } +} + +fn free_port() -> u16 { + TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .port() +} + +fn binary_path(name: &str) -> PathBuf { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let path = manifest_dir + .parent() + .unwrap() + .parent() + .unwrap() + .join("target") + .join("debug") + .join(name); + assert!( + path.exists(), + "binary not found at {}: run `cargo build -p funnel-server -p funnel-client` first", + path.display() + ); + path +} + +fn start_mock_server() -> (JoinHandle<()>, u16) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + listener.set_nonblocking(true).unwrap(); + + let handle = tokio::spawn(async move { + let listener = tokio::net::TcpListener::from_std(listener).unwrap(); + let app = mock_app(); + axum::serve(listener, app).await.unwrap(); + }); + + (handle, port) +} + +fn mock_app() -> Router { + Router::new() + .route("/hello", get(|| async { "hello from local service" })) + .route( + "/echo", + post(|body: String| async move { format!("echo: {body}") }), + ) + .route( + "/headers", + get(|headers: axum::http::HeaderMap| async move { + let map: std::collections::HashMap = headers + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + ( + [("x-custom-header", "test-value")], + axum::Json(map), + ) + }), + ) + .route( + "/large", + post(|body: axum::body::Bytes| async move { body }), + ) + .layer(DefaultBodyLimit::max(MOCK_BODY_LIMIT)) +} + +fn start_server_process(http_port: u16, quic_port: u16) -> Child { + Command::new(binary_path("funnel-server")) + .args([ + "--port", + &http_port.to_string(), + "--quic-port", + &quic_port.to_string(), + "--host", + "127.0.0.1", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("failed to start funnel-server") +} + +fn start_client_process( + local_port: u16, + server_http_port: u16, + quic_port: u16, + tunnel_id: &str, +) -> Child { + Command::new(binary_path("funnel-client")) + .args([ + "http", + &format!("127.0.0.1:{local_port}"), + "--server", + &format!("http://127.0.0.1:{server_http_port}"), + "--id", + tunnel_id, + "--quic-port", + &quic_port.to_string(), + "--insecure", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("failed to start funnel-client") +} + +async fn wait_for_tcp(port: u16) { + let deadline = tokio::time::Instant::now() + READY_TIMEOUT; + loop { + if tokio::time::Instant::now() > deadline { + panic!("tcp port {port} did not become ready"); + } + if tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")) + .await + .is_ok() + { + return; + } + tokio::time::sleep(POLL_INTERVAL).await; + } +} + +async fn wait_for_tunnel(client: &reqwest::Client, http_port: u16, host_header: &str) { + let url = format!("http://127.0.0.1:{http_port}/hello"); + let deadline = tokio::time::Instant::now() + READY_TIMEOUT; + loop { + if tokio::time::Instant::now() > deadline { + panic!("tunnel did not become ready"); + } + match client.get(&url).header("host", host_header).send().await { + Ok(resp) if resp.status().is_success() => return, + _ => tokio::time::sleep(POLL_INTERVAL).await, + } + } +} diff --git a/crates/funnel-e2e/tests/e2e/main.rs b/crates/funnel-e2e/tests/e2e/main.rs new file mode 100644 index 0000000..ce6dd07 --- /dev/null +++ b/crates/funnel-e2e/tests/e2e/main.rs @@ -0,0 +1,136 @@ +mod harness; + +use harness::TestEnv; +use reqwest::Method; + +#[tokio::test(flavor = "multi_thread")] +async fn e2e() { + let env = TestEnv::start().await; + + basic_get(&env).await; + post_echo(&env).await; + large_body(&env).await; + concurrent_requests(&env).await; + response_headers(&env).await; + metrics_endpoint(&env).await; + unknown_tunnel_404(&env).await; +} + +async fn basic_get(env: &TestEnv) { + let resp = env + .tunnel_request(Method::GET, "/hello") + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200, "basic_get: expected 200"); + assert_eq!( + resp.text().await.unwrap(), + "hello from local service", + "basic_get: unexpected body" + ); +} + +async fn post_echo(env: &TestEnv) { + let resp = env + .tunnel_request(Method::POST, "/echo") + .body("test body") + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200, "post_echo: expected 200"); + assert_eq!( + resp.text().await.unwrap(), + "echo: test body", + "post_echo: unexpected body" + ); +} + +async fn large_body(env: &TestEnv) { + let size = 2_500_000; + let payload = vec![b'x'; size]; + let resp = env + .tunnel_request(Method::POST, "/large") + .body(payload) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200, "large_body: expected 200"); + let body = resp.bytes().await.unwrap(); + assert_eq!(body.len(), size, "large_body: response size mismatch"); +} + +async fn concurrent_requests(env: &TestEnv) { + let mut set = tokio::task::JoinSet::new(); + + for _ in 0..10 { + let client = env.client.clone(); + let url = format!("http://127.0.0.1:{}/hello", env.http_port); + let host = env.host_header.clone(); + + set.spawn(async move { + client + .get(&url) + .header("host", &host) + .send() + .await + .unwrap() + }); + } + + let mut ok_count = 0; + while let Some(result) = set.join_next().await { + let resp = result.unwrap(); + assert_eq!(resp.status(), 200, "concurrent_requests: expected 200"); + ok_count += 1; + } + assert_eq!(ok_count, 10, "concurrent_requests: expected 10 responses"); +} + +async fn response_headers(env: &TestEnv) { + let resp = env + .tunnel_request(Method::GET, "/headers") + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200, "response_headers: expected 200"); + + let custom = resp.headers().get("x-custom-header"); + assert_eq!( + custom.and_then(|v| v.to_str().ok()), + Some("test-value"), + "response_headers: missing or wrong x-custom-header" + ); +} + +async fn metrics_endpoint(env: &TestEnv) { + let resp = env + .client + .get(&env.server_url("/api/metrics")) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200, "metrics_endpoint: expected 200"); + let body = resp.text().await.unwrap(); + assert!( + body.contains("funnel_requests_total"), + "metrics_endpoint: expected funnel_requests_total in prometheus output" + ); +} + +async fn unknown_tunnel_404(env: &TestEnv) { + let url = format!("http://127.0.0.1:{}/hello", env.http_port); + let resp = env + .client + .get(&url) + .header("host", format!("nonexistent.localhost:{}", env.http_port)) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 404, "unknown_tunnel_404: expected 404"); +} diff --git a/crates/funnel-server/Cargo.toml b/crates/funnel-server/Cargo.toml index f274d84..dc19524 100644 --- a/crates/funnel-server/Cargo.toml +++ b/crates/funnel-server/Cargo.toml @@ -8,19 +8,23 @@ funnel-core = { path = "../funnel-core" } anyhow = "1" argon2 = "0.5" -axum = { version = "0.8", features = ["ws"] } +axum = "0.8" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4", features = ["derive", "env"] } dashmap = "6" -futures = "0.3" +http-body-util = "0.1" jsonwebtoken = "9" +metrics = "0.24" +metrics-exporter-prometheus = "0.16" +quinn = "0.11" +rcgen = "0.13" serde = { version = "1", features = ["derive"] } serde_json = "1" ipnetwork = "0.20" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "migrate", "ipnetwork"] } thiserror = "2" tokio = { version = "1", features = ["full"] } -tokio-util = "0.7" +tokio-util = { version = "0.7", features = ["io"] } tower = "0.5" tower-http = { version = "0.6", features = ["cors", "trace"] } tracing = "0.1" diff --git a/crates/funnel-server/src/app.rs b/crates/funnel-server/src/app.rs index dc70298..f343b6e 100644 --- a/crates/funnel-server/src/app.rs +++ b/crates/funnel-server/src/app.rs @@ -2,15 +2,17 @@ use std::sync::Arc; use axum::Router; use axum::routing::get; +use metrics_exporter_prometheus::PrometheusHandle; use sqlx::PgPool; use tokio::time::Instant; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; use crate::api; -use crate::tunnel::manager::TunnelManager; -use crate::ws; +use crate::error::AppError; +use crate::metrics; use crate::proxy; +use crate::tunnel::manager::TunnelManager; pub struct AppState { pub db: Option, @@ -29,14 +31,14 @@ impl AppState { } } - pub fn require_db(&self) -> Result<&PgPool, crate::error::AppError> { + pub fn require_db(&self) -> Result<&PgPool, AppError> { self.db .as_ref() - .ok_or_else(|| crate::error::AppError::BadRequest("database not configured".into())) + .ok_or_else(|| AppError::BadRequest("database not configured".into())) } } -pub fn build_router(state: Arc) -> Router { +pub fn build_router(state: Arc, metrics_handle: PrometheusHandle) -> Router { let api_routes = Router::new() .route("/health", get(api::health::handler)) .route("/tunnels", get(api::tunnels::list)) @@ -45,11 +47,14 @@ pub fn build_router(state: Arc) -> Router { get(api::tunnels::get_tunnel).delete(api::tunnels::delete), ) .route("/keys", get(api::keys::list).post(api::keys::create)) - .route("/keys/{id}", axum::routing::delete(api::keys::revoke)); + .route("/keys/{id}", axum::routing::delete(api::keys::revoke)) + .route( + "/metrics", + get(metrics::handler).with_state(metrics_handle), + ); Router::new() .nest("/api", api_routes) - .route("/ws", get(ws::handler::upgrade)) .fallback(proxy::router::handle_tunnel_request) .with_state(state) .layer(TraceLayer::new_for_http()) diff --git a/crates/funnel-server/src/error.rs b/crates/funnel-server/src/error.rs index 88a056f..8b37bb4 100644 --- a/crates/funnel-server/src/error.rs +++ b/crates/funnel-server/src/error.rs @@ -1,7 +1,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; -use funnel_core::error::ValidationError; +use funnel_core::error::TunnelIdError; use serde::Serialize; #[derive(Debug, Serialize)] @@ -10,31 +10,18 @@ struct ApiErrorBody { } #[derive(Debug, thiserror::Error)] -#[allow(dead_code)] pub enum AppError { #[error("tunnel not found: {0}")] TunnelNotFound(String), - #[error("tunnel ID already in use: {0}")] - TunnelConflict(String), - - #[error("unauthorized: {0}")] - Unauthorized(String), - - #[error("forbidden: {0}")] - Forbidden(String), - #[error("not found: {0}")] NotFound(String), - #[error("conflict: {0}")] - Conflict(String), - #[error("bad request: {0}")] BadRequest(String), - #[error("validation error: {0}")] - Validation(#[from] ValidationError), + #[error("invalid tunnel id: {0}")] + InvalidTunnelId(#[from] TunnelIdError), #[error("database error")] Database(#[from] sqlx::Error), @@ -47,13 +34,9 @@ impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match &self { Self::TunnelNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()), - Self::TunnelConflict(_) => (StatusCode::CONFLICT, self.to_string()), - Self::Unauthorized(_) => (StatusCode::UNAUTHORIZED, self.to_string()), - Self::Forbidden(_) => (StatusCode::FORBIDDEN, self.to_string()), Self::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()), - Self::Conflict(_) => (StatusCode::CONFLICT, self.to_string()), Self::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()), - Self::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()), + Self::InvalidTunnelId(_) => (StatusCode::BAD_REQUEST, self.to_string()), Self::Database(e) => { tracing::error!(error = %e, "database error"); ( diff --git a/crates/funnel-server/src/main.rs b/crates/funnel-server/src/main.rs index 642162f..2713f80 100644 --- a/crates/funnel-server/src/main.rs +++ b/crates/funnel-server/src/main.rs @@ -3,14 +3,16 @@ mod app; mod auth; mod db; mod error; +mod metrics; mod proxy; +mod quic; mod tls; mod tunnel; -mod ws; use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; +use std::time::Duration; use axum::extract::Request; use axum::response::Redirect; @@ -19,6 +21,11 @@ use clap::Parser; use sqlx::postgres::PgPoolOptions; use tracing_subscriber::EnvFilter; +use funnel_core::protocol::QUIC_ALPN; + +const QUIC_KEEP_ALIVE: Duration = Duration::from_secs(15); +const QUIC_IDLE_TIMEOUT: Duration = Duration::from_secs(60); + #[derive(Parser)] #[command(name = "funnel-server", about = "Funnel tunnel server")] struct Cli { @@ -30,6 +37,10 @@ struct Cli { #[arg(long, default_value = "0.0.0.0")] host: String, + /// QUIC port for tunnel connections + #[arg(long, default_value_t = 4433)] + quic_port: u16, + /// PostgreSQL connection URL #[arg(long, env = "DATABASE_URL")] database_url: Option, @@ -65,6 +76,10 @@ struct Cli { #[tokio::main] async fn main() -> anyhow::Result<()> { + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install default crypto provider"); + tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), @@ -74,7 +89,7 @@ async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - tracing::info!(host = %cli.host, port = cli.port, "starting funnel server"); + tracing::info!(host = %cli.host, port = cli.port, quic_port = cli.quic_port, "starting funnel server"); let pool = if let Some(ref database_url) = cli.database_url { let pool = PgPoolOptions::new() @@ -91,7 +106,7 @@ async fn main() -> anyhow::Result<()> { tracing::info!("database migrations applied"); Some(pool) } else { - tracing::info!("no database configured, running in memory-only mode"); + tracing::info!("no database configured, running in memory only mode"); None }; @@ -103,15 +118,24 @@ async fn main() -> anyhow::Result<()> { } async fn run_plain(cli: Cli, pool: Option) -> anyhow::Result<()> { + let metrics_handle = metrics::setup(); let state = Arc::new(app::AppState::new(pool, false)); - let router = app::build_router(state); + let router = app::build_router(Arc::clone(&state), metrics_handle); + + let quic_handle = spawn_quic_listener(&cli.host, cli.quic_port, Arc::clone(&state))?; let addr: SocketAddr = format!("{}:{}", cli.host, cli.port).parse()?; let listener = tokio::net::TcpListener::bind(addr).await?; - tracing::info!(addr = %addr, "server listening"); + tracing::info!(addr = %addr, "http server listening"); - axum::serve(listener, router.into_make_service_with_connect_info::()).await?; + let http_server = + axum::serve(listener, router.into_make_service_with_connect_info::()); + + tokio::select! { + res = http_server => { res?; } + _ = quic_handle => {} + }; Ok(()) } @@ -130,8 +154,11 @@ async fn run_with_tls(cli: Cli, pool: Option) -> anyhow::Result<() tls::setup(Path::new(&cli.cert_dir), Path::new(config_path), email, cli.acme_staging) .await?; + let metrics_handle = metrics::setup(); let state = Arc::new(app::AppState::new(pool, true)); - let router = app::build_router(state); + let router = app::build_router(Arc::clone(&state), metrics_handle); + + let quic_handle = spawn_quic_listener(&cli.host, cli.quic_port, Arc::clone(&state))?; let tls_addr: SocketAddr = format!("{}:{}", cli.host, cli.tls_port).parse()?; let http_addr: SocketAddr = format!("{}:{}", cli.host, cli.port).parse()?; @@ -148,11 +175,82 @@ async fn run_with_tls(cli: Cli, pool: Option) -> anyhow::Result<() tokio::select! { res = https_server => { res?; } res = http_server => { res?; } + _ = quic_handle => {} }; Ok(()) } +fn spawn_quic_listener( + host: &str, + port: u16, + state: Arc, +) -> anyhow::Result> { + let server_config = build_self_signed_quic_config()?; + let endpoint = build_quic_endpoint(server_config, host, port)?; + + Ok(tokio::spawn(async move { + if let Err(e) = quic::listener::run(endpoint, state).await { + tracing::error!(error = %e, "quic listener failed"); + } + })) +} + +fn build_self_signed_quic_config() -> anyhow::Result { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()])?; + let cert_der = cert.cert.der().clone(); + let key_der = rustls::pki_types::PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); + + let mut server_crypto = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + vec![cert_der], + rustls::pki_types::PrivateKeyDer::Pkcs8(key_der), + )?; + + server_crypto.alpn_protocols = vec![QUIC_ALPN.to_vec()]; + + let mut server_config = quinn::ServerConfig::with_crypto(Arc::new( + quinn::crypto::rustls::QuicServerConfig::try_from(server_crypto)?, + )); + + let mut transport = quinn::TransportConfig::default(); + transport.keep_alive_interval(Some(QUIC_KEEP_ALIVE)); + transport.max_idle_timeout(Some( + quinn::IdleTimeout::try_from(QUIC_IDLE_TIMEOUT)?, + )); + + server_config.transport_config(Arc::new(transport)); + + Ok(server_config) +} + +/// build a quic endpoint, using a dual stack ipv6 socket when the host is a +/// wildcard address so that both ipv4 and ipv6 clients can connect. +fn build_quic_endpoint( + config: quinn::ServerConfig, + host: &str, + port: u16, +) -> anyhow::Result { + let addr: SocketAddr = format!("{host}:{port}").parse()?; + + // when binding to 0.0.0.0, also bind the ipv6 wildcard so dual stack works + if addr == SocketAddr::from(([0, 0, 0, 0], port)) { + let v6_addr: SocketAddr = format!("[::]:{port}").parse()?; + let socket = std::net::UdpSocket::bind(v6_addr)?; + let runtime = quinn::default_runtime() + .ok_or_else(|| anyhow::anyhow!("no async runtime available for quic endpoint"))?; + Ok(quinn::Endpoint::new( + quinn::EndpointConfig::default(), + Some(config), + socket, + runtime, + )?) + } else { + Ok(quinn::Endpoint::server(config, addr)?) + } +} + fn https_redirect_router(tls_port: u16) -> Router { Router::new().fallback(move |request: Request| async move { let host = request diff --git a/crates/funnel-server/src/metrics.rs b/crates/funnel-server/src/metrics.rs new file mode 100644 index 0000000..3ea125a --- /dev/null +++ b/crates/funnel-server/src/metrics.rs @@ -0,0 +1,21 @@ +use axum::http::StatusCode; +use axum::response::IntoResponse; +use metrics_exporter_prometheus::PrometheusHandle; + +pub fn setup() -> PrometheusHandle { + let builder = metrics_exporter_prometheus::PrometheusBuilder::new(); + builder + .install_recorder() + .expect("failed to install prometheus recorder") +} + +pub async fn handler( + axum::extract::State(handle): axum::extract::State, +) -> impl IntoResponse { + let body = handle.render(); + ( + StatusCode::OK, + [("content-type", "text/plain; version=0.0.4; charset=utf-8")], + body, + ) +} diff --git a/crates/funnel-server/src/proxy/router.rs b/crates/funnel-server/src/proxy/router.rs index 3fce6fc..ea1f867 100644 --- a/crates/funnel-server/src/proxy/router.rs +++ b/crates/funnel-server/src/proxy/router.rs @@ -3,15 +3,15 @@ use std::sync::Arc; use axum::body::Body; use axum::extract::State; use axum::http::{Request, Response, StatusCode}; +use tokio_util::io::ReaderStream; -use funnel_core::protocol::RequestPayload; +use funnel_core::protocol::{RequestMeta, ResponseMeta}; use funnel_core::tunnel::TunnelId; use crate::app::AppState; +use crate::tunnel::connection::{CountedRecvStream, SendError}; use super::headers::prepare_forwarding_headers; -const MAX_BODY_SIZE: usize = 10 * 1024 * 1024; // 10 mb - /// axum fallback handler that routes requests based on subdomain. /// requests to `{tunnel_id}.{base_domain}` are forwarded through the matching tunnel. pub async fn handle_tunnel_request( @@ -42,40 +42,35 @@ pub async fn handle_tunnel_request( let method = request.method().to_string(); let path = request.uri().to_string(); + let fallback_addr = std::net::SocketAddr::from(([0, 0, 0, 0], 0)); let remote_addr = request .extensions() .get::>() - .map(|ci| ci.0); - - let headers = match remote_addr { - Some(addr) => prepare_forwarding_headers(request.headers(), host, addr, state.is_tls), - None => prepare_forwarding_headers( - request.headers(), - host, - std::net::SocketAddr::from(([0, 0, 0, 0], 0)), - state.is_tls, - ), - }; + .map(|ci| ci.0) + .unwrap_or(fallback_addr); - let body_bytes = match axum::body::to_bytes(request.into_body(), MAX_BODY_SIZE).await { - Ok(b) => b.to_vec(), - Err(_) => return error_response(StatusCode::BAD_REQUEST, "request body too large"), - }; + let headers = prepare_forwarding_headers(request.headers(), host, remote_addr, state.is_tls); - let payload = RequestPayload { + let meta = RequestMeta { method, path, headers, - body: body_bytes, }; - match tunnel.send_request(payload).await { - Ok(response) => build_response(response), - Err(crate::tunnel::connection::SendError::Timeout) => { + let body = request.into_body(); + + match tunnel.send_request(meta, body).await { + Ok((resp_meta, recv_stream)) => build_response(resp_meta, recv_stream), + Err(SendError::Timeout) => { error_response(StatusCode::GATEWAY_TIMEOUT, "request timed out") } - Err(crate::tunnel::connection::SendError::TunnelClosed) => { - error_response(StatusCode::BAD_GATEWAY, "tunnel connection lost") + Err(SendError::ReadBody(e)) => { + tracing::debug!(error = %e, "failed to read request body"); + error_response(StatusCode::BAD_REQUEST, "failed to read request body") + } + Err(e) => { + tracing::debug!(error = %e, "tunnel request failed"); + error_response(StatusCode::BAD_GATEWAY, "tunnel error") } } } @@ -91,18 +86,18 @@ fn extract_subdomain(host: &str) -> Option<&str> { let subdomain = &host[..dot]; let rest = &host[dot + 1..]; - if rest.contains('.') { - Some(subdomain) - } else { + if rest.is_empty() { None + } else { + Some(subdomain) } } -fn build_response(payload: funnel_core::protocol::ResponsePayload) -> Response { - let mut builder = Response::builder().status(payload.status); +fn build_response(meta: ResponseMeta, recv: CountedRecvStream) -> Response { + let mut builder = Response::builder().status(meta.status); if let Some(headers) = builder.headers_mut() { - for (name, values) in &payload.headers { + for (name, values) in &meta.headers { for value in values { if let (Ok(name), Ok(value)) = ( axum::http::HeaderName::try_from(name.as_str()), @@ -114,8 +109,11 @@ fn build_response(payload: funnel_core::protocol::ResponsePayload) -> Response Response { .status(status) .header("content-type", "text/plain") .body(Body::from(msg.to_string())) - .unwrap() + .expect("hardcoded error response must be valid") } #[cfg(test)] @@ -146,9 +144,14 @@ mod tests { assert_eq!(extract_subdomain("my-tunnel.example.com:8080"), Some("my-tunnel")); } + #[test] + fn extract_subdomain_single_label() { + assert_eq!(extract_subdomain("abc.localhost"), Some("abc")); + assert_eq!(extract_subdomain("abc.localhost:8080"), Some("abc")); + } + #[test] fn extract_subdomain_no_subdomain() { - assert_eq!(extract_subdomain("example.com"), None); assert_eq!(extract_subdomain("localhost"), None); assert_eq!(extract_subdomain("localhost:8080"), None); } diff --git a/crates/funnel-server/src/quic/handler.rs b/crates/funnel-server/src/quic/handler.rs new file mode 100644 index 0000000..7a8572c --- /dev/null +++ b/crates/funnel-server/src/quic/handler.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use funnel_core::protocol::{self, Handshake, HandshakeResponse}; +use crate::app::AppState; +use crate::tunnel::connection::ActiveTunnel; + +#[derive(Debug, thiserror::Error)] +pub enum ConnectionError { + #[error("connection error: {0}")] + Connection(#[from] quinn::ConnectionError), + + #[error("frame error: {0}")] + Frame(#[from] protocol::FrameError), + + #[error("tunnel id conflict: {0}")] + Conflict(String), +} + +pub async fn handle_connection( + conn: quinn::Connection, + state: Arc, +) -> Result<(), ConnectionError> { + let (mut send, mut recv) = conn + .accept_bi() + .await + .map_err(ConnectionError::Connection)?; + + let handshake: Handshake = protocol::read_meta(&mut recv).await?; + let tunnel_id = handshake.tunnel_id; + + let tunnel = Arc::new(ActiveTunnel::new(tunnel_id.clone(), conn.clone())); + + if state + .tunnels + .insert(tunnel_id.clone(), Arc::clone(&tunnel)) + .is_err() + { + let resp = HandshakeResponse::rejected(format!("tunnel id already in use: {tunnel_id}")); + let _ = protocol::write_meta(&mut send, &resp).await; + return Err(ConnectionError::Conflict(tunnel_id.to_string())); + } + + protocol::write_meta(&mut send, &HandshakeResponse::ok()).await?; + + tracing::info!(tunnel_id = %tunnel_id, "tunnel connected via quic"); + + metrics::gauge!("funnel_tunnels_active").increment(1.0); + metrics::counter!("funnel_tunnels_total").increment(1); + + // wait until the connection closes (control stream EOF or transport error) + let mut buf = [0u8; 1]; + tokio::select! { + _ = recv.read(&mut buf) => {}, + _ = conn.closed() => {}, + } + + let stats = tunnel.stats(); + state.tunnels.remove(&tunnel_id); + + let session_secs = tunnel.connected_at().elapsed().as_secs_f64(); + + metrics::gauge!("funnel_tunnels_active").decrement(1.0); + metrics::histogram!("funnel_tunnel_session_duration_seconds").record(session_secs); + + tracing::info!( + tunnel_id = %tunnel_id, + bytes_in = stats.bytes_in, + bytes_out = stats.bytes_out, + requests = stats.requests, + session_secs = session_secs, + remaining_tunnels = state.tunnels.count(), + "tunnel disconnected" + ); + + Ok(()) +} diff --git a/crates/funnel-server/src/quic/listener.rs b/crates/funnel-server/src/quic/listener.rs new file mode 100644 index 0000000..7caf56e --- /dev/null +++ b/crates/funnel-server/src/quic/listener.rs @@ -0,0 +1,30 @@ +use std::sync::Arc; + +use crate::app::AppState; + +use super::handler; + +pub async fn run(endpoint: quinn::Endpoint, state: Arc) -> anyhow::Result<()> { + tracing::info!( + addr = %endpoint.local_addr()?, + "quic endpoint listening" + ); + + while let Some(incoming) = endpoint.accept().await { + let state = Arc::clone(&state); + tokio::spawn(async move { + match incoming.await { + Ok(conn) => { + if let Err(e) = handler::handle_connection(conn, state).await { + tracing::debug!(error = %e, "connection ended"); + } + } + Err(e) => { + tracing::debug!(error = %e, "failed to accept connection"); + } + } + }); + } + + Ok(()) +} diff --git a/crates/funnel-server/src/quic/mod.rs b/crates/funnel-server/src/quic/mod.rs new file mode 100644 index 0000000..dae6833 --- /dev/null +++ b/crates/funnel-server/src/quic/mod.rs @@ -0,0 +1,2 @@ +pub mod handler; +pub mod listener; diff --git a/crates/funnel-server/src/tls/manager.rs b/crates/funnel-server/src/tls/manager.rs index 89be9c7..345fb54 100644 --- a/crates/funnel-server/src/tls/manager.rs +++ b/crates/funnel-server/src/tls/manager.rs @@ -15,7 +15,7 @@ use x509_parser::prelude::*; use super::provider::ProviderMux; -const LRU_CACHE_SIZE: usize = 512; +const LRU_CACHE_SIZE: std::num::NonZeroUsize = std::num::NonZeroUsize::new(512).unwrap(); const RENEWAL_CHECK_INTERVAL: Duration = Duration::from_secs(3600); const RENEWAL_WINDOW: Duration = Duration::from_secs(30 * 24 * 3600); const ACME_POLL_TIMEOUT: Duration = Duration::from_secs(300); @@ -54,9 +54,7 @@ impl CertificateManager { .context("failed to create certificate directory")?; let account = load_or_create_account(&cert_dir, email, staging).await?; - let cache = Mutex::new(LruCache::new( - std::num::NonZeroUsize::new(LRU_CACHE_SIZE).unwrap(), - )); + let cache = Mutex::new(LruCache::new(LRU_CACHE_SIZE)); Ok(Self { cache, @@ -72,7 +70,7 @@ impl CertificateManager { match self.load_from_disk(&domain).await { Ok(Some((certified_key, not_after))) => { tracing::info!(domain = %domain, "loaded certificate from disk"); - let mut cache = self.cache.lock().unwrap(); + let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner()); cache.put( domain, CachedCert { @@ -103,7 +101,7 @@ impl CertificateManager { // double check cache after acquiring lock { - let cache = self.cache.lock().unwrap(); + let cache = self.cache.lock().unwrap_or_else(|e| e.into_inner()); if let Some(cached) = cache.peek(domain) { if cached.not_after > SystemTime::now() + RENEWAL_WINDOW { return Ok(()); @@ -201,7 +199,7 @@ impl CertificateManager { let not_after = parse_cert_expiry(&cert_pem)?; { - let mut cache = self.cache.lock().unwrap(); + let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner()); cache.put( domain.to_string(), CachedCert { @@ -257,7 +255,7 @@ impl CertificateManager { async fn check_renewals(&self) { let domains_to_renew: Vec = { - let cache = self.cache.lock().unwrap(); + let cache = self.cache.lock().unwrap_or_else(|e| e.into_inner()); cache .iter() .filter(|(_, cert)| { @@ -285,7 +283,7 @@ impl ResolvesServerCert for CertificateManager { client_hello: rustls::server::ClientHello<'_>, ) -> Option> { let sni = client_hello.server_name()?; - let cache = self.cache.lock().unwrap(); + let cache = self.cache.lock().unwrap_or_else(|e| e.into_inner()); let cached = cache.peek(sni)?; if cached.not_after > SystemTime::now() { @@ -306,7 +304,8 @@ fn load_certified_key(cert_pem: &str, key_pem: &str) -> Result { .context("no private key found")?; let signing_key = rustls::crypto::ring::sign::any_supported_type(&key) - .map_err(|e| anyhow::anyhow!("unsupported private key type: {e}"))?; + .map_err(|e| anyhow::anyhow!(e)) + .context("unsupported private key type")?; Ok(CertifiedKey::new(certs, signing_key)) } @@ -316,7 +315,8 @@ fn parse_cert_expiry(cert_pem: &str) -> Result { .collect::, _>>()?; let cert = certs.first().context("no certificate found")?; let (_, parsed) = X509Certificate::from_der(cert.as_ref()) - .map_err(|e| anyhow::anyhow!("failed to parse x509 certificate: {e}"))?; + .map_err(|e| anyhow::anyhow!(e.to_string())) + .context("failed to parse x509 certificate")?; let not_after = parsed.validity().not_after; Ok(not_after.to_datetime().into()) diff --git a/crates/funnel-server/src/tls/mod.rs b/crates/funnel-server/src/tls/mod.rs index 0aad9a8..154bd5f 100644 --- a/crates/funnel-server/src/tls/mod.rs +++ b/crates/funnel-server/src/tls/mod.rs @@ -13,10 +13,6 @@ pub async fn setup( email: &str, acme_staging: bool, ) -> Result { - rustls::crypto::ring::default_provider() - .install_default() - .expect("failed to install default crypto provider"); - let provider_config = config::ProviderConfig::load(providers_config_path).await?; let provider_mux = provider::ProviderMux::from_config(&provider_config).await?; diff --git a/crates/funnel-server/src/tunnel/connection.rs b/crates/funnel-server/src/tunnel/connection.rs index c9f4f15..e8fe513 100644 --- a/crates/funnel-server/src/tunnel/connection.rs +++ b/crates/funnel-server/src/tunnel/connection.rs @@ -1,31 +1,35 @@ +use std::pin::Pin; use std::sync::Arc; +use std::task::{Context, Poll}; use std::time::Duration; -use axum::extract::ws::{Message, WebSocket}; -use dashmap::DashMap; -use futures::{SinkExt, StreamExt}; -use tokio::sync::{mpsc, oneshot}; -use tokio_util::sync::CancellationToken; -use uuid::Uuid; +use http_body_util::BodyExt; +use tokio::io::{AsyncRead, ReadBuf}; -use funnel_core::protocol::{ResponsePayload, TunnelMessage}; +use funnel_core::protocol::{self, FrameError, RequestMeta, ResponseMeta}; use funnel_core::tunnel::TunnelId; use super::stats::TunnelStats; -const CHANNEL_BUFFER: usize = 128; const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); pub struct ActiveTunnel { id: TunnelId, - outgoing_tx: mpsc::Sender, - pending_requests: DashMap>, - stats: TunnelStats, + conn: quinn::Connection, + stats: Arc, connected_at: tokio::time::Instant, - cancel: CancellationToken, } impl ActiveTunnel { + pub fn new(id: TunnelId, conn: quinn::Connection) -> Self { + Self { + id, + conn, + stats: Arc::new(TunnelStats::new()), + connected_at: tokio::time::Instant::now(), + } + } + pub fn id(&self) -> &TunnelId { &self.id } @@ -38,191 +42,158 @@ impl ActiveTunnel { self.connected_at } - #[allow(dead_code)] - pub fn is_alive(&self) -> bool { - !self.cancel.is_cancelled() - } - - /// wait until the tunnel connection has been closed. - pub async fn cancelled(&self) { - self.cancel.cancelled().await; - } - - /// send a request through the tunnel and wait for the client's response. + /// open a quic bidirectional stream, send the request, and return the + /// response metadata along with a counted recv stream for body streaming. pub async fn send_request( &self, - payload: funnel_core::protocol::RequestPayload, - ) -> Result { - let request_id = Uuid::now_v7(); - let (tx, rx) = oneshot::channel(); - - self.pending_requests.insert(request_id, tx); + meta: RequestMeta, + body: axum::body::Body, + ) -> Result<(ResponseMeta, CountedRecvStream), SendError> { self.stats.inc_requests(); - let msg = TunnelMessage::Request { - tunnel_id: self.id.clone(), - request_id, - payload, - }; + let start = std::time::Instant::now(); + + let result = tokio::time::timeout(REQUEST_TIMEOUT, async { + let (mut send, mut recv) = self + .conn + .open_bi() + .await + .map_err(SendError::OpenStream)?; + + protocol::write_meta(&mut send, &meta) + .await + .map_err(SendError::SendMeta)?; + + let mut body = body; + let mut bytes_sent: u64 = 0; + while let Some(frame) = body.frame().await { + let frame = frame.map_err(SendError::ReadBody)?; + if let Ok(data) = frame.into_data() { + bytes_sent += data.len() as u64; + send.write_all(&data) + .await + .map_err(SendError::WriteBody)?; + } + } - if self.outgoing_tx.send(msg).await.is_err() { - self.pending_requests.remove(&request_id); - return Err(SendError::TunnelClosed); - } + send.finish().map_err(SendError::FinishStream)?; - let result = tokio::time::timeout(REQUEST_TIMEOUT, rx).await; + self.stats.add_bytes_out(bytes_sent); + metrics::counter!("funnel_bytes_out_total").increment(bytes_sent); + metrics::histogram!("funnel_request_body_bytes").record(bytes_sent as f64); - // clean up regardless of outcome - self.pending_requests.remove(&request_id); + let resp_meta: ResponseMeta = protocol::read_meta(&mut recv) + .await + .map_err(SendError::ReadResponse)?; + + let counted_recv = CountedRecvStream::new(recv, Arc::clone(&self.stats)); + Ok::<_, SendError>((resp_meta, counted_recv)) + }) + .await; + + let duration = start.elapsed().as_secs_f64(); + metrics::histogram!("funnel_request_duration_seconds").record(duration); match result { - Ok(Ok(response)) => Ok(response), - Ok(Err(_)) => Err(SendError::TunnelClosed), + Ok(Ok(response)) => { + metrics::counter!("funnel_requests_total", "outcome" => "success").increment(1); + Ok(response) + } + Ok(Err(e)) => { + metrics::counter!("funnel_requests_total", "outcome" => e.outcome_label()).increment(1); + Err(e) + } Err(_) => { - // send cancel so the client can stop working on this request - let _ = self.outgoing_tx.send(TunnelMessage::RequestCancel { request_id }).await; + metrics::counter!("funnel_requests_total", "outcome" => "timeout").increment(1); Err(SendError::Timeout) } } } - /// shut down the tunnel connection. pub fn close(&self) { - self.cancel.cancel(); + self.conn + .close(quinn::VarInt::from_u32(0), b"tunnel closed"); } } #[derive(Debug, thiserror::Error)] pub enum SendError { - #[error("tunnel connection closed")] - TunnelClosed, + #[error("failed to open tunnel stream")] + OpenStream(#[source] quinn::ConnectionError), + + #[error("failed to send request metadata")] + SendMeta(#[source] FrameError), + + #[error("failed to read request body")] + ReadBody(#[source] axum::Error), + + #[error("failed to write body to tunnel")] + WriteBody(#[source] quinn::WriteError), + + #[error("failed to finish send stream")] + FinishStream(#[source] quinn::ClosedStream), + + #[error("failed to read response metadata")] + ReadResponse(#[source] FrameError), + #[error("request timed out")] Timeout, } -/// spawn the read/write tasks for a websocket connection and return the active tunnel. -/// the returned tunnel is live immediately; the background tasks run until the -/// connection drops or `close()` is called. -pub fn spawn(id: TunnelId, ws: WebSocket) -> Arc { - let (ws_sink, ws_stream) = ws.split(); - let (outgoing_tx, outgoing_rx) = mpsc::channel(CHANNEL_BUFFER); - let cancel = CancellationToken::new(); - - let tunnel = Arc::new(ActiveTunnel { - id: id.clone(), - outgoing_tx, - pending_requests: DashMap::new(), - stats: TunnelStats::new(), - connected_at: tokio::time::Instant::now(), - cancel: cancel.clone(), - }); - - let read_tunnel = Arc::clone(&tunnel); - let read_cancel = cancel.clone(); - tokio::spawn(async move { - read_loop(ws_stream, &read_tunnel, read_cancel).await; - }); - - let write_cancel = cancel.clone(); - tokio::spawn(async move { - write_loop(ws_sink, outgoing_rx, write_cancel).await; - }); - - tunnel -} - -async fn read_loop( - mut stream: futures::stream::SplitStream, - tunnel: &ActiveTunnel, - cancel: CancellationToken, -) { - loop { - let msg = tokio::select! { - msg = stream.next() => msg, - _ = cancel.cancelled() => break, - }; - - let Some(result) = msg else { break }; - - let frame = match result { - Ok(frame) => frame, - Err(e) => { - tracing::debug!(tunnel_id = %tunnel.id, error = %e, "websocket read error"); - break; - } - }; - - match frame { - Message::Text(text) => { - tunnel.stats.add_bytes_in(text.len() as u64); - handle_incoming_text(tunnel, &text); - } - Message::Close(_) => break, - _ => {} +impl SendError { + pub fn outcome_label(&self) -> &'static str { + match self { + Self::OpenStream(_) => "stream_open_failed", + Self::SendMeta(_) => "send_meta_failed", + Self::ReadBody(_) => "read_body_failed", + Self::WriteBody(_) => "write_body_failed", + Self::FinishStream(_) => "finish_stream_failed", + Self::ReadResponse(_) => "read_response_failed", + Self::Timeout => "timeout", } } - tracing::debug!(tunnel_id = %tunnel.id, "read loop exiting"); - cancel.cancel(); } -fn handle_incoming_text(tunnel: &ActiveTunnel, text: &str) { - let msg: TunnelMessage = match serde_json::from_str(text) { - Ok(m) => m, - Err(e) => { - tracing::warn!(tunnel_id = %tunnel.id, error = %e, "malformed message from client"); - return; - } - }; - - match msg { - TunnelMessage::Response { - request_id, - payload, - } => { - if let Some((_, tx)) = tunnel.pending_requests.remove(&request_id) { - let _ = tx.send(payload); - } - } - TunnelMessage::Pong => {} - other => { - tracing::debug!( - tunnel_id = %tunnel.id, - msg_type = ?other, - "unexpected message type from client" - ); +/// wraps a quinn::RecvStream and tracks bytes read for metrics and per tunnel stats. +/// on drop, records funnel_bytes_in_total and funnel_response_body_bytes. +pub struct CountedRecvStream { + inner: quinn::RecvStream, + bytes_read: u64, + stats: Arc, +} + +impl CountedRecvStream { + fn new(inner: quinn::RecvStream, stats: Arc) -> Self { + Self { + inner, + bytes_read: 0, + stats, } } } -async fn write_loop( - mut sink: futures::stream::SplitSink, - mut rx: mpsc::Receiver, - cancel: CancellationToken, -) { - loop { - let msg = tokio::select! { - msg = rx.recv() => msg, - _ = cancel.cancelled() => break, - }; - - let Some(msg) = msg else { break }; - - let json = match serde_json::to_string(&msg) { - Ok(j) => j, - Err(e) => { - tracing::error!(error = %e, "failed to serialize tunnel message"); - continue; - } - }; - - if sink.send(Message::text(json)).await.is_err() { - break; - } +impl AsyncRead for CountedRecvStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + let before = buf.filled().len(); + let result = Pin::new(&mut this.inner).poll_read(cx, buf); + let after = buf.filled().len(); + this.bytes_read += (after - before) as u64; + result } +} - tracing::debug!("write loop exiting"); - cancel.cancel(); +impl Drop for CountedRecvStream { + fn drop(&mut self) { + self.stats.add_bytes_in(self.bytes_read); + metrics::counter!("funnel_bytes_in_total").increment(self.bytes_read); + metrics::histogram!("funnel_response_body_bytes").record(self.bytes_read as f64); + } } #[cfg(test)] @@ -231,7 +202,12 @@ mod tests { #[test] fn send_error_display() { - assert_eq!(SendError::TunnelClosed.to_string(), "tunnel connection closed"); assert_eq!(SendError::Timeout.to_string(), "request timed out"); } + + #[test] + fn send_error_outcome_labels() { + assert_eq!(SendError::Timeout.outcome_label(), "timeout"); + } + } diff --git a/crates/funnel-server/src/tunnel/manager.rs b/crates/funnel-server/src/tunnel/manager.rs index 5de137e..803c4c7 100644 --- a/crates/funnel-server/src/tunnel/manager.rs +++ b/crates/funnel-server/src/tunnel/manager.rs @@ -45,10 +45,6 @@ impl TunnelManager { self.tunnels.get(id).map(|r| Arc::clone(r.value())) } - pub fn exists(&self, id: &TunnelId) -> bool { - self.tunnels.contains_key(id) - } - pub fn list(&self) -> Vec { self.tunnels .iter() @@ -79,13 +75,6 @@ mod tests { assert!(mgr.list().is_empty()); } - #[test] - fn exists_returns_false_for_missing() { - let mgr = TunnelManager::new(); - let id = TunnelId::new("test-abc").unwrap(); - assert!(!mgr.exists(&id)); - } - #[test] fn get_returns_none_for_missing() { let mgr = TunnelManager::new(); diff --git a/crates/funnel-server/src/tunnel/stats.rs b/crates/funnel-server/src/tunnel/stats.rs index b9a26a7..d90738f 100644 --- a/crates/funnel-server/src/tunnel/stats.rs +++ b/crates/funnel-server/src/tunnel/stats.rs @@ -28,7 +28,6 @@ impl TunnelStats { self.bytes_in.fetch_add(n, Ordering::Relaxed); } - #[allow(dead_code)] pub fn add_bytes_out(&self, n: u64) { self.bytes_out.fetch_add(n, Ordering::Relaxed); } diff --git a/crates/funnel-server/src/ws/handler.rs b/crates/funnel-server/src/ws/handler.rs deleted file mode 100644 index 13f2f54..0000000 --- a/crates/funnel-server/src/ws/handler.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::sync::Arc; - -use axum::extract::ws::WebSocket; -use axum::extract::{Query, State, WebSocketUpgrade}; -use axum::response::Response; -use serde::Deserialize; - -use funnel_core::tunnel::TunnelId; - -use crate::app::AppState; -use crate::error::AppError; -use crate::tunnel::connection; - -#[derive(Deserialize)] -pub struct ConnectParams { - id: Option, -} - -pub async fn upgrade( - State(state): State>, - Query(params): Query, - ws: WebSocketUpgrade, -) -> Result { - let tunnel_id = match params.id { - Some(raw) => TunnelId::new(raw)?, - None => TunnelId::generate(), - }; - - if state.tunnels.exists(&tunnel_id) { - return Err(AppError::TunnelConflict(tunnel_id.to_string())); - } - - tracing::info!(tunnel_id = %tunnel_id, "websocket upgrade accepted"); - - Ok(ws.on_upgrade(move |socket| handle_socket(state, tunnel_id, socket))) -} - -async fn handle_socket(state: Arc, id: TunnelId, socket: WebSocket) { - let tunnel = connection::spawn(id.clone(), socket); - - if let Err(_returned) = state.tunnels.insert(id.clone(), Arc::clone(&tunnel)) { - tracing::warn!(tunnel_id = %id, "tunnel id became occupied during upgrade"); - tunnel.close(); - return; - } - - tracing::info!( - tunnel_id = %id, - total_tunnels = state.tunnels.count(), - "tunnel connected" - ); - - // wait until the tunnel connection drops - tunnel.cancelled().await; - - let stats = tunnel.stats(); - state.tunnels.remove(&id); - - tracing::info!( - tunnel_id = %id, - bytes_in = stats.bytes_in, - bytes_out = stats.bytes_out, - requests = stats.requests, - uptime_secs = tunnel.connected_at().elapsed().as_secs_f64(), - remaining_tunnels = state.tunnels.count(), - "tunnel disconnected" - ); -} diff --git a/crates/funnel-server/src/ws/mod.rs b/crates/funnel-server/src/ws/mod.rs deleted file mode 100644 index 062ae9d..0000000 --- a/crates/funnel-server/src/ws/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod handler; diff --git a/scripts/e2e.sh b/scripts/e2e.sh new file mode 100755 index 0000000..40e293e --- /dev/null +++ b/scripts/e2e.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cargo build -p funnel-server -p funnel-client +cargo test -p funnel-e2e -- --nocapture "$@" From 0a6d27c27f29b6082808146758753494a2b5c5d7 Mon Sep 17 00:00:00 2001 From: Karol Broda Date: Sun, 3 May 2026 22:48:17 +0200 Subject: [PATCH 18/18] refactor: use workspace deps, remove unused crates, and clean up core types --- Cargo.lock | 131 +++--------------- Cargo.toml | 42 ++++++ crates/funnel-client/Cargo.toml | 26 ++-- crates/funnel-client/src/forwarder.rs | 2 +- crates/funnel-client/src/main.rs | 2 +- crates/funnel-client/src/runner.rs | 2 +- crates/funnel-client/src/tunnel.rs | 15 +- crates/funnel-core/Cargo.toml | 15 +- crates/funnel-core/src/auth/mod.rs | 4 +- crates/funnel-core/src/error.rs | 19 --- crates/funnel-core/src/lib.rs | 1 - crates/funnel-core/src/protocol/handshake.rs | 30 ++-- crates/funnel-core/src/protocol/mod.rs | 4 - crates/funnel-core/src/tunnel/id.rs | 30 +++- crates/funnel-core/src/tunnel/mod.rs | 4 +- crates/funnel-e2e/Cargo.toml | 8 +- crates/funnel-server/Cargo.toml | 47 +++---- crates/funnel-server/src/api/tunnels.rs | 2 +- crates/funnel-server/src/db/api_keys.rs | 2 +- crates/funnel-server/src/error.rs | 2 +- crates/funnel-server/src/proxy/router.rs | 4 +- crates/funnel-server/src/quic/handler.rs | 11 +- crates/funnel-server/src/tls/config.rs | 10 +- crates/funnel-server/src/tls/provider/mod.rs | 11 +- crates/funnel-server/src/tunnel/connection.rs | 9 +- crates/funnel-server/src/tunnel/manager.rs | 2 +- crates/funnel-web/Cargo.toml | 6 +- 27 files changed, 192 insertions(+), 249 deletions(-) delete mode 100644 crates/funnel-core/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 57b6302..af833aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,18 +102,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures 0.2.17", - "password-hash", -] - [[package]] name = "asn1-rs" version = "0.7.1" @@ -834,15 +822,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -1529,7 +1508,7 @@ dependencies = [ "funnel-core", "http 1.4.0", "quinn", - "reqwest 0.12.28", + "reqwest", "rustls 0.23.40", "serde", "tokio", @@ -1546,13 +1525,10 @@ name = "funnel-core" version = "0.1.0" dependencies = [ "base64", - "bytes", "getrandom 0.3.4", "hex", - "http 1.4.0", "nanoid", "pretty_assertions", - "regex", "rmp-serde", "serde", "serde_json", @@ -1566,7 +1542,7 @@ name = "funnel-e2e" version = "0.1.0" dependencies = [ "axum 0.8.9", - "reqwest 0.12.28", + "reqwest", "tokio", ] @@ -1575,7 +1551,6 @@ name = "funnel-server" version = "0.1.0" dependencies = [ "anyhow", - "argon2", "aws-config", "aws-sdk-route53", "axum 0.8.9", @@ -1588,14 +1563,13 @@ dependencies = [ "http-body-util", "instant-acme", "ipnetwork", - "jsonwebtoken", "lru", "metrics", "metrics-exporter-prometheus", "pretty_assertions", "quinn", "rcgen 0.13.2", - "reqwest 0.13.3", + "reqwest", "rustls 0.23.40", "rustls-pemfile", "serde", @@ -1604,7 +1578,6 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-util", - "tower", "tower-http", "tracing", "tracing-subscriber", @@ -2114,7 +2087,6 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.7", ] [[package]] @@ -2459,21 +2431,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - [[package]] name = "konst" version = "0.2.20" @@ -3023,17 +2980,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -3583,47 +3529,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-core", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.9.0", - "hyper-rustls 0.27.9", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls 0.23.40", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls 0.26.4", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots 1.0.7", -] - [[package]] name = "reqwest" version = "0.13.3" @@ -3633,6 +3538,7 @@ dependencies = [ "base64", "bytes", "futures-core", + "futures-util", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -3653,12 +3559,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls 0.26.4", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams 0.5.0", "web-sys", ] @@ -4112,7 +4020,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", "web-sys", "xxhash-rust", ] @@ -4225,18 +4133,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - [[package]] name = "siphasher" version = "1.0.2" @@ -5298,6 +5194,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" diff --git a/Cargo.toml b/Cargo.toml index 6a93871..1b60801 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,45 @@ members = [ edition = "2024" license = "MIT" repository = "https://github.com/karol-broda/funnel" + +[workspace.dependencies] +# internal +funnel-core = { path = "crates/funnel-core" } + +# serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rmp-serde = "1" + +# async +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["io"] } + +# error handling +thiserror = "2" +anyhow = "1" + +# networking +quinn = "0.11" +rustls = { version = "0.23", default-features = false } +reqwest = { version = "0.13", default-features = false } + +# http +axum = "0.8" +http = "1" +tower-http = { version = "0.6", features = ["cors", "trace"] } + +# observability +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# cli +clap = { version = "4", features = ["derive", "env"] } + +# database +sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "migrate", "ipnetwork"] } +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["v7", "serde"] } + +# misc +pretty_assertions = "1" diff --git a/crates/funnel-client/Cargo.toml b/crates/funnel-client/Cargo.toml index 8c5b1dd..b361f9d 100644 --- a/crates/funnel-client/Cargo.toml +++ b/crates/funnel-client/Cargo.toml @@ -1,23 +1,23 @@ [package] name = "funnel-client" version = "0.1.0" -edition = "2024" +edition.workspace = true [dependencies] -funnel-core = { path = "../funnel-core" } +funnel-core.workspace = true -anyhow = "1" -clap = { version = "4", features = ["derive"] } +anyhow.workspace = true +clap = { workspace = true, features = ["derive"] } dirs = "6" -http = "1" -quinn = "0.11" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] } -rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } -serde = { version = "1", features = ["derive"] } -tokio = { version = "1", features = ["full"] } -tokio-util = { version = "0.7", features = ["io"] } +http.workspace = true +quinn.workspace = true +reqwest = { workspace = true, features = ["rustls", "stream"] } +rustls = { workspace = true, default-features = false, features = ["ring", "std"] } +serde.workspace = true +tokio.workspace = true +tokio-util.workspace = true toml = "0.8" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing.workspace = true +tracing-subscriber.workspace = true url = "2" webpki-roots = "1" diff --git a/crates/funnel-client/src/forwarder.rs b/crates/funnel-client/src/forwarder.rs index 552008e..0b00414 100644 --- a/crates/funnel-client/src/forwarder.rs +++ b/crates/funnel-client/src/forwarder.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use funnel_core::protocol::{RequestMeta, ResponseMeta}; +use funnel_core::protocol::request::{RequestMeta, ResponseMeta}; const HOP_BY_HOP_HEADERS: &[&str] = &[ "connection", diff --git a/crates/funnel-client/src/main.rs b/crates/funnel-client/src/main.rs index 11ced8b..5ce49da 100644 --- a/crates/funnel-client/src/main.rs +++ b/crates/funnel-client/src/main.rs @@ -7,7 +7,7 @@ use clap::{Parser, Subcommand}; use tokio_util::sync::CancellationToken; use tracing_subscriber::EnvFilter; -use funnel_core::tunnel::TunnelId; +use funnel_core::tunnel::id::TunnelId; #[derive(Parser)] #[command(name = "funnel", about = "tunnel client for exposing local services")] diff --git a/crates/funnel-client/src/runner.rs b/crates/funnel-client/src/runner.rs index 305849f..7da50a1 100644 --- a/crates/funnel-client/src/runner.rs +++ b/crates/funnel-client/src/runner.rs @@ -3,7 +3,7 @@ use std::time::Duration; use tokio_util::sync::CancellationToken; use url::Url; -use funnel_core::tunnel::TunnelId; +use funnel_core::tunnel::id::TunnelId; use crate::tunnel::TunnelClient; diff --git a/crates/funnel-client/src/tunnel.rs b/crates/funnel-client/src/tunnel.rs index ce1c397..7e4cc8e 100644 --- a/crates/funnel-client/src/tunnel.rs +++ b/crates/funnel-client/src/tunnel.rs @@ -6,8 +6,11 @@ use tokio_util::io::ReaderStream; use tokio_util::sync::CancellationToken; use url::Url; -use funnel_core::protocol::{self, Handshake, HandshakeResponse, RequestMeta, QUIC_ALPN}; -use funnel_core::tunnel::TunnelId; +use funnel_core::protocol::frame; +use funnel_core::protocol::handshake::{Handshake, HandshakeResponse}; +use funnel_core::protocol::request::RequestMeta; +use funnel_core::protocol::QUIC_ALPN; +use funnel_core::tunnel::id::TunnelId; use crate::forwarder::Forwarder; @@ -121,9 +124,9 @@ impl TunnelClient { token: self.token.clone(), }; - protocol::write_meta(&mut send, &handshake).await?; + frame::write_meta(&mut send, &handshake).await?; - let resp: HandshakeResponse = protocol::read_meta(&mut recv).await?; + let resp: HandshakeResponse = frame::read_meta(&mut recv).await?; resp.into_result()?; tracing::info!(tunnel_id = %self.tunnel_id, "quic tunnel connected"); @@ -145,7 +148,7 @@ async fn handle_stream( mut recv: quinn::RecvStream, forwarder: &Forwarder, ) -> anyhow::Result<()> { - let meta: RequestMeta = protocol::read_meta(&mut recv).await?; + let meta: RequestMeta = frame::read_meta(&mut recv).await?; tracing::debug!( method = %meta.method, @@ -159,7 +162,7 @@ async fn handle_stream( let (resp_meta, resp_body) = forwarder.forward(meta, body).await; - protocol::write_meta(&mut send, &resp_meta).await?; + frame::write_meta(&mut send, &resp_meta).await?; send.write_all(&resp_body).await?; send.finish()?; diff --git a/crates/funnel-core/Cargo.toml b/crates/funnel-core/Cargo.toml index 5d17dd6..6a3ff11 100644 --- a/crates/funnel-core/Cargo.toml +++ b/crates/funnel-core/Cargo.toml @@ -1,23 +1,20 @@ [package] name = "funnel-core" version = "0.1.0" -edition = "2024" +edition.workspace = true [dependencies] base64 = "0.22" -bytes = "1" getrandom = "0.3" hex = "0.4" -http = "1" nanoid = "0.4" -regex = "1" -rmp-serde = "1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" +rmp-serde.workspace = true +serde.workspace = true sha2 = "0.10" -thiserror = "2" +thiserror.workspace = true tokio = { version = "1", features = ["io-util"] } [dev-dependencies] -pretty_assertions = "1" +pretty_assertions.workspace = true +serde_json.workspace = true tokio = { version = "1", features = ["io-util", "rt", "macros"] } diff --git a/crates/funnel-core/src/auth/mod.rs b/crates/funnel-core/src/auth/mod.rs index 8798cec..79c66ba 100644 --- a/crates/funnel-core/src/auth/mod.rs +++ b/crates/funnel-core/src/auth/mod.rs @@ -1,3 +1 @@ -mod token; - -pub use token::{ApiKeyPrefix, hash_token, generate_api_key}; +pub mod token; diff --git a/crates/funnel-core/src/error.rs b/crates/funnel-core/src/error.rs deleted file mode 100644 index b6b979c..0000000 --- a/crates/funnel-core/src/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum TunnelIdError { - #[error("tunnel ID cannot be empty")] - Empty, - - #[error("tunnel ID must be at least 3 characters, got {0}")] - TooShort(usize), - - #[error("tunnel ID must be at most 63 characters, got {0}")] - TooLong(usize), - - #[error("tunnel ID must be lowercase")] - NotLowercase, - - #[error("tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen")] - InvalidFormat, -} diff --git a/crates/funnel-core/src/lib.rs b/crates/funnel-core/src/lib.rs index 3ba0702..6ae79bf 100644 --- a/crates/funnel-core/src/lib.rs +++ b/crates/funnel-core/src/lib.rs @@ -1,4 +1,3 @@ pub mod auth; -pub mod error; pub mod protocol; pub mod tunnel; diff --git a/crates/funnel-core/src/protocol/handshake.rs b/crates/funnel-core/src/protocol/handshake.rs index f9be233..f731393 100644 --- a/crates/funnel-core/src/protocol/handshake.rs +++ b/crates/funnel-core/src/protocol/handshake.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::tunnel::TunnelId; +use crate::tunnel::id::TunnelId; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Handshake { @@ -9,33 +9,29 @@ pub struct Handshake { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HandshakeResponse { - success: bool, - error: Option, +#[serde(tag = "status")] +pub enum HandshakeResponse { + #[serde(rename = "ok")] + Ok, + #[serde(rename = "rejected")] + Rejected { reason: String }, } impl HandshakeResponse { pub fn ok() -> Self { - Self { - success: true, - error: None, - } + Self::Ok } pub fn rejected(reason: impl Into) -> Self { - Self { - success: false, - error: Some(reason.into()), + Self::Rejected { + reason: reason.into(), } } pub fn into_result(self) -> Result<(), HandshakeRejected> { - if self.success { - Ok(()) - } else { - Err(HandshakeRejected { - reason: self.error.unwrap_or_else(|| "unknown error".to_string()), - }) + match self { + Self::Ok => Ok(()), + Self::Rejected { reason } => Err(HandshakeRejected { reason }), } } } diff --git a/crates/funnel-core/src/protocol/mod.rs b/crates/funnel-core/src/protocol/mod.rs index 8542261..d8218d6 100644 --- a/crates/funnel-core/src/protocol/mod.rs +++ b/crates/funnel-core/src/protocol/mod.rs @@ -2,9 +2,5 @@ pub mod frame; pub mod handshake; pub mod request; -pub use frame::{read_frame, read_meta, write_frame, write_meta, FrameError}; -pub use handshake::{Handshake, HandshakeRejected, HandshakeResponse}; -pub use request::{RequestMeta, ResponseMeta}; - /// application level protocol name used for QUIC ALPN negotiation pub const QUIC_ALPN: &[u8] = b"funnel/1"; diff --git a/crates/funnel-core/src/tunnel/id.rs b/crates/funnel-core/src/tunnel/id.rs index bc4fd10..1f488b2 100644 --- a/crates/funnel-core/src/tunnel/id.rs +++ b/crates/funnel-core/src/tunnel/id.rs @@ -1,13 +1,24 @@ use std::fmt; -use std::sync::LazyLock; -use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::error::TunnelIdError; +#[derive(Debug, thiserror::Error)] +pub enum TunnelIdError { + #[error("tunnel ID cannot be empty")] + Empty, -static TUNNEL_ID_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$").expect("valid regex")); + #[error("tunnel ID must be at least 3 characters, got {0}")] + TooShort(usize), + + #[error("tunnel ID must be at most 63 characters, got {0}")] + TooLong(usize), + + #[error("tunnel ID must be lowercase")] + NotLowercase, + + #[error("tunnel ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen")] + InvalidFormat, +} const DOMAIN_SAFE_ALPHABET: &[char] = &[ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', @@ -49,7 +60,14 @@ impl TunnelId { return Err(TunnelIdError::NotLowercase); } - if !TUNNEL_ID_REGEX.is_match(s) { + let bytes = s.as_bytes(); + let is_alnum = |b: u8| b.is_ascii_lowercase() || b.is_ascii_digit(); + let is_label_char = |b: u8| is_alnum(b) || b == b'-'; + + if !is_alnum(bytes[0]) + || !is_alnum(bytes[bytes.len() - 1]) + || !bytes[1..bytes.len() - 1].iter().all(|&b| is_label_char(b)) + { return Err(TunnelIdError::InvalidFormat); } diff --git a/crates/funnel-core/src/tunnel/mod.rs b/crates/funnel-core/src/tunnel/mod.rs index 0c73029..fd6bb6c 100644 --- a/crates/funnel-core/src/tunnel/mod.rs +++ b/crates/funnel-core/src/tunnel/mod.rs @@ -1,3 +1 @@ -mod id; - -pub use id::TunnelId; +pub mod id; diff --git a/crates/funnel-e2e/Cargo.toml b/crates/funnel-e2e/Cargo.toml index 128f369..63f1002 100644 --- a/crates/funnel-e2e/Cargo.toml +++ b/crates/funnel-e2e/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "funnel-e2e" version = "0.1.0" -edition = "2024" +edition.workspace = true [dev-dependencies] -tokio = { version = "1", features = ["full"] } -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } -axum = "0.8" +tokio.workspace = true +reqwest = { workspace = true, features = ["rustls"] } +axum.workspace = true diff --git a/crates/funnel-server/Cargo.toml b/crates/funnel-server/Cargo.toml index dc19524..47a1b20 100644 --- a/crates/funnel-server/Cargo.toml +++ b/crates/funnel-server/Cargo.toml @@ -1,35 +1,32 @@ [package] name = "funnel-server" version = "0.1.0" -edition = "2024" +edition.workspace = true [dependencies] -funnel-core = { path = "../funnel-core" } +funnel-core.workspace = true -anyhow = "1" -argon2 = "0.5" -axum = "0.8" -chrono = { version = "0.4", features = ["serde"] } -clap = { version = "4", features = ["derive", "env"] } +anyhow.workspace = true +axum.workspace = true +chrono.workspace = true +clap.workspace = true dashmap = "6" http-body-util = "0.1" -jsonwebtoken = "9" +ipnetwork = "0.20" metrics = "0.24" metrics-exporter-prometheus = "0.16" -quinn = "0.11" +quinn.workspace = true rcgen = "0.13" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -ipnetwork = "0.20" -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "migrate", "ipnetwork"] } -thiserror = "2" -tokio = { version = "1", features = ["full"] } -tokio-util = { version = "0.7", features = ["io"] } -tower = "0.5" -tower-http = { version = "0.6", features = ["cors", "trace"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } -uuid = { version = "1", features = ["v7", "serde"] } +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true +thiserror.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tower-http.workspace = true +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["json"] } +uuid.workspace = true # tls aws-config = "1" @@ -37,12 +34,12 @@ aws-sdk-route53 = "1" axum-server = { version = "0.8", features = ["tls-rustls"] } instant-acme = { version = "0.8", features = ["rcgen"] } lru = "0.16" -reqwest = { version = "0.13", default-features = false, features = ["rustls", "json", "query"] } -rustls = "0.23" +reqwest = { workspace = true, features = ["rustls", "json", "query"] } +rustls = { workspace = true, features = ["ring", "logging", "std", "tls12"] } rustls-pemfile = "2" x509-parser = "0.18" [dev-dependencies] axum-test = "16" -pretty_assertions = "1" -sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono"] } +pretty_assertions.workspace = true +sqlx.workspace = true diff --git a/crates/funnel-server/src/api/tunnels.rs b/crates/funnel-server/src/api/tunnels.rs index 0b7dc94..f925ba5 100644 --- a/crates/funnel-server/src/api/tunnels.rs +++ b/crates/funnel-server/src/api/tunnels.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use axum::Json; use axum::extract::{Path, State}; -use funnel_core::tunnel::TunnelId; +use funnel_core::tunnel::id::TunnelId; use crate::app::AppState; use crate::error::AppError; diff --git a/crates/funnel-server/src/db/api_keys.rs b/crates/funnel-server/src/db/api_keys.rs index 320e0c1..9423e1e 100644 --- a/crates/funnel-server/src/db/api_keys.rs +++ b/crates/funnel-server/src/db/api_keys.rs @@ -3,7 +3,7 @@ use serde::Serialize; use sqlx::PgPool; use uuid::Uuid; -use funnel_core::auth; +use funnel_core::auth::token as auth; #[derive(Debug, Clone, Serialize, sqlx::FromRow)] pub struct ApiKey { diff --git a/crates/funnel-server/src/error.rs b/crates/funnel-server/src/error.rs index 8b37bb4..aec36de 100644 --- a/crates/funnel-server/src/error.rs +++ b/crates/funnel-server/src/error.rs @@ -1,7 +1,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; -use funnel_core::error::TunnelIdError; +use funnel_core::tunnel::id::TunnelIdError; use serde::Serialize; #[derive(Debug, Serialize)] diff --git a/crates/funnel-server/src/proxy/router.rs b/crates/funnel-server/src/proxy/router.rs index ea1f867..3d7a7cc 100644 --- a/crates/funnel-server/src/proxy/router.rs +++ b/crates/funnel-server/src/proxy/router.rs @@ -5,8 +5,8 @@ use axum::extract::State; use axum::http::{Request, Response, StatusCode}; use tokio_util::io::ReaderStream; -use funnel_core::protocol::{RequestMeta, ResponseMeta}; -use funnel_core::tunnel::TunnelId; +use funnel_core::protocol::request::{RequestMeta, ResponseMeta}; +use funnel_core::tunnel::id::TunnelId; use crate::app::AppState; use crate::tunnel::connection::{CountedRecvStream, SendError}; diff --git a/crates/funnel-server/src/quic/handler.rs b/crates/funnel-server/src/quic/handler.rs index 7a8572c..85f2c6b 100644 --- a/crates/funnel-server/src/quic/handler.rs +++ b/crates/funnel-server/src/quic/handler.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use funnel_core::protocol::{self, Handshake, HandshakeResponse}; +use funnel_core::protocol::frame; +use funnel_core::protocol::handshake::{Handshake, HandshakeResponse}; use crate::app::AppState; use crate::tunnel::connection::ActiveTunnel; @@ -10,7 +11,7 @@ pub enum ConnectionError { Connection(#[from] quinn::ConnectionError), #[error("frame error: {0}")] - Frame(#[from] protocol::FrameError), + Frame(#[from] frame::FrameError), #[error("tunnel id conflict: {0}")] Conflict(String), @@ -25,7 +26,7 @@ pub async fn handle_connection( .await .map_err(ConnectionError::Connection)?; - let handshake: Handshake = protocol::read_meta(&mut recv).await?; + let handshake: Handshake = frame::read_meta(&mut recv).await?; let tunnel_id = handshake.tunnel_id; let tunnel = Arc::new(ActiveTunnel::new(tunnel_id.clone(), conn.clone())); @@ -36,11 +37,11 @@ pub async fn handle_connection( .is_err() { let resp = HandshakeResponse::rejected(format!("tunnel id already in use: {tunnel_id}")); - let _ = protocol::write_meta(&mut send, &resp).await; + let _ = frame::write_meta(&mut send, &resp).await; return Err(ConnectionError::Conflict(tunnel_id.to_string())); } - protocol::write_meta(&mut send, &HandshakeResponse::ok()).await?; + frame::write_meta(&mut send, &HandshakeResponse::ok()).await?; tracing::info!(tunnel_id = %tunnel_id, "tunnel connected via quic"); diff --git a/crates/funnel-server/src/tls/config.rs b/crates/funnel-server/src/tls/config.rs index 0e0ac6d..5ba8614 100644 --- a/crates/funnel-server/src/tls/config.rs +++ b/crates/funnel-server/src/tls/config.rs @@ -8,12 +8,20 @@ pub struct ProviderConfig { pub providers: Vec, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ProviderType { + Cloudflare, + Route53, + Exec, +} + #[derive(Debug, Deserialize)] pub struct DnsProviderEntry { #[allow(dead_code)] pub name: String, #[serde(rename = "type")] - pub provider_type: String, + pub provider_type: ProviderType, pub domains: Vec, pub config: HashMap, } diff --git a/crates/funnel-server/src/tls/provider/mod.rs b/crates/funnel-server/src/tls/provider/mod.rs index 6c72893..7271039 100644 --- a/crates/funnel-server/src/tls/provider/mod.rs +++ b/crates/funnel-server/src/tls/provider/mod.rs @@ -12,7 +12,7 @@ use anyhow::{Context, Result}; use self::cloudflare::CloudflareProvider; use self::exec::ExecProvider; use self::route53::Route53Provider; -use super::config::ProviderConfig; +use super::config::{ProviderConfig, ProviderType}; pub type BoxFuture<'a, T> = Pin + Send + 'a>>; @@ -37,26 +37,25 @@ impl ProviderMux { let mut mux = Self::new(); for entry in &config.providers { - let provider: Arc = match entry.provider_type.as_str() { - "cloudflare" => { + let provider: Arc = match entry.provider_type { + ProviderType::Cloudflare => { let api_token = entry .config .get("api_token") .context("cloudflare provider requires 'api_token' in config")?; Arc::new(CloudflareProvider::new(api_token.clone())) } - "route53" => { + ProviderType::Route53 => { let region = entry.config.get("region").cloned(); Arc::new(Route53Provider::new(region).await) } - "exec" => { + ProviderType::Exec => { let command = entry .config .get("command") .context("exec provider requires 'command' in config")?; Arc::new(ExecProvider::new(command.clone())) } - other => anyhow::bail!("unsupported DNS provider type: {other}"), }; for domain in &entry.domains { diff --git a/crates/funnel-server/src/tunnel/connection.rs b/crates/funnel-server/src/tunnel/connection.rs index e8fe513..9cecef4 100644 --- a/crates/funnel-server/src/tunnel/connection.rs +++ b/crates/funnel-server/src/tunnel/connection.rs @@ -6,8 +6,9 @@ use std::time::Duration; use http_body_util::BodyExt; use tokio::io::{AsyncRead, ReadBuf}; -use funnel_core::protocol::{self, FrameError, RequestMeta, ResponseMeta}; -use funnel_core::tunnel::TunnelId; +use funnel_core::protocol::frame::{self as frame, FrameError}; +use funnel_core::protocol::request::{RequestMeta, ResponseMeta}; +use funnel_core::tunnel::id::TunnelId; use super::stats::TunnelStats; @@ -60,7 +61,7 @@ impl ActiveTunnel { .await .map_err(SendError::OpenStream)?; - protocol::write_meta(&mut send, &meta) + frame::write_meta(&mut send, &meta) .await .map_err(SendError::SendMeta)?; @@ -82,7 +83,7 @@ impl ActiveTunnel { metrics::counter!("funnel_bytes_out_total").increment(bytes_sent); metrics::histogram!("funnel_request_body_bytes").record(bytes_sent as f64); - let resp_meta: ResponseMeta = protocol::read_meta(&mut recv) + let resp_meta: ResponseMeta = frame::read_meta(&mut recv) .await .map_err(SendError::ReadResponse)?; diff --git a/crates/funnel-server/src/tunnel/manager.rs b/crates/funnel-server/src/tunnel/manager.rs index 803c4c7..8fc05d5 100644 --- a/crates/funnel-server/src/tunnel/manager.rs +++ b/crates/funnel-server/src/tunnel/manager.rs @@ -4,7 +4,7 @@ use dashmap::DashMap; use dashmap::mapref::entry::Entry; use serde::Serialize; -use funnel_core::tunnel::TunnelId; +use funnel_core::tunnel::id::TunnelId; use super::connection::ActiveTunnel; use super::stats::TunnelStatsSnapshot; diff --git a/crates/funnel-web/Cargo.toml b/crates/funnel-web/Cargo.toml index 0945db1..3b7b773 100644 --- a/crates/funnel-web/Cargo.toml +++ b/crates/funnel-web/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "funnel-web" version = "0.1.0" -edition = "2024" +edition.workspace = true [dependencies] -funnel-core = { path = "../funnel-core" } +funnel-core.workspace = true leptos = { version = "0.7", features = ["csr"] } -serde = { version = "1", features = ["derive"] } +serde.workspace = true