[READ-ONLY] Mirror of https://github.com/FoxxMD/docker-proxy-filter. Filter the contents of Docker API responses
docker docker-socket-proxy filter
0

Configure Feed

Select the types of activity you want to include in your feed.

refactor: Move into files

FoxxMD (Oct 8, 2025, 4:01 PM UTC) 540d6693 6dbc90f2

+275 -247
+38
src/config.rs
··· 1 + use dotenvy; 2 + use serde::Deserialize; 3 + use std::path::PathBuf; 4 + use tracing::*; 5 + 6 + fn default_scrub() -> bool { 7 + false 8 + } 9 + fn default_port() -> u16 { 10 + 2375 11 + } 12 + 13 + #[derive(Deserialize, Debug)] 14 + pub struct Config { 15 + pub proxy_url: String, 16 + pub container_names: Vec<String>, 17 + #[serde(default = "default_scrub")] 18 + pub scrub_envs: bool, 19 + #[serde(default = "default_port")] 20 + pub port: u16, 21 + } 22 + 23 + pub fn loadenv() -> Result<PathBuf, dotenvy::Error> { 24 + dotenvy::dotenv() 25 + } 26 + 27 + pub fn get_config() -> Result<Config, envy::Error> { 28 + match envy::from_env::<Config>() { 29 + Ok(config) => { 30 + info!("{:#?}", config); 31 + Ok(config) 32 + } 33 + Err(error) => { 34 + error!("Could not parse envs correctly! {:#?}", error); 35 + Err(error) 36 + } 37 + } 38 + }
+61
src/docker.rs
··· 1 + use std::sync::LazyLock; 2 + use regex::Regex; 3 + 4 + pub mod types; 5 + 6 + pub struct GetMatch { 7 + pub id: String, 8 + pub resource: String 9 + } 10 + 11 + pub fn match_container_get(haystack: &str) -> Option<GetMatch> { 12 + static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"containers/(?<id>.+)/(?<resource>.+)").unwrap()); 13 + let captures = RE.captures(haystack); 14 + 15 + let get_match = match captures { 16 + Some(cap) => { 17 + Some(GetMatch { 18 + id: String::from(cap.name("id").unwrap().as_str()), 19 + resource: String::from(cap.name("resource").unwrap().as_str()) 20 + }) 21 + } 22 + None => { 23 + None 24 + } 25 + }; 26 + get_match 27 + } 28 + 29 + pub fn is_container_named(container: &types::ContainerSummary, container_names: &Vec<String>) -> bool { 30 + return Option::is_some(&container.names.clone().unwrap().iter().find(|&y| { 31 + return container_names.iter().any(|z| y.contains(z)); 32 + })); 33 + } 34 + 35 + pub async fn get_container_name(u: &String, container_id: &String) -> Result<String, Box<dyn std::error::Error>> { 36 + 37 + let url = format!("{}containers/{id}/json", u, id = container_id); 38 + //debug!(log, "Get Container URL: {}", url); 39 + let resp = reqwest::get(&url) 40 + .await? 41 + .json::<types::ContainerInspect>() 42 + .await; 43 + match resp { 44 + Ok(json_res) => { 45 + if json_res.message.is_some() { 46 + return Err(json_res.message.unwrap().into()) 47 + } 48 + match json_res.name { 49 + Some(v) => { 50 + Ok(v) 51 + } 52 + None => { 53 + return Err("Container has no name".into()) 54 + } 55 + } 56 + } 57 + Err(e) => { 58 + return Err(Box::new(e)); 59 + } 60 + } 61 + }
+48
src/docker/types.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use serde_json::Value; 3 + use std::collections::HashMap; 4 + 5 + #[derive(Serialize, Deserialize)] 6 + pub struct DockerErrorMessage { 7 + pub message: String 8 + } 9 + 10 + #[derive(Serialize, Deserialize)] 11 + pub struct ContainerSummary { 12 + #[serde(rename = "Id")] 13 + pub id: Option<String>, 14 + 15 + #[serde(rename = "Names")] 16 + pub names: Option<Vec<String>>, 17 + 18 + pub message: Option<String>, 19 + 20 + #[serde(flatten)] 21 + extra: HashMap<String, Value>, 22 + } 23 + 24 + #[derive(Serialize, Deserialize)] 25 + pub struct ContainerInspect { 26 + #[serde(rename = "Id")] 27 + pub id: Option<String>, 28 + 29 + #[serde(rename = "Name")] 30 + pub name: Option<String>, 31 + 32 + #[serde(rename = "Config")] 33 + pub config: Option<ContainerConfig>, 34 + 35 + pub message: Option<String>, 36 + 37 + #[serde(flatten)] 38 + extra: HashMap<String, Value>, 39 + } 40 + 41 + #[derive(Serialize, Deserialize)] 42 + pub struct ContainerConfig { 43 + #[serde(rename = "Env")] 44 + pub env: Option<Vec<String>>, 45 + 46 + #[serde(flatten)] 47 + extra: HashMap<String, Value>, 48 + }
+10 -247
src/main.rs
··· 1 - use tracing::{debug, error, info, warn}; 1 + use tracing::*; 2 2 use tracing_subscriber; 3 3 4 - use futures_util::TryStreamExt; 5 - use ::http::StatusCode; 6 - use ntex::{http::{self, HttpMessage}, web::{self}}; 4 + use ntex::{http, web::{self}}; 7 5 use std::sync::{Arc, Mutex}; 8 6 9 - use dotenvy; 10 7 use std::collections::HashMap; 11 - use serde::{Deserialize, Serialize}; 12 - use serde_json::Value; 13 8 14 - use std::sync::LazyLock; 15 - use regex::Regex; 9 + mod config; 10 + mod docker; 11 + mod proxy; 16 12 17 - #[derive(Clone)] 18 - struct AppStateWithContainerMap { 19 - container_map: Arc<Mutex<HashMap<String, Option<String>>>>, // <- Mutex is necessary to mutate safely across threads 20 - } 21 - 22 - struct GetMatch { 23 - id: String, 24 - resource: String 25 - } 26 - 27 - #[derive(Serialize, Deserialize)] 28 - struct DockerErrorMessage { 29 - pub message: String 30 - } 31 - 32 - #[derive(Serialize, Deserialize)] 33 - struct ContainerSummary { 34 - #[serde(rename = "Id")] 35 - pub id: Option<String>, 36 - 37 - #[serde(rename = "Names")] 38 - pub names: Option<Vec<String>>, 39 - 40 - pub message: Option<String>, 41 - 42 - #[serde(flatten)] 43 - extra: HashMap<String, Value>, 44 - } 45 - 46 - #[derive(Serialize, Deserialize)] 47 - struct ContainerInspect { 48 - #[serde(rename = "Id")] 49 - pub id: Option<String>, 50 - 51 - #[serde(rename = "Name")] 52 - pub name: Option<String>, 53 - 54 - #[serde(rename = "Config")] 55 - pub config: Option<ContainerConfig>, 56 - 57 - pub message: Option<String>, 58 - 59 - #[serde(flatten)] 60 - extra: HashMap<String, Value>, 61 - } 62 - 63 - #[derive(Serialize, Deserialize)] 64 - struct ContainerConfig { 65 - #[serde(rename = "Env")] 66 - env: Option<Vec<String>>, 67 - 68 - #[serde(flatten)] 69 - extra: HashMap<String, Value>, 70 - } 71 - 72 - #[derive(Deserialize, Debug)] 73 - struct Config { 74 - proxy_url: String, 75 - container_names: Vec<String>, 76 - #[serde(default="default_scrub")] 77 - scrub_envs: bool, 78 - #[serde(default="default_port")] 79 - port: u16 80 - } 81 - 82 - fn default_scrub() -> bool { 83 - false 84 - } 85 - fn default_port() -> u16 { 86 - 2375 87 - } 88 - 89 - async fn forward( 90 - req: web::HttpRequest, 91 - body: ntex::util::Bytes, 92 - client: web::types::State<http::Client>, 93 - forward_url: web::types::State<url::Url>, 94 - container_names: web::types::State<Vec<String>>, 95 - scrub_env: web::types::State<bool>, 96 - data: web::types::State<AppStateWithContainerMap> 97 - ) -> Result<web::HttpResponse, web::Error> { 98 - let mut new_url = forward_url.get_ref().clone(); 99 - new_url.set_path(req.uri().path()); 100 - new_url.set_query(req.uri().query()); 101 - let forwarded_req = client.request_from(new_url.as_str(), req.head()); 102 - let mut res = forwarded_req 103 - .send_body(body) 104 - .await 105 - .map_err(web::Error::from)?; 106 - 107 - let mut client_resp = web::HttpResponse::build(res.status()); 108 - client_resp.content_type(res.content_type()); 109 - 110 - if res.status() == 200 { 111 - 112 - if new_url.path().contains("containers/json") { 113 - 114 - let containers = &res.json::<Vec<ContainerSummary>>().await.unwrap(); 115 - 116 - let filtered_containers = containers.into_iter() 117 - .filter(|&con| is_container_named(con, &container_names.get_ref())) 118 - .collect::<Vec<&ContainerSummary>>(); 119 - 120 - let fresp = web::HttpResponse::build(res.status()).json(&filtered_containers); 121 - debug!("{} of {} containers valid", filtered_containers.len(), containers.len()); 122 - Ok(fresp) 123 - } else { 124 - 125 - match match_container_get(new_url.path()) { 126 - Some (m) => { 127 - let mut cm = data.container_map.lock().unwrap(); 128 - if !cm.contains_key(&m.id) { 129 - debug!("Requested container Id not in map, trying to inspect: {}", &m.id); 130 - let name_res = get_container_name(&forward_url.to_string(), &m.id).await; 131 - match name_res { 132 - Ok(name) => { 133 - debug!("Container Id {} has name '{}'", &m.id, &name); 134 - cm.insert(m.id.clone(), Some(name)); 135 - } 136 - Err(e) => { 137 - warn!("Could not inspect container {}: {e}", &m.id); 138 - cm.insert(m.id.clone(), None); 139 - } 140 - } 141 - } 142 - 143 - let name_val = cm.get(&m.id).unwrap(); 144 - 145 - match name_val { 146 - Some(n) => { 147 - if container_names.get_ref().iter().any(|x| n.contains(x)) { 148 - //if n.contains(container_name.get_ref()) { //n.iter().any(|x| x.contains(container_name.get_ref())) { 149 - 150 - if m.resource == "json" { 151 - 152 - client_resp.content_type("application/json"); 153 - 154 - if *scrub_env.get_ref() { 155 - let mut container = res.json::<ContainerInspect>().await.unwrap(); 156 - container.config.as_mut().unwrap().env = Some(Vec::new()); 157 - Ok(client_resp.json(&container)) 158 - } else { 159 - Ok(client_resp.streaming(res.into_stream())) 160 - } 161 - 162 - } else { 163 - client_resp.content_type(res.content_type()); 164 - Ok(client_resp.streaming(res.into_stream())) 165 - } 166 - } else { 167 - debug!("Container {} does not include container filter name, 404ing...", &m.id); 168 - client_resp.status(StatusCode::NOT_FOUND); 169 - Ok(client_resp.json(&DockerErrorMessage { message: format!("No such container: {}", &m.id)})) 170 - } 171 - } 172 - None => { 173 - debug!("Container {} does not exist or Docker API previously returned an error, 404ing...", &m.id); 174 - client_resp.status(StatusCode::NOT_FOUND); 175 - Ok(client_resp.json(&DockerErrorMessage { message: format!("No such container: {}", &m.id)})) 176 - } 177 - } 178 - } 179 - None => { 180 - let stream = res.into_stream(); 181 - Ok(client_resp.streaming(stream)) 182 - } 183 - } 184 - } 185 - 186 - } else { 187 - let stream = res.into_stream(); 188 - Ok(client_resp.streaming(stream)) 189 - } 190 - 191 - 192 - } 193 - 194 - fn match_container_get(haystack: &str) -> Option<GetMatch> { 195 - static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"containers/(?<id>.+)/(?<resource>.+)").unwrap()); 196 - let captures = RE.captures(haystack); 197 - 198 - let get_match = match captures { 199 - Some(cap) => { 200 - Some(GetMatch { 201 - id: String::from(cap.name("id").unwrap().as_str()), 202 - resource: String::from(cap.name("resource").unwrap().as_str()) 203 - }) 204 - } 205 - None => { 206 - None 207 - } 208 - }; 209 - get_match 210 - } 211 - 212 - // fn is_container_get(haystack: &str) -> bool { 213 - // match_container_get(&haystack).is_some() 214 - // } 215 - 216 - fn is_container_named(container: &ContainerSummary, container_names: &Vec<String>) -> bool { 217 - return Option::is_some(&container.names.clone().unwrap().iter().find(|&y| { 218 - return container_names.iter().any(|z| y.contains(z)); 219 - })); 220 - } 221 - 222 - async fn get_container_name(u: &String, container_id: &String) -> Result<String, Box<dyn std::error::Error>> { 223 - 224 - let url = format!("{}containers/{id}/json", u, id = container_id); 225 - //debug!(log, "Get Container URL: {}", url); 226 - let resp = reqwest::get(&url) 227 - .await? 228 - .json::<ContainerInspect>() 229 - .await; 230 - match resp { 231 - Ok(json_res) => { 232 - if json_res.message.is_some() { 233 - return Err(json_res.message.unwrap().into()) 234 - } 235 - match json_res.name { 236 - Some(v) => { 237 - Ok(v) 238 - } 239 - None => { 240 - return Err("Container has no name".into()) 241 - } 242 - } 243 - } 244 - Err(e) => { 245 - return Err(Box::new(e)); 246 - } 247 - } 248 - } 13 + use proxy::{AppStateWithContainerMap}; 249 14 250 15 #[ntex::main] 251 16 async fn main() -> std::io::Result<()> { 252 - let _ = dotenvy::dotenv(); 17 + let _ = config::loadenv(); 253 18 254 19 tracing_subscriber::fmt::fmt() 255 20 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 256 21 .init(); 257 22 258 - let config = match envy::from_env::<Config>() { 23 + let config = match config::get_config() { 259 24 Ok(config) => { 260 - info!("{:#?}", config); 261 25 config 262 26 }, 263 - Err(error) => { 264 - error!("Could not parse envs correctly! {:#?}", error); 27 + Err(_error) => { 265 28 panic!("Unable to start due to invalid envs") 266 29 } 267 30 }; ··· 280 43 .state(cm.clone()) 281 44 .state(config.scrub_envs.clone()) 282 45 .wrap(web::middleware::Logger::default()) 283 - .default_service(web::route().to(forward)) 46 + .default_service(web::route().to(proxy::forward)) 284 47 }) 285 48 .bind(("0.0.0.0", config.port))? 286 49 .run()
+118
src/proxy.rs
··· 1 + use tracing::*; 2 + use std::sync::{Arc, Mutex}; 3 + use std::collections::HashMap; 4 + use ntex::{http::{self, HttpMessage}, web::{self}}; 5 + use futures_util::TryStreamExt; 6 + use ::http::StatusCode; 7 + 8 + use crate::docker::{self, types::*}; 9 + 10 + #[derive(Clone)] 11 + pub struct AppStateWithContainerMap { 12 + pub container_map: Arc<Mutex<HashMap<String, Option<String>>>>, // <- Mutex is necessary to mutate safely across threads 13 + } 14 + 15 + pub async fn forward( 16 + req: web::HttpRequest, 17 + body: ntex::util::Bytes, 18 + client: web::types::State<http::Client>, 19 + forward_url: web::types::State<url::Url>, 20 + container_names: web::types::State<Vec<String>>, 21 + scrub_env: web::types::State<bool>, 22 + data: web::types::State<AppStateWithContainerMap> 23 + ) -> Result<web::HttpResponse, web::Error> { 24 + let mut new_url = forward_url.get_ref().clone(); 25 + new_url.set_path(req.uri().path()); 26 + new_url.set_query(req.uri().query()); 27 + let forwarded_req = client.request_from(new_url.as_str(), req.head()); 28 + let mut res = forwarded_req 29 + .send_body(body) 30 + .await 31 + .map_err(web::Error::from)?; 32 + 33 + let mut client_resp = web::HttpResponse::build(res.status()); 34 + client_resp.content_type(res.content_type()); 35 + 36 + if res.status() == 200 { 37 + 38 + if new_url.path().contains("containers/json") { 39 + 40 + let containers = &res.json::<Vec<ContainerSummary>>().await.unwrap(); 41 + 42 + let filtered_containers = containers.into_iter() 43 + .filter(|&con| docker::is_container_named(con, &container_names.get_ref())) 44 + .collect::<Vec<&ContainerSummary>>(); 45 + 46 + let fresp = web::HttpResponse::build(res.status()).json(&filtered_containers); 47 + debug!("{} of {} containers valid", filtered_containers.len(), containers.len()); 48 + Ok(fresp) 49 + } else { 50 + 51 + match docker::match_container_get(new_url.path()) { 52 + Some (m) => { 53 + let mut cm = data.container_map.lock().unwrap(); 54 + if !cm.contains_key(&m.id) { 55 + debug!("Requested container Id not in map, trying to inspect: {}", &m.id); 56 + let name_res = docker::get_container_name(&forward_url.to_string(), &m.id).await; 57 + match name_res { 58 + Ok(name) => { 59 + debug!("Container Id {} has name '{}'", &m.id, &name); 60 + cm.insert(m.id.clone(), Some(name)); 61 + } 62 + Err(e) => { 63 + warn!("Could not inspect container {}: {e}", &m.id); 64 + cm.insert(m.id.clone(), None); 65 + } 66 + } 67 + } 68 + 69 + let name_val = cm.get(&m.id).unwrap(); 70 + 71 + match name_val { 72 + Some(n) => { 73 + if container_names.get_ref().iter().any(|x| n.contains(x)) { 74 + //if n.contains(container_name.get_ref()) { //n.iter().any(|x| x.contains(container_name.get_ref())) { 75 + 76 + if m.resource == "json" { 77 + 78 + client_resp.content_type("application/json"); 79 + 80 + if *scrub_env.get_ref() { 81 + let mut container = res.json::<ContainerInspect>().await.unwrap(); 82 + container.config.as_mut().unwrap().env = Some(Vec::new()); 83 + Ok(client_resp.json(&container)) 84 + } else { 85 + Ok(client_resp.streaming(res.into_stream())) 86 + } 87 + 88 + } else { 89 + client_resp.content_type(res.content_type()); 90 + Ok(client_resp.streaming(res.into_stream())) 91 + } 92 + } else { 93 + debug!("Container {} does not include container filter name, 404ing...", &m.id); 94 + client_resp.status(StatusCode::NOT_FOUND); 95 + Ok(client_resp.json(&DockerErrorMessage { message: format!("No such container: {}", &m.id)})) 96 + } 97 + } 98 + None => { 99 + debug!("Container {} does not exist or Docker API previously returned an error, 404ing...", &m.id); 100 + client_resp.status(StatusCode::NOT_FOUND); 101 + Ok(client_resp.json(&DockerErrorMessage { message: format!("No such container: {}", &m.id)})) 102 + } 103 + } 104 + } 105 + None => { 106 + let stream = res.into_stream(); 107 + Ok(client_resp.streaming(stream)) 108 + } 109 + } 110 + } 111 + 112 + } else { 113 + let stream = res.into_stream(); 114 + Ok(client_resp.streaming(stream)) 115 + } 116 + 117 + 118 + }