diff --git a/README.md b/README.md index 5daed2a..c883d9a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ -# Harbr-Router: High-Performance Rust Reverse Proxy with TCP Support +# Harbr-Router: High-Performance Universal Proxy -A blazingly fast, memory-efficient reverse proxy built in Rust using async I/O and designed for high-scale production workloads. Harbr-Router now supports both HTTP and raw TCP traffic, making it perfect for database proxying and other non-HTTP protocols. +A blazingly fast, memory-efficient multi-protocol proxy built in Rust using async I/O and designed for high-scale production workloads. Harbr-Router supports HTTP, TCP, and UDP traffic through a unified configuration interface. ## Features - ⚡ **High Performance**: Built on Tokio and Hyper for maximum throughput - 🔄 **Automatic Retries**: Configurable retry logic for failed requests - ⏱️ **Smart Timeouts**: Per-route and global timeout configuration -- 🔍 **Health Checks**: Built-in health check support for upstreams - 📊 **Metrics**: Prometheus-compatible metrics for monitoring - 🔄 **Zero Downtime**: Graceful shutdown support - 🛡️ **Battle-tested**: Built on production-grade libraries -- 🎯 **Path-based Routing**: Flexible route configuration -- 🌐 **Protocol Agnostic**: Support for both HTTP and TCP traffic +- 🌐 **Protocol Agnostic**: Support for HTTP, TCP, and UDP traffic - 🗄️ **Database Support**: Automatic detection and handling of database protocols - 🔌 **Connection Pooling**: Efficient reuse of TCP connections for better performance +- 🔄 **UDP Proxying**: Support for stateless UDP protocols (DNS, syslog, game servers) +- 🎯 **Path-based Routing**: Flexible route configuration for HTTP +- 🔍 **Health Checks**: Built-in health check support for HTTP upstreams ## Quick Start @@ -29,12 +30,14 @@ listen_addr: "0.0.0.0:8080" global_timeout_ms: 5000 max_connections: 10000 -# TCP Proxy Configuration +# TCP/UDP Proxy Configuration tcp_proxy: enabled: true listen_addr: "0.0.0.0:9090" connection_pooling: true max_idle_time_secs: 60 + udp_enabled: true + udp_listen_addr: "0.0.0.0:9090" # Same port as TCP routes: # HTTP Routes @@ -43,12 +46,24 @@ routes: timeout_ms: 3000 retry_count: 2 + # Default HTTP route + "/": + upstream: "http://default-backend:8080" + timeout_ms: 5000 + retry_count: 1 + # Database Route (automatically handled as TCP) "postgres-db": upstream: "postgresql://postgres-db:5432" is_tcp: true db_type: "postgresql" timeout_ms: 10000 + + # UDP Route + "dns-service": + upstream: "dns-server:53" + is_udp: true + timeout_ms: 1000 ``` 3. Run the proxy: @@ -66,7 +81,7 @@ harbr-router -c config.yml | `global_timeout_ms` | Integer | Global request timeout in milliseconds | Required | | `max_connections` | Integer | Maximum number of concurrent connections | Required | -### TCP Proxy Configuration +### TCP/UDP Proxy Configuration | Field | Type | Description | Default | |-------|------|-------------|---------| @@ -74,6 +89,8 @@ harbr-router -c config.yml | `listen_addr` | String | Address and port for TCP listener | `0.0.0.0:9090` | | `connection_pooling` | Boolean | Enable connection pooling for TCP | `true` | | `max_idle_time_secs` | Integer | Max time to keep idle connections | `60` | +| `udp_enabled` | Boolean | Enable UDP proxy functionality | `false` | +| `udp_listen_addr` | String | Address and port for UDP listener | Same as TCP | ### HTTP Route Configuration @@ -85,16 +102,20 @@ Each HTTP route is defined by a path prefix and its configuration: | `health_check_path` | String | Path for health checks | Optional | | `timeout_ms` | Integer | Route-specific timeout in ms | Global timeout | | `retry_count` | Integer | Number of retry attempts | 0 | +| `priority` | Integer | Route priority (higher wins) | 0 | +| `preserve_host_header` | Boolean | Preserve original Host header | `false` | -### TCP/Database Route Configuration +### TCP/UDP/Database Route Configuration -TCP routes use the same configuration structure with additional fields: +Non-HTTP routes use the same configuration structure with additional fields: | Field | Type | Description | Default | |-------|------|-------------|---------| | `is_tcp` | Boolean | Mark route as TCP instead of HTTP | `false` | +| `is_udp` | Boolean | Mark route as UDP instead of TCP/HTTP | `false` | | `db_type` | String | Database type (mysql, postgresql, etc.) | Optional | | `tcp_listen_port` | Integer | Custom port for this TCP service | Optional | +| `udp_listen_port` | Integer | Custom port for this UDP service | Optional | ### Example Configuration @@ -106,8 +127,17 @@ max_connections: 10000 tcp_proxy: enabled: true listen_addr: "0.0.0.0:9090" + udp_enabled: true + udp_listen_addr: "0.0.0.0:9090" # Same port as TCP routes: + # HTTP Routes + "/api/critical": + upstream: "http://critical-backend:8080" + priority: 100 + timeout_ms: 1000 + retry_count: 3 + "/api": upstream: "http://backend-api:8080" health_check_path: "/health" @@ -135,11 +165,22 @@ routes: is_tcp: true db_type: "postgresql" tcp_listen_port: 5433 # Custom listening port + + # UDP Routes + "dns-service": + upstream: "dns-server:53" + is_udp: true + timeout_ms: 1000 + + "syslog-collector": + upstream: "logging-service:514" + is_udp: true + timeout_ms: 2000 ``` ## Database Support -Harbr-Router now includes automatic detection and support for common database protocols: +Harbr-Router automatically detects and supports common database protocols: - **MySQL/MariaDB** (ports 3306, 33060) - **PostgreSQL** (port 5432) @@ -157,6 +198,28 @@ Database connections are automatically detected by: 2. Port numbers in the upstream URL 3. Protocol prefixes (mysql://, postgresql://, etc.) +## UDP Protocol Support + +Harbr-Router provides first-class support for UDP-based protocols: + +- **DNS** (port 53) +- **Syslog** (port 514) +- **SNMP** (port 161) +- **NTP** (port 123) +- **Game server protocols** +- **Custom UDP services** + +UDP connections are explicitly configured with: +- `is_udp: true` in the route configuration +- Setting the upstream destination in the standard format + +## HTTP Route Matching + +- Routes are matched by prefix (most specific wins) +- Priority can be explicitly set (higher number = higher priority) +- More specific routes take precedence when priority is equal +- The "/" route acts as a catch-all default + ## TCP Proxy Operation The TCP proxy operates by: @@ -166,23 +229,42 @@ The TCP proxy operates by: 3. Maintaining connection pooling for better performance 4. Applying timeouts and retries as configured +## UDP Proxy Operation + +The UDP proxy operates by: + +1. Receiving datagrams on the configured UDP listening port +2. Determining the appropriate upstream based on client address or first packet +3. Forwarding datagrams to the upstream destination +4. Relaying responses back to the original client + ## Metrics -The proxy now exposes additional TCP proxy metrics at `/metrics`: +The proxy exposes Prometheus-compatible metrics at `/metrics`: ### Counter Metrics | Metric | Labels | Description | |--------|--------|-------------| +| `proxy_request_total` | `status=success\|error` | HTTP requests total | +| `proxy_attempt_total` | `result=success\|failure\|timeout` | HTTP request attempts | +| `proxy_timeout_total` | - | HTTP timeouts | | `tcp_proxy.connection.new` | - | New TCP connections created | | `tcp_proxy.connection.completed` | - | Completed TCP connections | | `tcp_proxy.timeout` | - | TCP connection timeouts | +| `udp_proxy.datagram.received` | - | UDP datagrams received | +| `udp_proxy.datagram.forwarded` | - | UDP datagrams forwarded | +| `udp_proxy.datagram.response_sent` | - | UDP responses sent back to clients | +| `udp_proxy.timeout` | - | UDP timeout counter | +| `udp_proxy.unexpected_source` | - | Responses from unexpected sources | ### Histogram Metrics | Metric | Description | |--------|-------------| +| `proxy_request_duration_seconds` | HTTP request duration histogram | | `tcp_proxy.connection.duration_seconds` | TCP connection duration histogram | +| `udp_proxy.datagram.duration` | UDP transaction duration histogram | ## Production Deployment @@ -195,8 +277,16 @@ COPY . . RUN cargo build --release FROM debian:bullseye-slim +RUN apt-get update && apt-get install -y ca-certificates tzdata && rm -rf /var/lib/apt/lists/* COPY --from=builder /usr/src/harbr-router/target/release/harbr-router /usr/local/bin/ +RUN mkdir -p /etc/harbr-router +RUN useradd -r -U -s /bin/false harbr && chown -R harbr:harbr /etc/harbr-router +USER harbr +WORKDIR /etc/harbr-router +ENV CONFIG_FILE="/etc/harbr-router/config.yml" +EXPOSE 8080 9090 ENTRYPOINT ["harbr-router"] +CMD ["-c", "/etc/harbr-router/config.yml"] ``` ### Docker Compose @@ -208,15 +298,18 @@ services: image: harbr-router:latest ports: - "8080:8080" # HTTP - - "9090:9090" # TCP + - "9090:9090/tcp" # TCP + - "9090:9090/udp" # UDP volumes: - ./config.yml:/etc/harbr-router/config.yml + environment: + - RUST_LOG=info command: ["-c", "/etc/harbr-router/config.yml"] ``` ### Kubernetes -A ConfigMap example with both HTTP and TCP configuration: +A ConfigMap example with HTTP, TCP, and UDP configuration: ```yaml apiVersion: v1 @@ -231,6 +324,8 @@ data: tcp_proxy: enabled: true listen_addr: "0.0.0.0:9090" + udp_enabled: true + udp_listen_addr: "0.0.0.0:9090" routes: "/api": upstream: "http://backend-api:8080" @@ -239,11 +334,14 @@ data: "postgres-db": upstream: "postgresql://postgres-db:5432" is_tcp: true + "dns-service": + upstream: "kube-dns.kube-system:53" + is_udp: true ``` ## Performance Tuning -For high-performance deployments with TCP traffic, adjust system limits: +For high-performance deployments with TCP and UDP traffic, adjust system limits: ```bash # /etc/sysctl.conf @@ -251,22 +349,23 @@ net.core.somaxconn = 65535 net.ipv4.tcp_max_syn_backlog = 65535 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_tw_reuse = 1 -fs.file-max = 2097152 # Increased for high TCP connection count +fs.file-max = 2097152 # Increased for high connection count +net.core.rmem_max = 26214400 # Increase UDP receive buffer +net.core.wmem_max = 26214400 # Increase UDP send buffer ``` ## Use Cases -- **Database Load Balancing**: Distribute database connections across multiple nodes -- **Database Connection Limiting**: Control the maximum connections to your database -- **Database Proxying**: Put your databases behind a secure proxy layer -- **Protocol Conversion**: Use as a bridge between different network protocols -- **Edge Proxy**: Use as an edge proxy for both HTTP and non-HTTP traffic -- **Multi-Protocol Gateway**: Handle mixed HTTP/TCP traffic at the edge +- **Multi-Protocol API Gateway**: Handle HTTP, TCP, and UDP services with a single proxy +- **Database Connection Management**: Control database connections with pooling and timeouts +- **Microservice Architecture**: Route traffic between internal services +- **Edge Proxy**: Use as an edge proxy for all protocols +- **IoT Gateway**: Handle diverse protocols used by IoT devices +- **Game Server Infrastructure**: Proxy both TCP and UDP game traffic ## Contributing -Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. - +Contributions are welcome! ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/config.yml b/config.yml index f0ed46a..2f436da 100644 --- a/config.yml +++ b/config.yml @@ -3,12 +3,14 @@ listen_addr: "0.0.0.0:8081" global_timeout_ms: 5000 max_connections: 10000 -# TCP Proxy Configuration +# TCP/UDP Proxy Configuration tcp_proxy: enabled: true listen_addr: "0.0.0.0:9090" connection_pooling: true max_idle_time_secs: 60 + udp_enabled: true + udp_listen_addr: "0.0.0.0:9090" # Same port as TCP routes: # HTTP Routes @@ -53,4 +55,20 @@ routes: is_tcp: true timeout_ms: 5000 retry_count: 1 - tcp_listen_port: 9001 \ No newline at end of file + tcp_listen_port: 9001 + + # UDP routes + "dns-server": + upstream: "dns-service:53" + is_udp: true + timeout_ms: 1000 + + "syslog-collector": + upstream: "logging-service:514" + is_udp: true + timeout_ms: 2000 + + "game-server": + upstream: "game-service:27015" + is_udp: true + timeout_ms: 5000 \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 393263b..9e2fa2b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +// src/config.rs (updated) use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -25,6 +26,10 @@ pub struct TcpProxyConfig { pub connection_pooling: bool, #[serde(default = "default_tcp_max_idle_time_secs")] pub max_idle_time_secs: u64, + #[serde(default = "default_udp_enabled")] + pub udp_enabled: bool, + #[serde(default = "default_udp_listen_addr")] + pub udp_listen_addr: String, } fn default_tcp_enabled() -> bool { @@ -43,6 +48,14 @@ fn default_tcp_max_idle_time_secs() -> u64 { 60 } +fn default_udp_enabled() -> bool { + false +} + +fn default_udp_listen_addr() -> String { + "0.0.0.0:9090".to_string() // Same port as TCP by default +} + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct RouteConfig { pub upstream: String, @@ -52,11 +65,19 @@ pub struct RouteConfig { pub priority: Option, pub preserve_host_header: Option, - // New TCP-specific configuration + // TCP-specific configuration #[serde(default = "default_is_tcp")] pub is_tcp: bool, #[serde(default = "default_tcp_port")] pub tcp_listen_port: Option, + + // UDP-specific configuration + #[serde(default = "default_is_udp")] + pub is_udp: Option, + #[serde(default = "default_udp_port")] + pub udp_listen_port: Option, + + // Database-specific configuration #[serde(default = "default_db_type")] pub db_type: Option, } @@ -69,6 +90,14 @@ fn default_tcp_port() -> Option { None } +fn default_is_udp() -> Option { + Some(false) +} + +fn default_udp_port() -> Option { + None +} + fn default_db_type() -> Option { None } diff --git a/src/proxy.rs b/src/http_proxy.rs similarity index 99% rename from src/proxy.rs rename to src/http_proxy.rs index 837fc31..d94a68b 100644 --- a/src/proxy.rs +++ b/src/http_proxy.rs @@ -43,7 +43,7 @@ pub async fn run_server( shutdown_signal().await; }); - tracing::info!("Reverse proxy listening on {}", addr); + tracing::info!("HTTP proxy listening on {}", addr); server.await; Ok(()) } diff --git a/src/main.rs b/src/main.rs index d5c8c92..0c22582 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ +// src/main.rs (updated) use anyhow::Result; use std::sync::Arc; use tokio::sync::RwLock; mod config; mod metrics; -mod proxy; -mod tcp_proxy; // Add the new TCP proxy module +mod http_proxy; +mod tcp_proxy; // TCP proxy module +mod udp_proxy; // UDP proxy module #[tokio::main] async fn main() -> Result<()> { @@ -27,6 +29,11 @@ async fn main() -> Result<()> { config::is_likely_database(route) }); + // Check for UDP routes + let has_udp_routes = config.routes.iter().any(|(_, route)| { + route.is_udp.unwrap_or(false) + }); + // Start TCP proxy if enabled or if database routes are detected if config.tcp_proxy.enabled || has_db_routes { tracing::info!("TCP proxy support enabled"); @@ -40,9 +47,26 @@ async fn main() -> Result<()> { } }); } + + // Start UDP proxy if enabled or if UDP routes are detected + if config.tcp_proxy.udp_enabled || has_udp_routes { + tracing::info!("UDP proxy support enabled"); + let udp_config = config_arc.clone(); + + // Use the same address as TCP proxy by default + let udp_listen_addr = config.tcp_proxy.udp_listen_addr.clone(); + + // Spawn UDP proxy server in a separate task + tokio::spawn(async move { + let udp_proxy = udp_proxy::UdpProxyServer::new(udp_config); + if let Err(e) = udp_proxy.run(&udp_listen_addr).await { + tracing::error!("UDP proxy server error: {}", e); + } + }); + } // Start the HTTP proxy server - proxy::run_server(config_arc) + http_proxy::run_server(config_arc) .await .map_err(|e| anyhow::anyhow!("HTTP Server error: {}", e))?; diff --git a/src/udp_proxy.rs b/src/udp_proxy.rs new file mode 100644 index 0000000..a0ca763 --- /dev/null +++ b/src/udp_proxy.rs @@ -0,0 +1,263 @@ +// src/udp_proxy.rs +use crate::config::{ProxyConfig, RouteConfig}; +use dashmap::DashMap; +use metrics::{counter, histogram}; +use std::io; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::net::UdpSocket; +use tokio::sync::{mpsc, RwLock}; +use tokio::time::timeout; + +type SharedConfig = Arc>; +type DestinationMap = Arc>; + +const MAX_DATAGRAM_SIZE: usize = 65507; // Maximum UDP datagram size + +pub struct UdpProxyServer { + config: SharedConfig, + destination_map: DestinationMap, +} + +impl UdpProxyServer { + pub fn new(config: SharedConfig) -> Self { + Self { + config, + destination_map: Arc::new(DashMap::new()), + } + } + + pub async fn run(&self, addr: &str) -> io::Result<()> { + // Initialize the UDP socket for listening + let socket = Arc::new(UdpSocket::bind(addr).await?); + tracing::info!("UDP proxy listening on {}", addr); + + // Create a channel for response handling + let (tx, mut rx) = mpsc::channel::<(Vec, SocketAddr, SocketAddr)>(1000); + + // Spawn a task for handling responses + let response_socket = socket.clone(); + tokio::spawn(async move { + while let Some((data, client_addr, _)) = rx.recv().await { + match response_socket.send_to(&data, client_addr).await { + Ok(sent) => { + tracing::debug!("Sent {} bytes response to {}", sent, client_addr); + counter!("udp_proxy.datagram.response_sent", 1); + } + Err(e) => { + tracing::error!("Error sending response to {}: {}", client_addr, e); + counter!("udp_proxy.error", 1); + } + } + } + }); + + // Buffer for receiving datagrams + let mut buf = vec![0u8; MAX_DATAGRAM_SIZE]; + + // Main processing loop + loop { + // Set up timeout based on global config + let timeout_ms = { + let config_guard = self.config.read().await; + config_guard.global_timeout_ms + }; + let timeout_duration = Duration::from_millis(timeout_ms); + + // Try to receive a datagram with timeout + let receive_result = match timeout( + timeout_duration, + socket.recv_from(&mut buf) + ).await { + Ok(result) => result, + Err(_) => { + // Timeout occurred, just continue the loop + counter!("udp_proxy.timeout", 1); + continue; + } + }; + + // Process the received datagram + match receive_result { + Ok((size, client_addr)) => { + tracing::debug!("Received {} bytes from {}", size, client_addr); + let start = Instant::now(); + counter!("udp_proxy.datagram.received", 1); + + // Clone required resources for the handler task + let request_socket = socket.clone(); + let config_clone = self.config.clone(); + let destination_map = self.destination_map.clone(); + let datagram = buf[..size].to_vec(); + let tx_clone = tx.clone(); + + // Process the datagram in a separate task + tokio::spawn(async move { + if let Err(e) = Self::handle_datagram( + request_socket, + client_addr, + datagram, + config_clone, + destination_map, + tx_clone, + start + ).await { + tracing::error!("Error handling UDP datagram: {}", e); + counter!("udp_proxy.error", 1); + } + }); + } + Err(e) => { + tracing::error!("Error receiving UDP datagram: {}", e); + counter!("udp_proxy.error", 1); + } + } + } + } + + async fn handle_datagram( + socket: Arc, + client_addr: SocketAddr, + datagram: Vec, + config: SharedConfig, + destination_map: DestinationMap, + tx: mpsc::Sender<(Vec, SocketAddr, SocketAddr)>, + start_time: Instant, + ) -> io::Result<()> { + // Determine the route and upstream destination + let destination = { + // Check if we already have a mapping for this client + if let Some(dest) = destination_map.get(&client_addr) { + dest.clone() + } else { + // For UDP, we need to determine the route based on client info + // or the contents of the first packet + // This is a simplified implementation - in a real-world scenario, + // you might need more sophisticated logic to determine the correct route + + // For now, use the first UDP route in the config + let config_guard = config.read().await; + let udp_route = config_guard.routes.iter() + .find(|(_, route)| { + route.is_udp.unwrap_or(false) + }); + + if let Some((_, route)) = udp_route { + // Extract host and port from the upstream URL + let upstream_url = &route.upstream; + let dest = extract_host_port(upstream_url); + + // Store the mapping for future datagrams from this client + destination_map.insert(client_addr, dest.clone()); + + dest + } else { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "No UDP route configured", + )); + } + } + }; + + // Get timeout from configuration + let timeout_ms = { + let config_guard = config.read().await; + let default_timeout = config_guard.global_timeout_ms; + + // Try to find a specific route config for this destination + config_guard.routes.iter() + .find(|(_, route)| { + extract_host_port(&route.upstream) == destination + }) + .and_then(|(_, route)| route.timeout_ms) + .unwrap_or(default_timeout) + }; + + // Forward the datagram to the destination + let dest_addr = destination.parse::().map_err(|e| { + io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid destination address: {}", e)) + })?; + + // Set up timeout for the send operation + match timeout( + Duration::from_millis(timeout_ms), + socket.send_to(&datagram, dest_addr) + ).await { + Ok(Ok(bytes_sent)) => { + tracing::debug!("Forwarded {} bytes to {}", bytes_sent, dest_addr); + counter!("udp_proxy.datagram.forwarded", 1); + } + Ok(Err(e)) => { + tracing::error!("Error forwarding UDP datagram: {}", e); + counter!("udp_proxy.error", 1); + return Err(e); + } + Err(_) => { + tracing::warn!("Timeout forwarding UDP datagram to {}", dest_addr); + counter!("udp_proxy.timeout", 1); + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "Timed out forwarding UDP datagram", + )); + } + } + + // Create a new socket for receiving responses + // We need a separate socket because we can't listen on the same socket we're sending from + // without more complex socket sharing logic + let bind_addr = if socket.local_addr()?.ip().is_unspecified() { + format!("0.0.0.0:0") + } else { + format!("{}:0", socket.local_addr()?.ip()) + }; + + let response_socket = UdpSocket::bind(bind_addr).await?; + + // Connected UDP sockets can only receive from the specific address they're connected to + response_socket.connect(dest_addr).await?; + + // Set up a task to wait for a response + let mut response_buf = vec![0u8; MAX_DATAGRAM_SIZE]; + match timeout( + Duration::from_millis(timeout_ms), + response_socket.recv(&mut response_buf) + ).await { + Ok(Ok(size)) => { + // Forward the response back to the client through the channel + if let Err(e) = tx.send(( + response_buf[..size].to_vec(), + client_addr, + dest_addr + )).await { + tracing::error!("Failed to send response through channel: {}", e); + counter!("udp_proxy.error", 1); + } + } + Ok(Err(e)) => { + tracing::error!("Error receiving response: {}", e); + counter!("udp_proxy.error", 1); + } + Err(_) => { + tracing::debug!("No response received within timeout"); + counter!("udp_proxy.timeout", 1); + } + } + + // Record metrics + let duration = start_time.elapsed(); + histogram!("udp_proxy.datagram.duration", duration.as_secs_f64()); + + Ok(()) + } +} + +// Helper function to extract host:port from a URL +fn extract_host_port(url: &str) -> String { + // Parse out protocol + let url_without_protocol = url.split("://").nth(1).unwrap_or(url); + + // Extract host:port part + url_without_protocol.split('/').next().unwrap_or(url_without_protocol).to_string() +} \ No newline at end of file