//! The remote interface: five operations, two backends. //! //! A remote is dumb storage that can do one safe swap. Everything clever — //! what to upload, what diverged, how to combine — lives in `src/exchange`, above //! this interface, so the folder and S3 backends stay thin and the sync logic //! is written once and tested offline. module remote import rand import os // get not finding a key is a normal answer, not an error, so `Object.found` // carries it and errors mean real trouble (a refused connection, bad keys). pub struct Object { pub: found bool body []u8 tag string } // Backend is one place to put bytes. `put_if_absent` never overwrites and // `put_if_match` only overwrites when the version still matches, which is what // makes two machines offering at once safe. pub interface Backend { mut: get(key string) !Object list(prefix string) ![]string put_if_absent(key string, body []u8) !bool put_if_match(key string, body []u8, tag string) !bool delete(key string) ! } pub const probe_key = 'koh-probe' // check runs the connection test `koh remote` insists on before saving a // setting: write a probe object, confirm a second write of it is refused, then // delete it. A backend that cannot do that cannot keep two machines honest. pub fn check(mut backend Backend) ! { payload := 'koh probe'.bytes() // Each check has its own key: with a shared one, two machines setting up // at once delete each other's probe and report a broken remote. key := '${probe_key}-${rand.hex(16)}' if !backend.put_if_absent(key, payload)! { return error('the remote refused to store a new object') } defer { // Also on the early returns below; deleting twice is harmless. backend.delete(key) or {} } if backend.put_if_absent(key, payload)! { return error('the remote overwrote an existing object — conditional writes are not working') } probe := backend.get(key)! if !probe.found || probe.body != payload { return error('the remote did not give back what was written') } // Strip needs delete, so a remote that cannot delete fails here. backend.delete(key)! } /// Find the system CA bundle for HTTPS; plain HTTP needs no TLS roots. pub fn tls_roots(url string) !string { mut roots := '' if url.starts_with('https://') { for path in [os.getenv('SSL_CERT_FILE'), '/etc/ssl/cert.pem', '/etc/ssl/certs/ca-certificates.crt', '/etc/ssl/certs/ca-bundle.crt', '/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/ca-bundle.pem'] { if path != '' && os.is_file(path) { roots = path break } } if roots == '' { return error('no system CA bundle found — set SSL_CERT_FILE to your trusted CA bundle') } } return roots }