1use anyhow::{ensure, Context, Result};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::BTreeMap;
6pub const MAX_BODY: usize = 4 * 1024 * 1024;
7pub const MAX_ITEMS: usize = 4096;
8pub const MAX_TEXT: usize = 16 * 1024;
9type Strings = BTreeMap<String, String>;
10macro_rules! object {
11 ($name:ident { $($field:ident : $ty:ty => $wire:literal),* $(,)? }) => {
12 #[derive(Debug, Deserialize, Serialize)]
13 pub struct $name { $(#[serde(rename(deserialize = $wire))] pub $field: $ty),* }
14 }
15}
16object!(Component { name:String => "Name", version:String => "Version", details:Option<Strings> => "Details" });
17object!(Platform { name:String => "Name" });
18object!(Version {
19 version:String => "Version", api_version:String => "ApiVersion", min_api_version:Option<String> => "MinAPIVersion",
20 os:String => "Os", arch:String => "Arch", git_commit:Option<String> => "GitCommit", go_version:Option<String> => "GoVersion",
21 kernel_version:Option<String> => "KernelVersion", experimental:Option<bool> => "Experimental",
22 components:Option<Vec<Component>> => "Components", platform:Option<Platform> => "Platform"
23});
24object!(Info {
25 id:String => "ID", name:String => "Name", server_version:String => "ServerVersion",
26 containers:u64 => "Containers", containers_running:u64 => "ContainersRunning", containers_paused:u64 => "ContainersPaused",
27 containers_stopped:u64 => "ContainersStopped", images:u64 => "Images", ncpu:u64 => "NCPU", memory_total:u64 => "MemTotal",
28 driver:Option<String> => "Driver", operating_system:Option<String> => "OperatingSystem", os_type:Option<String> => "OSType",
29 architecture:Option<String> => "Architecture", kernel_version:Option<String> => "KernelVersion",
30 labels:Option<Vec<String>> => "Labels", warnings:Option<Vec<String>> => "Warnings"
31});
32object!(Port { private_port:u16 => "PrivatePort", public_port:Option<u16> => "PublicPort", protocol:String => "Type", ip:Option<String> => "IP" });
33object!(Container {
34 id:String => "Id", names:Vec<String> => "Names", image:String => "Image", image_id:String => "ImageID", command:String => "Command",
35 created:i64 => "Created", ports:Option<Vec<Port>> => "Ports", labels:Option<Strings> => "Labels", state:String => "State", status:String => "Status",
36 size_rw:Option<u64> => "SizeRw", size_root_fs:Option<u64> => "SizeRootFs"
37});
38object!(ContainerState {
39 status:String => "Status", running:bool => "Running", paused:bool => "Paused", restarting:bool => "Restarting",
40 oom_killed:bool => "OOMKilled", dead:bool => "Dead", pid:u64 => "Pid", exit_code:i64 => "ExitCode", error:String => "Error",
41 started_at:String => "StartedAt", finished_at:String => "FinishedAt"
42});
43object!(Config {
44 image:String => "Image", hostname:Option<String> => "Hostname", user:Option<String> => "User", env:Option<Vec<String>> => "Env",
45 cmd:Option<Vec<String>> => "Cmd", entrypoint:Option<Vec<String>> => "Entrypoint", working_dir:Option<String> => "WorkingDir",
46 labels:Option<Strings> => "Labels", tty:Option<bool> => "Tty", open_stdin:Option<bool> => "OpenStdin"
47});
48object!(Binding { host_ip:String => "HostIp", host_port:String => "HostPort" });
49object!(HostConfig {
50 network_mode:Option<String> => "NetworkMode", memory:Option<i64> => "Memory", cpu_shares:Option<i64> => "CpuShares",
51 port_bindings:Option<BTreeMap<String,Option<Vec<Binding>>>> => "PortBindings"
52});
53object!(EndpointSettings {
54 network_id:Option<String> => "NetworkID", endpoint_id:Option<String> => "EndpointID", ip_address:Option<String> => "IPAddress",
55 ip_prefix_len:Option<u8> => "IPPrefixLen", gateway:Option<String> => "Gateway", mac_address:Option<String> => "MacAddress",
56 global_ipv6_address:Option<String> => "GlobalIPv6Address", global_ipv6_prefix_len:Option<u8> => "GlobalIPv6PrefixLen"
57});
58object!(NetworkSettings {
59 ports:Option<BTreeMap<String,Option<Vec<Binding>>>> => "Ports", networks:Option<BTreeMap<String,EndpointSettings>> => "Networks"
60});
61object!(Mount {
62 kind:String => "Type", name:Option<String> => "Name", source:String => "Source", destination:String => "Destination",
63 driver:Option<String> => "Driver", mode:Option<String> => "Mode", read_write:bool => "RW", propagation:Option<String> => "Propagation"
64});
65object!(Inspect {
66 id:String => "Id", name:String => "Name", created:String => "Created", image:String => "Image", path:String => "Path", args:Vec<String> => "Args",
67 state:ContainerState => "State", config:Config => "Config", host_config:HostConfig => "HostConfig", network_settings:NetworkSettings => "NetworkSettings",
68 mounts:Option<Vec<Mount>> => "Mounts", size_rw:Option<u64> => "SizeRw", size_root_fs:Option<u64> => "SizeRootFs"
69});
70object!(Image {
71 id:String => "Id", parent_id:Option<String> => "ParentId", repo_tags:Option<Vec<String>> => "RepoTags", repo_digests:Option<Vec<String>> => "RepoDigests",
72 created:i64 => "Created", size:u64 => "Size", shared_size:Option<i64> => "SharedSize", containers:Option<i64> => "Containers", labels:Option<Strings> => "Labels"
73});
74object!(IpamConfig { subnet:Option<String> => "Subnet", ip_range:Option<String> => "IPRange", gateway:Option<String> => "Gateway", auxiliary_addresses:Option<Strings> => "AuxiliaryAddresses" });
75object!(Ipam { driver:String => "Driver", options:Option<Strings> => "Options", config:Option<Vec<IpamConfig>> => "Config" });
76object!(Network {
77 id:String => "Id", name:String => "Name", created:Option<String> => "Created", scope:String => "Scope", driver:String => "Driver",
78 enable_ipv6:Option<bool> => "EnableIPv6", internal:bool => "Internal", attachable:Option<bool> => "Attachable", ingress:Option<bool> => "Ingress",
79 ipam:Ipam => "IPAM", options:Option<Strings> => "Options", labels:Option<Strings> => "Labels"
80});
81object!(Usage { ref_count:i64 => "RefCount", size:i64 => "Size" });
82object!(Volume {
83 name:String => "Name", driver:String => "Driver", mountpoint:String => "Mountpoint", created_at:Option<String> => "CreatedAt",
84 scope:String => "Scope", labels:Option<Strings> => "Labels", options:Option<Strings> => "Options", usage_data:Option<Usage> => "UsageData"
85});
86object!(Volumes { volumes:Option<Vec<Volume>> => "Volumes", warnings:Option<Vec<String>> => "Warnings" });
87object!(Error { message:String => "message" });
88struct Seed<'a> {
89 depth: usize,
90 nodes: &'a mut usize,
91}
92impl<'de> serde::de::DeserializeSeed<'de> for Seed<'_> {
93 type Value = Value;
94 fn deserialize<D: serde::Deserializer<'de>>(
95 self,
96 d: D,
97 ) -> std::result::Result<Value, D::Error> {
98 use serde::de::Error;
99 *self.nodes += 1;
100 if self.depth > 32 || *self.nodes > 65536 {
101 return Err(D::Error::custom("Docker JSON nesting/node limit"));
102 }
103 d.deserialize_any(self)
104 }
105}
106impl<'de> serde::de::Visitor<'de> for Seed<'_> {
107 type Value = Value;
108 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
109 f.write_str("bounded Docker JSON")
110 }
111 fn visit_bool<E: serde::de::Error>(self, v: bool) -> std::result::Result<Value, E> {
112 Ok(Value::Bool(v))
113 }
114 fn visit_i64<E: serde::de::Error>(self, v: i64) -> std::result::Result<Value, E> {
115 Ok(v.into())
116 }
117 fn visit_u64<E: serde::de::Error>(self, v: u64) -> std::result::Result<Value, E> {
118 Ok(v.into())
119 }
120 fn visit_f64<E: serde::de::Error>(self, v: f64) -> std::result::Result<Value, E> {
121 serde_json::Number::from_f64(v)
122 .map(Value::Number)
123 .ok_or_else(|| E::custom("invalid JSON number"))
124 }
125 fn visit_unit<E: serde::de::Error>(self) -> std::result::Result<Value, E> {
126 Ok(Value::Null)
127 }
128 fn visit_str<E: serde::de::Error>(self, v: &str) -> std::result::Result<Value, E> {
129 if v.len() > MAX_TEXT {
130 return Err(E::custom("Docker text limit"));
131 }
132 Ok(Value::String(v.into()))
133 }
134 fn visit_string<E: serde::de::Error>(self, v: String) -> std::result::Result<Value, E> {
135 if v.len() > MAX_TEXT {
136 return Err(E::custom("Docker text limit"));
137 }
138 Ok(Value::String(v))
139 }
140 fn visit_seq<A: serde::de::SeqAccess<'de>>(
141 self,
142 mut a: A,
143 ) -> std::result::Result<Value, A::Error> {
144 use serde::de::Error;
145 let mut values = Vec::new();
146 while let Some(v) = a.next_element_seed(Seed {
147 depth: self.depth + 1,
148 nodes: self.nodes,
149 })? {
150 if values.len() >= MAX_ITEMS {
151 return Err(A::Error::custom("Docker array limit"));
152 }
153 values.push(v);
154 }
155 Ok(Value::Array(values))
156 }
157 fn visit_map<A: serde::de::MapAccess<'de>>(
158 self,
159 mut a: A,
160 ) -> std::result::Result<Value, A::Error> {
161 use serde::de::Error;
162 let mut values = serde_json::Map::new();
163 while let Some(key) = a.next_key::<String>()? {
164 if key.len() > 256 || values.len() >= 256 {
165 return Err(A::Error::custom("Docker object field/name limit"));
166 }
167 if values.contains_key(&key) {
168 return Err(A::Error::custom("duplicate Docker JSON field"));
169 }
170 let value = a.next_value_seed(Seed {
171 depth: self.depth + 1,
172 nodes: self.nodes,
173 })?;
174 values.insert(key, value);
175 }
176 Ok(Value::Object(values))
177 }
178}
179pub fn json(body: &[u8]) -> Result<Value> {
180 ensure!(body.len() <= MAX_BODY, "Docker body limit");
181 use serde::de::DeserializeSeed;
182 let mut decoder = serde_json::Deserializer::from_slice(body);
183 let value = Seed {
184 depth: 0,
185 nodes: &mut 0,
186 }
187 .deserialize(&mut decoder)
188 .context("invalid Docker JSON")?;
189 decoder.end().context("trailing Docker JSON content")?;
190 Ok(value)
191}
192fn typed<T: serde::de::DeserializeOwned + Serialize>(value: Value) -> Result<Value> {
193 Ok(serde_json::to_value(
194 serde_json::from_value::<T>(value).context("invalid Docker response schema")?,
195 )?)
196}
197fn state(value: &str) -> Result<()> {
198 ensure!(
199 [
200 "created",
201 "running",
202 "paused",
203 "restarting",
204 "removing",
205 "exited",
206 "dead"
207 ]
208 .contains(&value),
209 "invalid container state"
210 );
211 Ok(())
212}
213fn id(value: &str) -> Result<()> {
214 ensure!(
215 !value.is_empty()
216 && value.len() <= 256
217 && value
218 .bytes()
219 .all(|c| c.is_ascii_alphanumeric() || b"_-.".contains(&c)),
220 "invalid container ID"
221 );
222 Ok(())
223}
224pub fn parse(operation: &str, body: &[u8]) -> Result<Value> {
225 let value = json(body)?;
226 let result = match operation {
227 "version" => typed::<Version>(value)?,
228 "info" => typed::<Info>(value)?,
229 "containers" => typed::<Vec<Container>>(value)?,
230 "container" => typed::<Inspect>(value)?,
231 "images" => typed::<Vec<Image>>(value)?,
232 "networks" => typed::<Vec<Network>>(value)?,
233 "volumes" => typed::<Volumes>(value)?,
234 _ => anyhow::bail!("unknown Docker response operation"),
235 };
236 match operation {
237 "version" => {
238 super::api_version(result["api_version"].as_str().unwrap())?;
239 if let Some(v) = result["min_api_version"].as_str() {
240 ensure!(
241 super::api_version(v)?
242 <= super::api_version(result["api_version"].as_str().unwrap())?,
243 "invalid API version range"
244 );
245 }
246 }
247 "info" => {
248 ensure!(
249 result["containers_running"]
250 .as_u64()
251 .unwrap()
252 .checked_add(result["containers_paused"].as_u64().unwrap())
253 .and_then(|n| n.checked_add(result["containers_stopped"].as_u64().unwrap()))
254 == result["containers"].as_u64(),
255 "inconsistent container counts"
256 );
257 }
258 "containers" => {
259 for c in result.as_array().unwrap() {
260 id(c["id"].as_str().unwrap())?;
261 state(c["state"].as_str().unwrap())?;
262 for p in c["ports"].as_array().into_iter().flatten() {
263 ensure!(
264 p["private_port"].as_u64().unwrap() > 0
265 && p["public_port"].as_u64() != Some(0),
266 "invalid container port"
267 );
268 ensure!(
269 ["tcp", "udp", "sctp"].contains(&p["protocol"].as_str().unwrap()),
270 "invalid port protocol"
271 );
272 }
273 }
274 }
275 "container" => {
276 id(result["id"].as_str().unwrap())?;
277 state(result["state"]["status"].as_str().unwrap())?;
278 }
279 "images" => {
280 for i in result.as_array().unwrap() {
281 for field in ["shared_size", "containers"] {
282 if let Some(n) = i[field].as_i64() {
283 ensure!(n >= -1, "invalid image {field}");
284 }
285 }
286 }
287 }
288 "volumes" => {
289 if let Some(volumes) = result["volumes"].as_array() {
290 for v in volumes {
291 if let Some(usage) = v["usage_data"].as_object() {
292 for field in ["size", "ref_count"] {
293 ensure!(
294 usage[field].as_i64().unwrap() >= -1,
295 "invalid volume usage {field}"
296 );
297 }
298 }
299 }
300 }
301 }
302 _ => {}
303 }
304 Ok(result)
305}
306pub fn error(body: &[u8]) -> Result<String> {
307 let e: Error = serde_json::from_value(json(body)?).context("invalid Docker error schema")?;
308 Ok(e.message)
309}