use anyhow::{bail, Context, Result};
use reqwest::blocking::{Client, RequestBuilder};
use std::time::Duration;
pub const API_BASE: &str = "https://cloudsign.webnotarius.pl";
pub fn client() -> Result<Client> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("multipart/form-data, application/json"),
);
Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(60))
.user_agent("ssign")
.default_headers(headers)
.build()
.context("building API client")
}
fn abs(link: &str) -> String {
if link.starts_with("http") {
link.to_string()
} else if let Some(rest) = link.strip_prefix('/') {
format!("{API_BASE}/{rest}")
} else {
format!("{API_BASE}/{link}")
}
}
pub fn run_task(
client: &Client,
token: &str,
initial: RequestBuilder,
) -> Result<(String, Vec<u8>)> {
let mut resp = initial.send().context("task request")?;
for _ in 0..120 {
let status = resp.status().as_u16();
let url = resp.url().clone();
let ct = resp
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let body = resp.bytes().context("reading task response")?.to_vec();
if status >= 400 {
bail!(
"task failed (HTTP {status}) at {url}: {}",
String::from_utf8_lossy(&body)
);
}
if !ct.contains("multipart") {
if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&body) {
if v.get("state").and_then(|s| s.as_str()) == Some("failed") {
bail!("cloud task failed: {}", String::from_utf8_lossy(&body));
}
if let Some(link) = v.get("atom:link").and_then(|x| x.as_str()) {
let ping = v.get("ping-after").and_then(|x| x.as_u64()).unwrap_or(0);
std::thread::sleep(Duration::from_millis(ping.clamp(200, 2000)));
resp = client
.get(abs(link))
.bearer_auth(token)
.send()
.context("task poll")?;
continue;
}
}
}
return Ok((ct, body));
}
bail!("task did not complete after 120 polls")
}
pub fn multipart_part(body: &[u8], name: &str) -> Result<Vec<u8>> {
let first_line_end = body
.windows(2)
.position(|w| w == b"\r\n")
.context("multipart body has no CRLF")?;
let delim = &body[..first_line_end];
if !delim.starts_with(b"--") {
bail!("multipart body does not start with a boundary");
}
let needle = format!("name=\"{name}\"");
for chunk in split_on(body, delim) {
let chunk = trim_crlf(chunk);
if chunk.is_empty() || chunk == b"--" {
continue;
}
let Some(sep) = find(chunk, b"\r\n\r\n") else {
continue;
};
let headers = &chunk[..sep];
let value = &chunk[sep + 4..];
if find(headers, needle.as_bytes()).is_some() {
return Ok(trim_crlf(value).to_vec());
}
}
bail!("multipart part `{name}` not found")
}
fn split_on<'a>(hay: &'a [u8], sep: &[u8]) -> Vec<&'a [u8]> {
let mut out = Vec::new();
let mut start = 0;
let mut i = 0;
while i + sep.len() <= hay.len() {
if &hay[i..i + sep.len()] == sep {
out.push(&hay[start..i]);
i += sep.len();
start = i;
} else {
i += 1;
}
}
out.push(&hay[start..]);
out
}
fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || hay.len() < needle.len() {
return None;
}
(0..=hay.len() - needle.len()).find(|&i| &hay[i..i + needle.len()] == needle)
}
fn trim_crlf(b: &[u8]) -> &[u8] {
let mut s = b;
while s.first() == Some(&b'\r') || s.first() == Some(&b'\n') {
s = &s[1..];
}
while s.last() == Some(&b'\r') || s.last() == Some(&b'\n') {
s = &s[..s.len() - 1];
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_named_part() {
let body = b"----BND\r\nContent-Disposition: form-data; name=\"certificate\"; filename=\"x.cer\"\r\nContent-Type: application/octet-stream\r\n\r\n\x01\x02\x03DER\r\n----BND\r\nContent-Disposition: form-data; name=\"res\"\r\nContent-Type: application/json\r\n\r\n[{}]\r\n----BND--\r\n";
let cert = multipart_part(body, "certificate").unwrap();
assert_eq!(cert, b"\x01\x02\x03DER");
let res = multipart_part(body, "res").unwrap();
assert_eq!(res, b"[{}]");
}
}