[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.

MVP

FoxxMD (Oct 7, 2025, 4:24 PM UTC) 820f3ce8 440f997a

+216 -46
+2
.devcontainer/compose.yml
··· 24 24 user: 0:0 25 25 mem_limit: 64M 26 26 read_only: true 27 + ports: 28 + - 2379:2375 27 29 cap_drop: 28 30 - ALL 29 31 security_opt:
+1 -1
.devcontainer/devcontainer.json
··· 18 18 19 19 // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 20 "forwardPorts": [ 21 - 2376 21 + 2375 22 22 ] 23 23 24 24 // Use 'postCreateCommand' to run commands after the container is created.
+2
Cargo.lock
··· 286 286 "futures-util", 287 287 "http", 288 288 "ntex", 289 + "regex", 289 290 "reqwest", 290 291 "serde", 292 + "serde_json", 291 293 "slog", 292 294 "slog-async", 293 295 "slog-term",
+2
Cargo.toml
··· 8 8 futures-util = "0.3.31" 9 9 http = "1.3.1" 10 10 ntex = { version = "2.16.0", features = ["tokio"] } 11 + regex = "1.11.3" 11 12 reqwest = {version = "0.12.23", features = ["json"] } 12 13 serde = {version ="1.0.228", features = ["derive"] } 14 + serde_json = "1.0.145" 13 15 slog = "2.8.0" 14 16 slog-async = "2.8.0" 15 17 slog-term = "2.9.2"
+209 -45
src/main.rs
··· 4 4 extern crate slog_term; 5 5 6 6 use futures_util::TryStreamExt; 7 - use ntex::{http, web}; 8 - use slog::Drain; 7 + use ::http::StatusCode; 8 + use ntex::{http, web::{self, Responder, HttpResponse}}; 9 + use std::sync::{Arc, Mutex}; 10 + 11 + use slog::{Drain, Logger}; 9 12 use dotenv::dotenv; 10 13 use std::env; 11 - //use std::collections::HashMap; 14 + use std::collections::HashMap; 12 15 use serde::{Deserialize, Serialize}; 16 + use serde_json::Value; 13 17 18 + use std::sync::LazyLock; 19 + use regex::Regex; 20 + 21 + #[derive(Clone)] 22 + struct AppStateWithContainerMap { 23 + container_map: Arc<Mutex<HashMap<String, Option<String>>>>, // <- Mutex is necessary to mutate safely across threads 24 + } 25 + 26 + struct GetMatch { 27 + id: String, 28 + resource: String 29 + } 30 + 31 + // #[derive(Serialize, Deserialize)] 32 + // struct DockerErrorMessage { 33 + // pub message: Option<String> 34 + // } 14 35 15 36 #[derive(Serialize, Deserialize)] 16 - struct Container { 17 - Id: String, 18 - Names: Vec<String> 37 + struct ContainerSummary { 38 + #[serde(rename = "Id")] 39 + pub id: Option<String>, 40 + 41 + #[serde(rename = "Names")] 42 + pub names: Option<Vec<String>>, 43 + 44 + pub message: Option<String>, 45 + 46 + #[serde(flatten)] 47 + extra: HashMap<String, Value>, 48 + } 49 + 50 + #[derive(Serialize, Deserialize)] 51 + struct ContainerInspect { 52 + #[serde(rename = "Id")] 53 + pub id: Option<String>, 54 + 55 + #[serde(rename = "Name")] 56 + pub name: Option<String>, 57 + 58 + #[serde(rename = "Config")] 59 + pub config: Option<ContainerConfig>, 60 + 61 + pub message: Option<String>, 62 + 63 + #[serde(flatten)] 64 + extra: HashMap<String, Value>, 65 + } 66 + 67 + #[derive(Serialize, Deserialize)] 68 + struct ContainerConfig { 69 + #[serde(rename = "Env")] 70 + env: Option<Vec<String>>, 71 + 72 + #[serde(flatten)] 73 + extra: HashMap<String, Value>, 19 74 } 20 75 21 76 async fn forward( ··· 23 78 body: ntex::util::Bytes, 24 79 client: web::types::State<http::Client>, 25 80 forward_url: web::types::State<url::Url>, 26 - container_id: web::types::State<String>, 81 + container_name: web::types::State<String>, 82 + log: web::types::State<Logger>, 83 + data: web::types::State<AppStateWithContainerMap> 27 84 ) -> Result<web::HttpResponse, web::Error> { 28 85 let mut new_url = forward_url.get_ref().clone(); 29 86 new_url.set_path(req.uri().path()); 30 87 new_url.set_query(req.uri().query()); 31 88 let forwarded_req = client.request_from(new_url.as_str(), req.head()); 32 - let res = forwarded_req 89 + let mut res = forwarded_req 33 90 .send_body(body) 34 91 .await 35 92 .map_err(web::Error::from)?; 36 - // if new_url.path().contains("containers/json") { 37 - // let content = Vec<>; 38 - // res.json() 39 - // } 93 + 40 94 let mut client_resp = web::HttpResponse::build(res.status()); 41 - let stream = res.into_stream(); 42 - Ok(client_resp.streaming(stream)) 95 + 96 + info!(log, "test"); 97 + 98 + if res.status() == 200 { 99 + 100 + if new_url.path().contains("containers/json") { 101 + 102 + //let fresp = web::HttpResponse::build(res.status()).json(&filtered_containers); 103 + 104 + 105 + //let txt = String::from_utf8(res.body().await.unwrap().to_vec()).unwrap(); 106 + 107 + let containers = &res.json::<Vec<ContainerSummary>>().await.unwrap(); 108 + 109 + let filtered_containers = containers.into_iter() 110 + .filter(|&con| is_container_named(con, &container_name.get_ref())) 111 + .collect::<Vec<&ContainerSummary>>(); 112 + 113 + let fresp = web::HttpResponse::build(res.status()).json(&filtered_containers); 114 + 115 + //client_resp.json(&filtered_containers); 116 + 117 + //let stream = res.into_stream(); 118 + Ok(fresp) 119 + } else { 120 + 121 + match match_container_get(new_url.path()) { 122 + Some (m) => { 123 + let mut cm = data.container_map.lock().unwrap(); 124 + if !cm.contains_key(&m.id) { 125 + let id_res = get_container_name(&forward_url.to_string(), &m.id, &log).await; 126 + match id_res { 127 + Ok(id) => { 128 + cm.insert(m.id.clone(), Some(id)); 129 + } 130 + Err(e) => { 131 + warn!(log, "Could not get container info"; "error" => %e); 132 + cm.insert(m.id.clone(), None); 133 + } 134 + } 135 + } 136 + 137 + let name_val = cm.get(&m.id).unwrap(); 138 + 139 + match name_val { 140 + Some(n) => { 141 + if n.contains(container_name.get_ref()) { //n.iter().any(|x| x.contains(container_name.get_ref())) { 142 + let stream = res.into_stream(); 143 + client_resp.content_type("application/json"); 144 + Ok(client_resp.streaming(stream)) 145 + } else { 146 + client_resp.status(StatusCode::NOT_FOUND); 147 + Ok(client_resp.finish()) 148 + } 149 + } 150 + None => { 151 + client_resp.status(StatusCode::NOT_FOUND); 152 + Ok(client_resp.finish()) 153 + } 154 + } 155 + } 156 + None => { 157 + let stream = res.into_stream(); 158 + Ok(client_resp.streaming(stream)) 159 + } 160 + } 161 + } 162 + 163 + } else { 164 + let stream = res.into_stream(); 165 + Ok(client_resp.streaming(stream)) 166 + } 167 + 168 + 43 169 } 44 170 45 - async fn get_container(u: &String, container_name: &String) -> Result<Option<Container>, Box<dyn std::error::Error>> { 171 + fn match_container_get(haystack: &str) -> Option<GetMatch> { 172 + static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"containers/(?<id>.+)/(?<resource>.+)").unwrap()); 173 + let captures = RE.captures(haystack); 174 + 175 + let get_match = match captures { 176 + Some(cap) => { 177 + Some(GetMatch { 178 + id: String::from(cap.name("id").unwrap().as_str()), 179 + resource: String::from(cap.name("resource").unwrap().as_str()) 180 + }) 181 + } 182 + None => { 183 + None 184 + } 185 + }; 186 + get_match 187 + } 188 + 189 + // fn is_container_get(haystack: &str) -> bool { 190 + // match_container_get(&haystack).is_some() 191 + // } 192 + 193 + // fn is_container_json(haystack: &str) -> bool { 194 + // match match_container_get(&haystack) { 195 + // Some(m) => m.resource == "json", 196 + // None => false 197 + // } 198 + // } 199 + 200 + 201 + fn is_container_named(container: &ContainerSummary, container_name: &String) -> bool { 202 + return Option::is_some(&container.names.clone().unwrap().iter().find(|&y| { 203 + return y.contains(container_name); 204 + })); 205 + } 206 + 207 + async fn get_container_name(u: &String, container_id: &String, log: &Logger) -> Result<String, Box<dyn std::error::Error>> { 46 208 47 - let resp = reqwest::get(format!("{}/containers/json", u)) 209 + let url = format!("{}containers/{id}/json", u, id = container_id); 210 + debug!(log, "Get Container URL: {}", url); 211 + let resp = reqwest::get(&url) 48 212 .await? 49 - .json::<Vec<Container>>() 50 - .await?; 51 - let container = resp.into_iter().find(|x| Option::is_some(&x.Names.iter().find(|y| y.contains(container_name)))); //y == &container_name 52 - Ok(container) 213 + .json::<ContainerInspect>() 214 + .await; 215 + match resp { 216 + Ok(json_res) => { 217 + if json_res.message.is_some() { 218 + return Err(json_res.message.unwrap().into()) 219 + } 220 + match json_res.name { 221 + Some(v) => { 222 + Ok(v) 223 + } 224 + None => { 225 + return Err("Container has no names".into()) 226 + } 227 + } 228 + // Ok(json_res.names.unwrap()) 229 + } 230 + Err(e) => { 231 + return Err(Box::new(e)); 232 + } 233 + } 234 + 235 + //Ok(id) 53 236 } 54 237 55 238 #[ntex::main] ··· 90 273 }, 91 274 } 92 275 93 - let container_res = get_container(&proxy_url, &container_name).await; 94 - 95 - let container: Container = match container_res { 96 - Ok(val) => { 97 - 98 - 99 - match val { 100 - Some(v) => { 101 - info!(log, "Found container matching name '{}': {}", &container_name, v.Id); 102 - v 103 - } 104 - None => { 105 - crit!(log, "Could not find a contaienr with name '{}'", &container_name); 106 - panic!(); 107 - } 108 - } 109 - }, 110 - Err(e) => { 111 - crit!(log, "Error occurred while trying to get container"; "error" => %e); 112 - panic!(); 113 - }, 276 + let cm = AppStateWithContainerMap { 277 + container_map: Arc::new(Mutex::new(HashMap::<String, Option<String>>::new())) 114 278 }; 115 279 116 - info!(log, "fsdf {}", container.Id); 117 - 118 280 let forward_url = url::Url::parse(&proxy_url) 119 281 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; 120 282 web::server(move || { 121 283 web::App::new() 122 284 .state(http::Client::new()) 123 285 .state(forward_url.clone()) 124 - .state(container.Id.clone()) 286 + .state(container_name.clone()) 287 + .state(log.clone()) 288 + .state(cm.clone()) 125 289 .wrap(web::middleware::Logger::default()) 126 290 .default_service(web::route().to(forward)) 127 291 }) 128 - .bind(("0.0.0.0", 9090))? 292 + .bind(("0.0.0.0", 2376))? 129 293 .run() 130 294 .await 131 295 }