use crate::errors::{Error, Result};
use crate::server::{
AddVersionResult, GetVersionResult, HistorySegment, Server, Snapshot, SnapshotUrgency,
VersionId,
};
use std::time::Duration;
use url::Url;
use uuid::Uuid;
use super::encryption::{Cryptor, Sealed, Secret, Unsealed};
pub(crate) struct SyncServer {
base_url: Url,
client_id: Uuid,
cryptor: Cryptor,
agent: ureq::Agent,
}
const HISTORY_SEGMENT_CONTENT_TYPE: &str = "application/vnd.taskchampion.history-segment";
const SNAPSHOT_CONTENT_TYPE: &str = "application/vnd.taskchampion.snapshot";
impl SyncServer {
pub(crate) fn new(
url: String,
client_id: Uuid,
encryption_secret: Vec<u8>,
) -> Result<SyncServer> {
let url = Url::parse(&url)
.map_err(|_| Error::Server(format!("Could not parse {} as a URL", url)))?;
Ok(SyncServer {
base_url: url,
client_id,
cryptor: Cryptor::new(client_id, &Secret(encryption_secret.to_vec()))?,
agent: ureq::AgentBuilder::new()
.timeout_connect(Duration::from_secs(10))
.timeout_read(Duration::from_secs(60))
.build(),
})
}
fn construct_endpoint_url(&self, path_components: &str) -> Result<Url> {
self.base_url.join(path_components).map_err(|_| {
Error::Server(format!(
"Could not build url from base {} and path component(s) {}",
self.base_url, path_components
))
})
}
}
fn get_uuid_header(resp: &ureq::Response, name: &str) -> Result<Uuid> {
let value = resp
.header(name)
.ok_or_else(|| anyhow::anyhow!("Response does not have {} header", name))?;
let value = Uuid::parse_str(value)
.map_err(|e| anyhow::anyhow!("{} header is not a valid UUID: {}", name, e))?;
Ok(value)
}
fn get_snapshot_urgency(resp: &ureq::Response) -> SnapshotUrgency {
match resp.header("X-Snapshot-Request") {
None => SnapshotUrgency::None,
Some(hdr) => match hdr {
"urgency=low" => SnapshotUrgency::Low,
"urgency=high" => SnapshotUrgency::High,
_ => SnapshotUrgency::None,
},
}
}
fn sealed_from_resp(resp: ureq::Response, version_id: Uuid, content_type: &str) -> Result<Sealed> {
use std::io::Read;
if resp.header("Content-Type") == Some(content_type) {
let mut reader = resp.into_reader();
let mut payload = vec![];
reader.read_to_end(&mut payload)?;
Ok(Sealed {
version_id,
payload,
})
} else {
Err(Error::Server(String::from(
"Response did not have expected content-type",
)))
}
}
impl Server for SyncServer {
fn add_version(
&mut self,
parent_version_id: VersionId,
history_segment: HistorySegment,
) -> Result<(AddVersionResult, SnapshotUrgency)> {
let url = self.construct_endpoint_url(
format!("v1/client/add-version/{}", parent_version_id).as_str(),
)?;
let unsealed = Unsealed {
version_id: parent_version_id,
payload: history_segment,
};
let sealed = self.cryptor.seal(unsealed)?;
match self
.agent
.post(url.as_str())
.set("Content-Type", HISTORY_SEGMENT_CONTENT_TYPE)
.set("X-Client-Id", &self.client_id.to_string())
.send_bytes(sealed.as_ref())
{
Ok(resp) => {
let version_id = get_uuid_header(&resp, "X-Version-Id")?;
Ok((
AddVersionResult::Ok(version_id),
get_snapshot_urgency(&resp),
))
}
Err(ureq::Error::Status(409, resp)) => {
let parent_version_id = get_uuid_header(&resp, "X-Parent-Version-Id")?;
Ok((
AddVersionResult::ExpectedParentVersion(parent_version_id),
SnapshotUrgency::None,
))
}
Err(err) => Err(err.into()),
}
}
fn get_child_version(&mut self, parent_version_id: VersionId) -> Result<GetVersionResult> {
let url = self.construct_endpoint_url(
format!("v1/client/get-child-version/{}", parent_version_id).as_str(),
)?;
match self
.agent
.get(url.as_str())
.set("X-Client-Id", &self.client_id.to_string())
.call()
{
Ok(resp) => {
let parent_version_id = get_uuid_header(&resp, "X-Parent-Version-Id")?;
let version_id = get_uuid_header(&resp, "X-Version-Id")?;
let sealed =
sealed_from_resp(resp, parent_version_id, HISTORY_SEGMENT_CONTENT_TYPE)?;
let history_segment = self.cryptor.unseal(sealed)?.payload;
Ok(GetVersionResult::Version {
version_id,
parent_version_id,
history_segment,
})
}
Err(ureq::Error::Status(404, _)) => Ok(GetVersionResult::NoSuchVersion),
Err(err) => Err(err.into()),
}
}
fn add_snapshot(&mut self, version_id: VersionId, snapshot: Snapshot) -> Result<()> {
let url =
self.construct_endpoint_url(format!("v1/client/add-snapshot/{}", version_id).as_str())?;
let unsealed = Unsealed {
version_id,
payload: snapshot,
};
let sealed = self.cryptor.seal(unsealed)?;
Ok(self
.agent
.post(url.as_str())
.set("Content-Type", SNAPSHOT_CONTENT_TYPE)
.set("X-Client-Id", &self.client_id.to_string())
.send_bytes(sealed.as_ref())
.map(|_| ())?)
}
fn get_snapshot(&mut self) -> Result<Option<(VersionId, Snapshot)>> {
let url = self.construct_endpoint_url("v1/client/snapshot")?;
match self
.agent
.get(url.as_str())
.set("X-Client-Id", &self.client_id.to_string())
.call()
{
Ok(resp) => {
let version_id = get_uuid_header(&resp, "X-Version-Id")?;
let sealed = sealed_from_resp(resp, version_id, SNAPSHOT_CONTENT_TYPE)?;
let snapshot = self.cryptor.unseal(sealed)?.payload;
Ok(Some((version_id, snapshot)))
}
Err(ureq::Error::Status(404, _)) => Ok(None),
Err(err) => Err(err.into()),
}
}
}