Skip to main content

lit/network/
https.rs

1//! HTTPS transport for remote Lit repositories
2//!
3//! Communicates with a remote `lit serve` instance over HTTP/HTTPS.
4//! Uses the transport API endpoints for object and ref transfer.
5
6use crate::core::{Object, ObjectHash};
7use crate::network::transport::RemoteRef;
8use crate::storage::ObjectStore;
9
10/// Check whether a URL uses the HTTPS transport
11pub fn is_https_url(url: &str) -> bool {
12    url.starts_with("https://") || url.starts_with("http://")
13}
14
15/// Create a ureq agent (reusable HTTP client)
16fn agent() -> ureq::Agent {
17    ureq::Agent::new()
18}
19
20/// Build an HTTP request with optional bearer token auth
21fn get(url: &str, token: Option<&str>) -> ureq::Request {
22    let req = agent().get(url);
23    if let Some(t) = token {
24        req.set("Authorization", &format!("Bearer {}", t))
25    } else {
26        req
27    }
28}
29
30fn post(url: &str, token: Option<&str>) -> ureq::Request {
31    let req = agent().post(url);
32    if let Some(t) = token {
33        req.set("Authorization", &format!("Bearer {}", t))
34    } else {
35        req
36    }
37}
38
39fn put(url: &str, token: Option<&str>) -> ureq::Request {
40    let req = agent().put(url);
41    if let Some(t) = token {
42        req.set("Authorization", &format!("Bearer {}", t))
43    } else {
44        req
45    }
46}
47
48/// Parse a JSON response body
49fn read_json(resp: ureq::Response) -> Result<serde_json::Value, String> {
50    resp.into_json::<serde_json::Value>()
51        .map_err(|e| format!("Failed to parse response: {}", e))
52}
53
54/// Handle HTTP errors
55fn check_response(resp: Result<ureq::Response, ureq::Error>) -> Result<ureq::Response, String> {
56    match resp {
57        Ok(r) => Ok(r),
58        Err(ureq::Error::Status(code, resp)) => {
59            let body = resp.into_string().unwrap_or_default();
60            // Try to extract error message from JSON
61            if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
62                if let Some(msg) = v
63                    .get("error")
64                    .and_then(|e| e.get("message"))
65                    .and_then(|m| m.as_str())
66                {
67                    return Err(format!("HTTP {}: {}", code, msg));
68                }
69            }
70            Err(format!("HTTP {}: {}", code, body))
71        }
72        Err(ureq::Error::Transport(t)) => Err(format!("Connection error: {}", t)),
73    }
74}
75
76/// List refs from a remote server
77pub fn list_refs_http(
78    base_url: &str,
79    kind: &str,
80    token: Option<&str>,
81) -> Result<Vec<RemoteRef>, String> {
82    let url = format!("{}/api/v1/transport/refs?kind={}", base_url, kind);
83    let resp = check_response(get(&url, token).call())?;
84    let json = read_json(resp)?;
85
86    let refs = json
87        .get("refs")
88        .and_then(|v| v.as_array())
89        .ok_or("Invalid refs response")?;
90
91    let mut result = Vec::new();
92    for r in refs {
93        let kind = r.get("kind").and_then(|v| v.as_str()).unwrap_or("heads");
94        let name = r
95            .get("name")
96            .and_then(|v| v.as_str())
97            .ok_or("Missing ref name")?;
98        let hash = r
99            .get("hash")
100            .and_then(|v| v.as_str())
101            .ok_or("Missing ref hash")?;
102        result.push(RemoteRef {
103            kind: kind.to_string(),
104            name: name.to_string(),
105            hash: hash.to_string(),
106        });
107    }
108    Ok(result)
109}
110
111/// Read a branch ref from a remote server
112pub fn read_ref_http(base_url: &str, branch: &str, token: Option<&str>) -> Result<String, String> {
113    let url = format!("{}/api/v1/transport/refs/heads/{}", base_url, branch);
114    let resp = check_response(get(&url, token).call())?;
115    let json = read_json(resp)?;
116    json.get("hash")
117        .and_then(|v| v.as_str())
118        .map(|s| s.to_string())
119        .ok_or("Missing hash in response".to_string())
120}
121
122/// Read HEAD from a remote server
123pub fn read_head_http(base_url: &str, token: Option<&str>) -> Result<String, String> {
124    let url = format!("{}/api/v1/transport/head", base_url);
125    let resp = check_response(get(&url, token).call())?;
126    let json = read_json(resp)?;
127    json.get("head")
128        .and_then(|v| v.as_str())
129        .map(|s| s.to_string())
130        .ok_or("Missing head in response".to_string())
131}
132
133/// Update a branch ref on a remote server
134pub fn update_ref_http(
135    base_url: &str,
136    branch: &str,
137    hash: &str,
138    force: bool,
139    token: Option<&str>,
140) -> Result<(), String> {
141    let url = format!("{}/api/v1/transport/refs/heads/{}", base_url, branch);
142    let body = serde_json::json!({"hash": hash, "force": force});
143    check_response(put(&url, token).send_json(body))?;
144    Ok(())
145}
146
147/// Negotiate which objects are needed (server-side graph walk)
148pub fn negotiate_http(
149    base_url: &str,
150    wants: &[String],
151    haves: &[String],
152    token: Option<&str>,
153) -> Result<Vec<ObjectHash>, String> {
154    let url = format!("{}/api/v1/transport/negotiate", base_url);
155    let body = serde_json::json!({"wants": wants, "haves": haves});
156    let resp = check_response(post(&url, token).send_json(body))?;
157    let json = read_json(resp)?;
158
159    let needed = json
160        .get("needed")
161        .and_then(|v| v.as_array())
162        .ok_or("Invalid negotiate response")?;
163
164    Ok(needed
165        .iter()
166        .filter_map(|v| v.as_str())
167        .map(|s| ObjectHash::from_hex(s.to_string()))
168        .collect())
169}
170
171/// Download objects from a remote server into a local store
172pub fn download_objects_http(
173    base_url: &str,
174    local_store: &ObjectStore,
175    hashes: &[ObjectHash],
176    token: Option<&str>,
177) -> Result<usize, String> {
178    let mut count = 0;
179    // Download in batches to avoid excessive individual requests
180    for chunk in hashes.chunks(50) {
181        for hash in chunk {
182            if local_store.exists(hash) {
183                continue;
184            }
185            let url = format!("{}/api/v1/transport/objects/{}", base_url, hash.as_str());
186            let resp = check_response(get(&url, token).call())?;
187            let json = read_json(resp)?;
188
189            let b64_data = json
190                .get("data")
191                .and_then(|v| v.as_str())
192                .ok_or("Missing object data")?;
193
194            let compressed = base64_decode(b64_data)?;
195
196            use std::io::Read as _;
197            let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
198            let mut raw = Vec::new();
199            decoder
200                .read_to_end(&mut raw)
201                .map_err(|e| format!("Decompress error: {}", e))?;
202
203            let obj = Object::from_bytes(&raw)?;
204            local_store.write(&obj)?;
205            count += 1;
206        }
207    }
208    Ok(count)
209}
210
211/// Upload objects from a local store to a remote server
212pub fn upload_objects_http(
213    base_url: &str,
214    local_store: &ObjectStore,
215    hashes: &[ObjectHash],
216    token: Option<&str>,
217) -> Result<usize, String> {
218    let url = format!("{}/api/v1/transport/objects", base_url);
219
220    // Upload in batches of 50
221    let mut total = 0;
222    for chunk in hashes.chunks(50) {
223        let mut objects_json = Vec::new();
224        for hash in chunk {
225            let obj = local_store.read(hash)?;
226            let data = obj.to_bytes();
227            use std::io::Write as _;
228            let mut encoder =
229                flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
230            encoder
231                .write_all(&data)
232                .map_err(|e| format!("Compress error: {}", e))?;
233            let compressed = encoder
234                .finish()
235                .map_err(|e| format!("Compress error: {}", e))?;
236            let b64 = base64_encode(&compressed);
237            objects_json.push(serde_json::json!({"hash": hash.as_str(), "data": b64}));
238        }
239
240        let body = serde_json::json!({"objects": objects_json});
241        let resp = check_response(post(&url, token).send_json(body))?;
242        let json = read_json(resp)?;
243        total += json.get("written").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
244    }
245    Ok(total)
246}
247
248// ── Base64 helpers ──
249
250fn base64_encode(data: &[u8]) -> String {
251    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
252    let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
253    for chunk in data.chunks(3) {
254        let b0 = chunk[0] as u32;
255        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
256        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
257        let triple = (b0 << 16) | (b1 << 8) | b2;
258        out.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
259        out.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
260        if chunk.len() > 1 {
261            out.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
262        } else {
263            out.push('=');
264        }
265        if chunk.len() > 2 {
266            out.push(CHARS[(triple & 0x3F) as usize] as char);
267        } else {
268            out.push('=');
269        }
270    }
271    out
272}
273
274fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
275    fn val(c: u8) -> Result<u32, String> {
276        match c {
277            b'A'..=b'Z' => Ok((c - b'A') as u32),
278            b'a'..=b'z' => Ok((c - b'a' + 26) as u32),
279            b'0'..=b'9' => Ok((c - b'0' + 52) as u32),
280            b'+' => Ok(62),
281            b'/' => Ok(63),
282            b'=' => Ok(0),
283            _ => Err(format!("Invalid base64 character: {}", c as char)),
284        }
285    }
286    let bytes: Vec<u8> = input.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
287    let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
288    for chunk in bytes.chunks(4) {
289        if chunk.len() < 4 {
290            return Err("Invalid base64 length".to_string());
291        }
292        let a = val(chunk[0])?;
293        let b = val(chunk[1])?;
294        let c = val(chunk[2])?;
295        let d = val(chunk[3])?;
296        let triple = (a << 18) | (b << 12) | (c << 6) | d;
297        out.push(((triple >> 16) & 0xFF) as u8);
298        if chunk[2] != b'=' {
299            out.push(((triple >> 8) & 0xFF) as u8);
300        }
301        if chunk[3] != b'=' {
302            out.push((triple & 0xFF) as u8);
303        }
304    }
305    Ok(out)
306}