reddb_engine/query/local_database/
service.rs1use super::{vault::LocalVaultKey, LocalDatabase};
4use reddb_file::vfs::StdFile;
5use reddb_file::{EmbeddedRdbWriterLock, OpenMode, StdVfs, Vfs, VfsFile};
6use reddb_types::json::Value as Json;
7use reddb_types::Value;
8use std::io::{BufRead, Read, Write};
9use std::net::{TcpListener, TcpStream};
10use std::path::Path;
11use std::sync::{
12 atomic::{AtomicBool, Ordering},
13 Arc,
14};
15use std::time::Duration;
16use zeroize::Zeroizing;
17
18type Result<T> = std::result::Result<T, String>;
19const REQUEST_BYTES_MAX: usize = 2 * 1024 * 1024;
20
21pub fn run(arguments: &[String]) -> Result<()> {
25 let [flag, path] = arguments else {
26 return Err("expected --path <private.rdb>".into());
27 };
28 if flag != "--path" {
29 return Err("expected --path <private.rdb>".into());
30 }
31 let mut bootstrap = Zeroizing::new(String::new());
32 std::io::stdin()
33 .lock()
34 .take(8192)
35 .read_line(&mut bootstrap)
36 .map_err(error)?;
37 if !bootstrap.ends_with('\n') {
38 return Err("invalid bootstrap frame".into());
39 }
40 let frame: Json = reddb_types::serde_json::from_str(&bootstrap).map_err(error)?;
41 drop(bootstrap);
42 let password = match frame {
43 Json::Object(mut object) => match object.remove("password") {
44 Some(Json::String(password)) => Some(Zeroizing::new(password)),
45 Some(Json::Null) | None => None,
46 _ => return Err("invalid opening credential".into()),
47 },
48 _ => return Err("invalid bootstrap frame".into()),
49 };
50 let path = Path::new(path);
51 let _owner = EmbeddedRdbWriterLock::acquire(path).map_err(error)?;
52 let existed = path.exists();
53 let mut mode = if existed {
54 OpenMode::create_keep()
55 } else {
56 OpenMode::create_new()
57 };
58 mode.read = true;
59 let file = StdVfs.open(path, mode).map_err(error)?;
60 let database = if existed {
61 LocalDatabase::open(file, path)
62 } else {
63 LocalDatabase::create(file, path, 0)
64 }
65 .map_err(error)?;
66 let mut service = Service {
67 database,
68 key: None,
69 };
70 service.database.query("CREATE TABLE IF NOT EXISTS workspace_metadata (entry_key TEXT PRIMARY KEY, entry_value TEXT NOT NULL)", 0).map_err(error)?;
71 if let Some(password) = &password {
72 let create = !service.database.vault_initialized().map_err(error)?;
74 service.key = service.database.vault_unlock(password, create).ok();
75 }
76 drop(password);
77 let listener = TcpListener::bind("127.0.0.1:0").map_err(error)?;
78 listener.set_nonblocking(true).map_err(error)?;
79 let token = private_token()?;
80 println!("{}", reddb_types::json!({ "protocol": 1, "port": listener.local_addr().map_err(error)?.port(), "token": token.clone() }).to_string_compact());
81 std::io::stdout().flush().map_err(error)?;
82 let alive = Arc::new(AtomicBool::new(true));
83 let lifeline = alive.clone();
84 std::thread::spawn(move || {
85 let mut sink = [0u8; 1];
86 while std::io::stdin()
87 .read(&mut sink)
88 .is_ok_and(|count| count > 0)
89 {}
90 lifeline.store(false, Ordering::Release);
91 });
92 while alive.load(Ordering::Acquire) {
93 match listener.accept() {
94 Ok((mut stream, _)) => {
95 let request = read_request(&mut stream, &token);
96 let (response, stop) = match request {
97 Ok(request) => {
98 let stop = request.get("method").and_then(Json::as_str) == Some("shutdown");
99 let result = service.execute(&request);
100 (
101 match result {
102 Ok(value) => reddb_types::json!({ "ok": true, "result": value }),
103 Err(message) => {
104 reddb_types::json!({ "ok": false, "error": message })
105 }
106 },
107 stop,
108 )
109 }
110 Err(_) => {
111 let _ = stream.write_all(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
112 continue;
113 }
114 };
115 let body = response.to_string_compact();
116 let _ = write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nCache-Control: no-store\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
117 if stop {
118 break;
119 }
120 }
121 Err(failure) if failure.kind() == std::io::ErrorKind::WouldBlock => {
122 std::thread::sleep(Duration::from_millis(20))
123 }
124 Err(failure) => return Err(error(failure)),
125 }
126 }
127 service.database.into_inner().sync_all().map_err(error)
128}
129
130fn error(error: impl std::fmt::Display) -> String {
131 error.to_string()
132}
133
134fn private_token() -> Result<String> {
135 let mut bytes = [0u8; 32];
136 reddb_crypto::os_random::fill_bytes(&mut bytes).map_err(error)?;
138 Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect())
139}
140
141fn read_request(stream: &mut TcpStream, token: &str) -> Result<Json> {
142 stream
143 .set_read_timeout(Some(Duration::from_secs(5)))
144 .map_err(error)?;
145 stream
146 .set_write_timeout(Some(Duration::from_secs(5)))
147 .map_err(error)?;
148 let mut header = Vec::new();
149 while !header.ends_with(b"\r\n\r\n") && header.len() < 8192 {
150 let mut byte = [0];
151 stream.read_exact(&mut byte).map_err(error)?;
152 header.push(byte[0]);
153 }
154 let header = std::str::from_utf8(&header).map_err(error)?;
155 let mut lines = header.split("\r\n");
156 if lines.next() != Some("POST /workspace HTTP/1.1") {
157 return Err("invalid endpoint".into());
158 }
159 let mut length = None;
160 let mut authenticated = false;
161 for line in lines.filter(|line| !line.is_empty()) {
162 let (name, value) = line.split_once(':').ok_or("invalid header")?;
163 let name = name.to_ascii_lowercase();
164 let value = value.trim();
165 match name.as_str() {
166 "origin" | "transfer-encoding" => return Err("browser requests are refused".into()),
167 "authorization" => {
168 let expected = format!("Bearer {token}");
169 authenticated = value.len() == expected.len()
170 && value
171 .bytes()
172 .zip(expected.bytes())
173 .fold(0u8, |difference, (left, right)| difference | (left ^ right))
174 == 0;
175 }
176 "content-length" => {
177 if length.is_some() {
178 return Err("duplicate length".into());
179 }
180 length = Some(value.parse::<usize>().map_err(error)?);
181 }
182 _ => {}
183 }
184 }
185 let length = length
186 .filter(|length| *length > 0 && *length <= REQUEST_BYTES_MAX)
187 .ok_or("invalid request size")?;
188 if !authenticated {
189 return Err("unauthenticated".into());
190 }
191 let mut body = Zeroizing::new(vec![0; length]);
192 stream.read_exact(&mut body).map_err(error)?;
193 reddb_types::serde_json::from_slice(&body).map_err(error)
194}
195
196struct Service {
197 database: LocalDatabase<StdFile>,
198 key: Option<LocalVaultKey>,
199}
200
201impl Service {
202 fn execute(&mut self, request: &Json) -> Result<Json> {
203 let params = request.get("params").unwrap_or(&Json::Null);
204 let name = || {
205 params
206 .get("name")
207 .and_then(Json::as_str)
208 .ok_or_else(|| "missing Vault key".to_string())
209 };
210 match request
211 .get("method")
212 .and_then(Json::as_str)
213 .ok_or("missing method")?
214 {
215 "read" => {
216 let result = self
217 .database
218 .query("SELECT entry_key, entry_value FROM workspace_metadata", 0)
219 .map_err(error)?;
220 let mut object = reddb_types::json::Map::new();
221 for row in result.rows {
222 let key = row[0].as_text().ok_or("invalid metadata key")?;
223 let value = row[1].as_text().ok_or("invalid metadata value")?;
224 object.insert(key.into(), Json::String(value.into()));
225 }
226 Ok(Json::Object(object))
227 }
228 "commit" => self.commit(params),
229 "vaultStatus" => Ok(
230 reddb_types::json!({ "initialized": self.database.vault_initialized().map_err(error)?, "locked": self.key.is_none() }),
231 ),
232 "vaultUnlock" => {
233 self.key = None;
234 let password = params
235 .get("password")
236 .and_then(Json::as_str)
237 .ok_or("missing opening credential")?;
238 let create = params
239 .get("create")
240 .and_then(Json::as_bool)
241 .unwrap_or(false);
242 self.key = Some(
243 self.database
244 .vault_unlock(password, create)
245 .map_err(error)?,
246 );
247 Ok(Json::Null)
248 }
249 "vaultLock" => {
250 self.key = None;
251 Ok(Json::Null)
252 }
253 "vaultKeys" => Ok(Json::Array(
254 self.database
255 .vault_keys(self.key.as_ref().ok_or("VAULT_LOCKED")?)
256 .map_err(error)?
257 .into_iter()
258 .map(Json::String)
259 .collect(),
260 )),
261 "vaultReveal" => Ok(self
262 .database
263 .vault_reveal(self.key.as_ref().ok_or("VAULT_LOCKED")?, name()?)
264 .map_err(error)?
265 .map(Json::String)
266 .unwrap_or(Json::Null)),
267 "shutdown" => Ok(Json::Null),
268 _ => Err("unsupported workspace operation".into()),
269 }
270 }
271
272 fn commit(&mut self, params: &Json) -> Result<Json> {
273 self.database.query("BEGIN", 0).map_err(error)?;
274 let result = self.commit_inner(params);
275 if result.is_err() {
276 let _ = self.database.query("ROLLBACK", 0);
277 }
278 result
279 }
280
281 fn commit_inner(&mut self, params: &Json) -> Result<Json> {
282 let metadata = params
283 .get("metadata")
284 .and_then(Json::as_object)
285 .ok_or("missing metadata batch")?;
286 let if_absent = params
287 .get("ifAbsent")
288 .and_then(Json::as_bool)
289 .unwrap_or(false);
290 for (key, value) in metadata {
291 if key.len() > 2048 {
292 return Err("metadata key too long".into());
293 }
294 if if_absent
295 && !self
296 .database
297 .query_with_params(
298 "SELECT entry_key FROM workspace_metadata WHERE entry_key = $1",
299 &[Value::text(key.as_str())],
300 0,
301 )
302 .map_err(error)?
303 .rows
304 .is_empty()
305 {
306 continue;
307 }
308 self.database
309 .query_with_params(
310 "DELETE FROM workspace_metadata WHERE entry_key = $1",
311 &[Value::text(key.as_str())],
312 0,
313 )
314 .map_err(error)?;
315 if !matches!(value, Json::Null) {
316 let value = value.as_str().ok_or("invalid metadata value")?;
317 self.database
318 .query_with_params(
319 "INSERT INTO workspace_metadata VALUES ($1, $2)",
320 &[Value::text(key.as_str()), Value::text(value)],
321 0,
322 )
323 .map_err(error)?;
324 }
325 }
326 if let Some(secrets) = params.get("secrets").and_then(Json::as_object) {
327 for (name, value) in secrets {
328 if if_absent
329 && self
330 .database
331 .vault_reveal(self.key.as_ref().ok_or("VAULT_LOCKED")?, name)
332 .map_err(error)?
333 .is_some()
334 {
335 continue;
336 }
337 let value = if matches!(value, Json::Null) {
338 None
339 } else {
340 Some(value.as_str().ok_or("invalid secret value")?)
341 };
342 self.database
343 .vault_put(self.key.as_ref().ok_or("VAULT_LOCKED")?, name, value)
344 .map_err(error)?;
345 }
346 }
347 self.database
348 .query_with_params(
349 "DELETE FROM workspace_metadata WHERE entry_key = $1",
350 &[Value::text("__workspace_migration_v1")],
351 0,
352 )
353 .map_err(error)?;
354 self.database
355 .query_with_params(
356 "INSERT INTO workspace_metadata VALUES ($1, $2)",
357 &[
358 Value::text("__workspace_migration_v1"),
359 Value::text("complete"),
360 ],
361 0,
362 )
363 .map_err(error)?;
364 self.database.query("COMMIT", 0).map_err(error)?;
365 Ok(Json::Null)
366 }
367}