|
| 1 | +use crate::cli::http::command::HttpAction; |
| 2 | +use crate::cli::http::command::HttpCommand; |
| 3 | +use crate::command::BuildCommand; |
| 4 | +use crate::command::BuildContext; |
| 5 | +use crate::command::Command; |
| 6 | +use crate::ConfigError; |
| 7 | +use anyhow::anyhow; |
| 8 | +use anyhow::Error; |
| 9 | +use camino::Utf8PathBuf; |
| 10 | +use certificate::CloudRootCerts; |
| 11 | +use clap::Args; |
| 12 | +use reqwest::blocking; |
| 13 | +use reqwest::Identity; |
| 14 | +use std::fs::File; |
| 15 | +use tedge_config::OptionalConfig; |
| 16 | +use tedge_config::ProfileName; |
| 17 | + |
| 18 | +#[derive(clap::Subcommand, Debug)] |
| 19 | +pub enum TEdgeHttpCli { |
| 20 | + /// POST content to thin-edge local HTTP servers |
| 21 | + /// |
| 22 | + /// Examples: |
| 23 | + /// # Create a new Cumulocity Managed Object via the proxy service |
| 24 | + /// tedge http post /c8y/inventory/managedObjects '{"name":"test"}' --accept-type application/json |
| 25 | + /// |
| 26 | + /// # Create a new child device |
| 27 | + /// tedge http post /tedge/entity-store/v1/entities '{ |
| 28 | + /// "@topic-id": "device/a//", |
| 29 | + /// "@type": "child-device", |
| 30 | + /// "@parent": "device/main//" |
| 31 | + /// }' |
| 32 | + #[clap(verbatim_doc_comment)] |
| 33 | + Post { |
| 34 | + /// Target URI |
| 35 | + uri: String, |
| 36 | + |
| 37 | + /// Content to send |
| 38 | + #[command(flatten)] |
| 39 | + content: Content, |
| 40 | + |
| 41 | + /// MIME type of the content |
| 42 | + #[clap(long)] |
| 43 | + #[arg(value_parser = parse_mime_type)] |
| 44 | + content_type: Option<String>, |
| 45 | + |
| 46 | + /// MIME type of the expected content |
| 47 | + #[clap(long)] |
| 48 | + #[arg(value_parser = parse_mime_type)] |
| 49 | + accept_type: Option<String>, |
| 50 | + |
| 51 | + /// Optional c8y cloud profile |
| 52 | + #[clap(long)] |
| 53 | + profile: Option<ProfileName>, |
| 54 | + }, |
| 55 | + |
| 56 | + /// PUT content to thin-edge local HTTP servers |
| 57 | + /// |
| 58 | + /// Examples: |
| 59 | + /// # Upload file to the file transfer service |
| 60 | + /// tedge http put /tedge/file-transfer/target.txt --file source.txt |
| 61 | + /// |
| 62 | + /// # Update a Cumulocity Managed Object. Note: Assuming tedge is the owner of the managed object |
| 63 | + /// tedge http put /c8y/inventory/managedObjects/2343978440 '{"name":"item A"}' --accept-type application/json |
| 64 | + #[clap(verbatim_doc_comment)] |
| 65 | + Put { |
| 66 | + /// Target URI |
| 67 | + uri: String, |
| 68 | + |
| 69 | + /// Content to send |
| 70 | + #[command(flatten)] |
| 71 | + content: Content, |
| 72 | + |
| 73 | + /// MIME type of the content |
| 74 | + #[clap(long)] |
| 75 | + #[arg(value_parser = parse_mime_type)] |
| 76 | + content_type: Option<String>, |
| 77 | + |
| 78 | + /// MIME type of the expected content |
| 79 | + #[clap(long)] |
| 80 | + #[arg(value_parser = parse_mime_type)] |
| 81 | + accept_type: Option<String>, |
| 82 | + |
| 83 | + /// Optional c8y cloud profile |
| 84 | + #[clap(long)] |
| 85 | + profile: Option<ProfileName>, |
| 86 | + }, |
| 87 | + |
| 88 | + /// GET content from thin-edge local HTTP servers |
| 89 | + /// |
| 90 | + /// Examples: |
| 91 | + /// # Download file from the file transfer service |
| 92 | + /// tedge http get /tedge/file-transfer/target.txt |
| 93 | + /// |
| 94 | + /// # Download file from Cumulocity's binary api |
| 95 | + /// tedge http get /c8y/inventory/binaries/104332 > my_file.bin |
| 96 | + #[clap(verbatim_doc_comment)] |
| 97 | + Get { |
| 98 | + /// Source URI |
| 99 | + uri: String, |
| 100 | + |
| 101 | + /// MIME type of the expected content |
| 102 | + #[clap(long)] |
| 103 | + #[arg(value_parser = parse_mime_type)] |
| 104 | + accept_type: Option<String>, |
| 105 | + |
| 106 | + /// Optional c8y cloud profile |
| 107 | + #[clap(long)] |
| 108 | + profile: Option<ProfileName>, |
| 109 | + }, |
| 110 | + |
| 111 | + /// DELETE resource from thin-edge local HTTP servers |
| 112 | + /// |
| 113 | + /// Examples: |
| 114 | + /// # Delete a file from the file transfer service |
| 115 | + /// tedge http delete /tedge/file-transfer/target.txt |
| 116 | + /// |
| 117 | + /// # Delete a Cumulocity managed object. Note: Assuming tedge is the owner of the managed object |
| 118 | + /// tedge http delete /c8y/inventory/managedObjects/2343978440 |
| 119 | + #[clap(verbatim_doc_comment)] |
| 120 | + Delete { |
| 121 | + /// Source URI |
| 122 | + uri: String, |
| 123 | + |
| 124 | + /// Optional c8y cloud profile |
| 125 | + #[clap(long)] |
| 126 | + profile: Option<ProfileName>, |
| 127 | + }, |
| 128 | +} |
| 129 | + |
| 130 | +#[derive(Args, Clone, Debug)] |
| 131 | +#[group(required = true, multiple = false)] |
| 132 | +pub struct Content { |
| 133 | + /// Content to send |
| 134 | + #[arg(name = "content")] |
| 135 | + arg2: Option<String>, |
| 136 | + |
| 137 | + /// Content to send |
| 138 | + #[arg(long)] |
| 139 | + data: Option<String>, |
| 140 | + |
| 141 | + /// File which content is sent |
| 142 | + #[arg(long)] |
| 143 | + file: Option<Utf8PathBuf>, |
| 144 | +} |
| 145 | + |
| 146 | +fn parse_mime_type(input: &str) -> Result<String, Error> { |
| 147 | + Ok(input.parse::<mime_guess::mime::Mime>()?.to_string()) |
| 148 | +} |
| 149 | + |
| 150 | +impl TryFrom<Content> for blocking::Body { |
| 151 | + type Error = std::io::Error; |
| 152 | + |
| 153 | + fn try_from(content: Content) -> Result<Self, Self::Error> { |
| 154 | + let body: blocking::Body = if let Some(data) = content.arg2 { |
| 155 | + data.into() |
| 156 | + } else if let Some(data) = content.data { |
| 157 | + data.into() |
| 158 | + } else if let Some(file) = content.file { |
| 159 | + File::open(file)?.into() |
| 160 | + } else { |
| 161 | + "".into() |
| 162 | + }; |
| 163 | + |
| 164 | + Ok(body) |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +impl BuildCommand for TEdgeHttpCli { |
| 169 | + fn build_command(self, context: BuildContext) -> Result<Box<dyn Command>, ConfigError> { |
| 170 | + let config = context.load_config()?; |
| 171 | + let uri = self.uri(); |
| 172 | + |
| 173 | + let (protocol, host, port) = if uri.starts_with("/c8y") { |
| 174 | + let c8y_config = config.c8y.try_get(self.c8y_profile())?; |
| 175 | + let client = &c8y_config.proxy.client; |
| 176 | + let protocol = https_if_some(&c8y_config.proxy.cert_path); |
| 177 | + (protocol, client.host.clone(), client.port) |
| 178 | + } else if uri.starts_with("/tedge") { |
| 179 | + let client = &config.http.client; |
| 180 | + let protocol = https_if_some(&config.http.cert_path); |
| 181 | + (protocol, client.host.clone(), client.port) |
| 182 | + } else { |
| 183 | + return Err(anyhow!("Not a local HTTP uri: {uri}").into()); |
| 184 | + }; |
| 185 | + |
| 186 | + let url = format!("{protocol}://{host}:{port}{uri}"); |
| 187 | + let identity = config.http.client.auth.identity()?; |
| 188 | + let client = http_client(config.cloud_root_certs(), identity.as_ref())?; |
| 189 | + let action = self.into(); |
| 190 | + |
| 191 | + Ok(HttpCommand { |
| 192 | + client, |
| 193 | + url, |
| 194 | + action, |
| 195 | + } |
| 196 | + .into_boxed()) |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +impl From<TEdgeHttpCli> for HttpAction { |
| 201 | + fn from(value: TEdgeHttpCli) -> Self { |
| 202 | + match value { |
| 203 | + TEdgeHttpCli::Post { |
| 204 | + content, |
| 205 | + content_type, |
| 206 | + accept_type, |
| 207 | + .. |
| 208 | + } => HttpAction::Post { |
| 209 | + content, |
| 210 | + content_type, |
| 211 | + accept_type, |
| 212 | + }, |
| 213 | + TEdgeHttpCli::Put { |
| 214 | + content, |
| 215 | + content_type, |
| 216 | + accept_type, |
| 217 | + .. |
| 218 | + } => HttpAction::Put { |
| 219 | + content, |
| 220 | + content_type, |
| 221 | + accept_type, |
| 222 | + }, |
| 223 | + TEdgeHttpCli::Get { accept_type, .. } => HttpAction::Get { accept_type }, |
| 224 | + TEdgeHttpCli::Delete { .. } => HttpAction::Delete, |
| 225 | + } |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +impl TEdgeHttpCli { |
| 230 | + fn uri(&self) -> &str { |
| 231 | + match self { |
| 232 | + TEdgeHttpCli::Post { uri, .. } |
| 233 | + | TEdgeHttpCli::Put { uri, .. } |
| 234 | + | TEdgeHttpCli::Get { uri, .. } |
| 235 | + | TEdgeHttpCli::Delete { uri, .. } => uri.as_ref(), |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + fn c8y_profile(&self) -> Option<&ProfileName> { |
| 240 | + match self { |
| 241 | + TEdgeHttpCli::Post { profile, .. } |
| 242 | + | TEdgeHttpCli::Put { profile, .. } |
| 243 | + | TEdgeHttpCli::Get { profile, .. } |
| 244 | + | TEdgeHttpCli::Delete { profile, .. } => profile.as_ref(), |
| 245 | + } |
| 246 | + } |
| 247 | +} |
| 248 | + |
| 249 | +fn https_if_some<T>(cert_path: &OptionalConfig<T>) -> &'static str { |
| 250 | + cert_path.or_none().map_or("http", |_| "https") |
| 251 | +} |
| 252 | + |
| 253 | +fn http_client( |
| 254 | + root_certs: CloudRootCerts, |
| 255 | + identity: Option<&Identity>, |
| 256 | +) -> Result<blocking::Client, Error> { |
| 257 | + let builder = root_certs.blocking_client_builder(); |
| 258 | + let builder = if let Some(identity) = identity { |
| 259 | + builder.identity(identity.clone()) |
| 260 | + } else { |
| 261 | + builder |
| 262 | + }; |
| 263 | + Ok(builder.build()?) |
| 264 | +} |
| 265 | + |
| 266 | +impl Content { |
| 267 | + pub fn length(&self) -> Option<usize> { |
| 268 | + if let Some(content) = &self.arg2 { |
| 269 | + Some(content.len()) |
| 270 | + } else if let Some(data) = &self.data { |
| 271 | + Some(data.len()) |
| 272 | + } else if let Some(file) = &self.file { |
| 273 | + Some(std::fs::metadata(file).ok()?.len().try_into().ok()?) |
| 274 | + } else { |
| 275 | + None |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + pub fn mime_type(&self) -> Option<String> { |
| 280 | + let file = self.file.as_ref()?; |
| 281 | + Some( |
| 282 | + mime_guess::from_path(file) |
| 283 | + .first_or_octet_stream() |
| 284 | + .to_string(), |
| 285 | + ) |
| 286 | + } |
| 287 | +} |
0 commit comments