1use super::ExpirationConfig;
7use crate::docmeta::{AuthCertMeta, ConsensusMeta};
8use crate::err::ReadOnlyStorageError;
9use crate::storage::{InputString, Store};
10use crate::{Error, Result};
11
12use fs_mistrust::CheckedDir;
13use tor_basic_utils::PathExt as _;
14use tor_error::{internal, into_internal, warn_report};
15use tor_netdoc::doc::authcert::AuthCertKeyIds;
16use tor_netdoc::doc::microdesc::MdDigest;
17use tor_netdoc::doc::netstatus::{ConsensusFlavor, Lifetime};
18#[cfg(feature = "routerdesc")]
19use tor_netdoc::doc::routerdesc::RdDigest;
20use web_time_compat::SystemTimeExt;
21
22#[cfg(feature = "bridge-client")]
23pub(crate) use {crate::storage::CachedBridgeDescriptor, tor_guardmgr::bridge::BridgeConfig};
24
25use std::collections::{HashMap, HashSet};
26use std::fs::OpenOptions;
27use std::path::{Path, PathBuf};
28use std::result::Result as StdResult;
29use std::sync::Arc;
30use std::time::SystemTime;
31
32use fslock_guard::LockFileGuard;
33use rusqlite::{OpenFlags, OptionalExtension, Transaction, params};
34use time::OffsetDateTime;
35use tracing::{trace, warn};
36
37enum LockFile {
42 NotLocking,
48
49 Unlocked(PathBuf),
54
55 Locked(
58 #[allow(unused)] LockFileGuard,
61 ),
62}
63
64pub(crate) struct SqliteStore {
66 conn: rusqlite::Connection,
68 sql_path: Option<PathBuf>,
70 blob_dir: CheckedDir,
72 lockfile: LockFile,
80}
81
82#[allow(unused)]
157mod blob_consistency {}
158
159#[derive(Debug)]
163enum AbsentBlob {
164 VanishedFile,
166 NothingToRead,
168}
169
170impl SqliteStore {
171 pub(crate) fn from_path_and_mistrust<P: AsRef<Path>>(
189 path: P,
190 mistrust: &fs_mistrust::Mistrust,
191 mut readonly: bool,
192 ) -> Result<Self> {
193 let path = path.as_ref();
194 let sqlpath = path.join("dir.sqlite3");
195 let blobpath = path.join("dir_blobs/");
196 let lockpath = path.join("dir.lock");
197
198 let verifier = mistrust.verifier().permit_readable().check_content();
199
200 let blob_dir = if readonly {
201 verifier.secure_dir(blobpath)?
202 } else {
203 verifier.make_secure_dir(blobpath)?
204 };
205
206 for p in [&lockpath, &sqlpath] {
209 match mistrust
210 .verifier()
211 .permit_readable()
212 .require_file()
213 .check(p)
214 {
215 Ok(()) | Err(fs_mistrust::Error::NotFound(_)) => {}
216 Err(e) => return Err(e.into()),
217 }
218 }
219
220 let lockfile = if !readonly {
221 match LockFileGuard::try_lock(&lockpath).map_err(Error::from_lockfile)? {
222 Some(guard) => LockFile::Locked(guard),
223 None => {
224 readonly = true;
226 LockFile::Unlocked(lockpath)
227 }
228 }
229 } else {
230 LockFile::Unlocked(lockpath)
231 };
232
233 let flags = if readonly {
234 OpenFlags::SQLITE_OPEN_READ_ONLY
235 } else {
236 OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE
237 };
238 let conn = rusqlite::Connection::open_with_flags(&sqlpath, flags)?;
239 let mut store = SqliteStore::from_conn_internal(conn, blob_dir, readonly)?;
240 store.sql_path = Some(sqlpath);
241 store.lockfile = lockfile;
242 Ok(store)
243 }
244
245 #[cfg(test)]
253 fn from_conn(conn: rusqlite::Connection, blob_dir: CheckedDir) -> Result<Self> {
254 Self::from_conn_internal(conn, blob_dir, false)
255 }
256
257 fn from_conn_internal(
262 conn: rusqlite::Connection,
263 blob_dir: CheckedDir,
264 readonly: bool,
265 ) -> Result<Self> {
266 conn.pragma_update(None, "foreign_keys", "ON")?;
269
270 let mut result = SqliteStore {
271 conn,
272 blob_dir,
273 lockfile: LockFile::NotLocking,
274 sql_path: None,
275 };
276
277 result.check_schema(readonly)?;
278
279 Ok(result)
280 }
281
282 fn check_schema(&mut self, readonly: bool) -> Result<()> {
285 let tx = self.conn.transaction()?;
286 let db_n_tables: u32 = tx.query_row(
287 "SELECT COUNT(name) FROM sqlite_master
288 WHERE type='table'
289 AND name NOT LIKE 'sqlite_%'",
290 [],
291 |row| row.get(0),
292 )?;
293 let db_exists = db_n_tables > 0;
294
295 let update_schema = |tx: &rusqlite::Transaction, current_vsn| {
297 for (from_vsn, update) in UPDATE_SCHEMA.iter().enumerate() {
298 let from_vsn = u32::try_from(from_vsn).expect("schema version >2^32");
299 let new_vsn = from_vsn + 1;
300 if current_vsn < new_vsn {
301 tx.execute_batch(update)?;
302 tx.execute(UPDATE_SCHEMA_VERSION, params![new_vsn, new_vsn])?;
303 }
304 }
305 Ok::<_, Error>(())
306 };
307
308 if !db_exists {
309 if !readonly {
310 tx.execute_batch(INSTALL_V0_SCHEMA)?;
311 update_schema(&tx, 0)?;
312 tx.commit()?;
313 } else {
314 return Err(Error::ReadOnlyStorage(ReadOnlyStorageError::NoDatabase));
316 }
317 return Ok(());
318 }
319
320 let (version, readable_by): (u32, u32) = tx.query_row(
321 "SELECT version, readable_by FROM TorSchemaMeta
322 WHERE name = 'TorDirStorage'",
323 [],
324 |row| Ok((row.get(0)?, row.get(1)?)),
325 )?;
326
327 if version < SCHEMA_VERSION {
328 if !readonly {
329 update_schema(&tx, version)?;
330 tx.commit()?;
331 } else {
332 return Err(Error::ReadOnlyStorage(
333 ReadOnlyStorageError::IncompatibleSchema {
334 schema: version,
335 supported: SCHEMA_VERSION,
336 },
337 ));
338 }
339
340 return Ok(());
341 } else if readable_by > SCHEMA_VERSION {
342 return Err(Error::UnrecognizedSchema {
343 schema: readable_by,
344 supported: SCHEMA_VERSION,
345 });
346 }
347
348 Ok(())
350 }
351
352 fn read_blob(&self, path: &str) -> Result<StdResult<InputString, AbsentBlob>> {
359 let file = match self.blob_dir.open(path, OpenOptions::new().read(true)) {
360 Ok(file) => file,
361 Err(fs_mistrust::Error::NotFound(_)) => {
362 warn!(
363 "{:?} was listed in the database, but its corresponding file had been deleted",
364 path
365 );
366 return Ok(Err(AbsentBlob::VanishedFile));
367 }
368 Err(e) => return Err(e.into()),
369 };
370
371 InputString::load(file)
372 .map_err(|err| Error::CacheFile {
373 action: "loading",
374 fname: PathBuf::from(path),
375 error: Arc::new(err),
376 })
377 .map(Ok)
378 }
379
380 fn save_blob_internal(
387 &mut self,
388 contents: &[u8],
389 doctype: &str,
390 digest_type: &str,
391 digest: &[u8],
392 expires: OffsetDateTime,
393 ) -> Result<blob_handle::SavedBlobHandle<'_>> {
394 let digest = hex::encode(digest);
395 let digeststr = format!("{}-{}", digest_type, digest);
396 let fname = format!("{}_{}", doctype, digeststr);
397
398 let full_path = self.blob_dir.join(&fname)?;
399 let unlinker = blob_handle::Unlinker::new(&full_path);
400 self.blob_dir
401 .write_and_replace(&fname, contents)
402 .map_err(|e| match e {
403 fs_mistrust::Error::Io { err, .. } => Error::CacheFile {
404 action: "saving",
405 fname: full_path,
406 error: err,
407 },
408 err => err.into(),
409 })?;
410
411 let tx = self.conn.unchecked_transaction()?;
412 tx.execute(INSERT_EXTDOC, params![digeststr, expires, doctype, fname])?;
413
414 Ok(blob_handle::SavedBlobHandle::new(
415 tx, fname, digeststr, unlinker,
416 ))
417 }
418
419 fn latest_consensus_internal(
421 &self,
422 flavor: ConsensusFlavor,
423 pending: Option<bool>,
424 ) -> Result<StdResult<InputString, AbsentBlob>> {
425 trace!(?flavor, ?pending, "Loading latest consensus from cache");
426 let rv: Option<(OffsetDateTime, OffsetDateTime, String)> = match pending {
427 None => self
428 .conn
429 .query_row(FIND_CONSENSUS, params![flavor.name()], |row| row.try_into())
430 .optional()?,
431 Some(pending_val) => self
432 .conn
433 .query_row(
434 FIND_CONSENSUS_P,
435 params![pending_val, flavor.name()],
436 |row| row.try_into(),
437 )
438 .optional()?,
439 };
440
441 if let Some((_va, _vu, filename)) = rv {
442 self.read_blob(&filename)
445 } else {
446 Ok(Err(AbsentBlob::NothingToRead))
447 }
448 }
449
450 #[cfg(test)]
452 fn save_blob(
453 &mut self,
454 contents: &[u8],
455 doctype: &str,
456 digest_type: &str,
457 digest: &[u8],
458 expires: OffsetDateTime,
459 ) -> Result<String> {
460 let h = self.save_blob_internal(contents, doctype, digest_type, digest, expires)?;
461 let fname = h.fname().to_string();
462 h.commit()?;
463 Ok(fname)
464 }
465
466 #[cfg(test)]
468 fn latest_consensus_time(&self, flavor: ConsensusFlavor) -> Result<Option<OffsetDateTime>> {
470 Ok(self
471 .latest_consensus_meta(flavor)?
472 .map(|m| m.lifetime().valid_after().into()))
473 }
474
475 fn remove_blob_or_warn<P: AsRef<Path>>(&self, fname: P) {
480 let fname = fname.as_ref();
481 if let Err(e) = self.blob_dir.remove_file(fname) {
482 warn_report!(e, "Unable to remove {}", fname.display_lossy());
483 }
484 }
485
486 fn remove_unreferenced_blobs(
491 &self,
492 now: OffsetDateTime,
493 expiration: &ExpirationConfig,
494 ) -> Result<()> {
495 for ent in self.blob_dir.read_directory(".")?.flatten() {
497 let md_error = |io_error| Error::CacheFile {
498 action: "getting metadata",
499 fname: ent.file_name().into(),
500 error: Arc::new(io_error),
501 };
502 if ent
503 .metadata()
504 .map_err(md_error)?
505 .modified()
506 .map_err(md_error)?
507 + expiration.consensuses
508 >= now
509 {
510 continue;
512 }
513 let filename = match ent.file_name().into_string() {
514 Ok(s) => s,
515 Err(os_str) => {
516 warn!(
518 "Removing bizarre file '{}' from blob store.",
519 os_str.to_string_lossy()
520 );
521 self.remove_blob_or_warn(ent.file_name());
522 continue;
523 }
524 };
525 let found: (u32,) =
526 self.conn
527 .query_row(COUNT_EXTDOC_BY_PATH, params![&filename], |row| {
528 row.try_into()
529 })?;
530 if found == (0,) {
531 warn!("Removing unreferenced file '{}' from blob store", &filename);
532 self.remove_blob_or_warn(ent.file_name());
533 }
534 }
535
536 Ok(())
537 }
538
539 fn remove_entries_for_vanished_blobs<'a>(
544 blob_dir: &CheckedDir,
545 tx: &Transaction<'a>,
546 ) -> Result<usize> {
547 let in_directory: HashSet<PathBuf> = blob_dir
548 .read_directory(".")?
549 .flatten()
550 .map(|dir_entry| PathBuf::from(dir_entry.file_name()))
551 .collect();
552 let in_db: Vec<String> = tx
553 .prepare(FIND_ALL_EXTDOC_FILENAMES)?
554 .query_map([], |row| row.get::<_, String>(0))?
555 .collect::<StdResult<Vec<String>, _>>()?;
556
557 let mut n_removed = 0;
558 for fname in in_db {
559 if in_directory.contains(Path::new(&fname)) {
560 continue;
562 }
563
564 n_removed += tx.execute(DELETE_EXTDOC_BY_FILENAME, [fname])?;
565 }
566
567 Ok(n_removed)
568 }
569}
570
571impl Store for SqliteStore {
572 fn is_readonly(&self) -> bool {
573 match &self.lockfile {
574 LockFile::NotLocking => false, LockFile::Unlocked(_) => true, LockFile::Locked(_) => false, }
578 }
579
580 fn upgrade_to_readwrite(&mut self) -> Result<bool> {
581 let Some(sql_path) = self.sql_path.as_ref() else {
582 return Ok(true);
584 };
585
586 let lockpath = match &self.lockfile {
587 LockFile::NotLocking => {
588 return Err(
590 internal!("No lockfile open; cannot upgrade to read-write storage").into(),
591 );
592 }
593 LockFile::Locked(_) => return Ok(true),
594 LockFile::Unlocked(path) => path,
595 };
596 let Some(guard) = LockFileGuard::try_lock(lockpath).map_err(Error::from_lockfile)? else {
598 return Ok(false);
600 };
601
602 let new_conn = rusqlite::Connection::open(sql_path)?;
605 self.conn = new_conn;
606 self.lockfile = LockFile::Locked(guard);
607 Ok(true)
608 }
609 fn expire_all(&mut self, expiration: &ExpirationConfig) -> Result<()> {
610 let tx = self.conn.transaction()?;
611 #[allow(clippy::let_and_return)]
614 let expired_blobs: Vec<String> = {
615 let mut stmt = tx.prepare(FIND_EXPIRED_EXTDOCS)?;
616 let names: Vec<String> = stmt
617 .query_map([], |row| row.get::<_, String>(0))?
618 .collect::<StdResult<Vec<String>, _>>()?;
619 names
620 };
621
622 let now = now_utc();
623 tx.execute(DROP_OLD_EXTDOCS, [])?;
624
625 tx.execute(DROP_OLD_MICRODESCS, [now - expiration.microdescs])?;
629 tx.execute(DROP_OLD_AUTHCERTS, [now - expiration.authcerts])?;
630 tx.execute(DROP_OLD_CONSENSUSES, [now - expiration.consensuses])?;
631 tx.execute(DROP_OLD_ROUTERDESCS, [now - expiration.router_descs])?;
632
633 #[cfg(feature = "bridge-client")]
637 tx.execute(DROP_OLD_BRIDGEDESCS, [now, now])?;
638
639 let remove_consensus_blobs = {
642 let mut stmt = tx.prepare(FIND_UNREFERENCED_CONSENSUS_EXTDOCS)?;
645 let filenames: Vec<String> = stmt
646 .query_map([], |row| row.get::<_, String>(0))?
647 .collect::<StdResult<Vec<String>, _>>()?;
648 drop(stmt);
649 let mut stmt = tx.prepare(DELETE_EXTDOC_BY_FILENAME)?;
650 for fname in filenames.iter() {
651 stmt.execute([fname])?;
652 }
653 filenames
654 };
655
656 tx.commit()?;
657 let mut remove_blob_files: HashSet<_> = expired_blobs.iter().collect();
660 remove_blob_files.extend(remove_consensus_blobs.iter());
661
662 for name in remove_blob_files {
663 let fname = self.blob_dir.join(name);
664 if let Ok(fname) = fname {
665 if let Err(e) = std::fs::remove_file(&fname) {
666 warn_report!(
667 e,
668 "Couldn't remove orphaned blob file {}",
669 fname.display_lossy()
670 );
671 }
672 }
673 }
674
675 self.remove_unreferenced_blobs(now, expiration)?;
676
677 Ok(())
678 }
679
680 fn latest_consensus(
682 &self,
683 flavor: ConsensusFlavor,
684 pending: Option<bool>,
685 ) -> Result<Option<InputString>> {
686 match self.latest_consensus_internal(flavor, pending)? {
687 Ok(s) => return Ok(Some(s)),
688 Err(AbsentBlob::NothingToRead) => return Ok(None),
689 Err(AbsentBlob::VanishedFile) => {
690 }
692 }
693
694 static_assertions::assert_not_impl_any!(SqliteStore: Sync);
702
703 let tx = self.conn.unchecked_transaction()?;
709 Self::remove_entries_for_vanished_blobs(&self.blob_dir, &tx)?;
710 tx.commit()?;
711
712 match self.latest_consensus_internal(flavor, pending)? {
713 Ok(s) => Ok(Some(s)),
714 Err(AbsentBlob::NothingToRead) => Ok(None),
715 Err(AbsentBlob::VanishedFile) => {
716 warn!("Somehow remove_entries_for_vanished_blobs didn't resolve a VanishedFile");
717 Ok(None)
718 }
719 }
720 }
721
722 fn latest_consensus_meta(&self, flavor: ConsensusFlavor) -> Result<Option<ConsensusMeta>> {
723 let mut stmt = self.conn.prepare(FIND_LATEST_CONSENSUS_META)?;
724 let mut rows = stmt.query(params![flavor.name()])?;
725 if let Some(row) = rows.next()? {
726 Ok(Some(cmeta_from_row(row)?))
727 } else {
728 Ok(None)
729 }
730 }
731 #[cfg(test)]
732 fn consensus_by_meta(&self, cmeta: &ConsensusMeta) -> Result<InputString> {
733 if let Some((text, _)) =
734 self.consensus_by_sha3_digest_of_signed_part(cmeta.sha3_256_of_signed())?
735 {
736 Ok(text)
737 } else {
738 Err(Error::CacheCorruption(
739 "couldn't find a consensus we thought we had.",
740 ))
741 }
742 }
743 fn consensus_by_sha3_digest_of_signed_part(
744 &self,
745 d: &[u8; 32],
746 ) -> Result<Option<(InputString, ConsensusMeta)>> {
747 let digest = hex::encode(d);
748 let mut stmt = self
749 .conn
750 .prepare(FIND_CONSENSUS_AND_META_BY_DIGEST_OF_SIGNED)?;
751 let mut rows = stmt.query(params![digest])?;
752 if let Some(row) = rows.next()? {
753 let meta = cmeta_from_row(row)?;
754 let fname: String = row.get(5)?;
755 if let Ok(text) = self.read_blob(&fname)? {
756 return Ok(Some((text, meta)));
757 }
758 }
759 Ok(None)
760 }
761 fn store_consensus(
762 &mut self,
763 cmeta: &ConsensusMeta,
764 flavor: ConsensusFlavor,
765 pending: bool,
766 contents: &str,
767 ) -> Result<()> {
768 let lifetime = cmeta.lifetime();
769 let sha3_of_signed = cmeta.sha3_256_of_signed();
770 let sha3_of_whole = cmeta.sha3_256_of_whole();
771 let valid_after: OffsetDateTime = lifetime.valid_after().into();
772 let fresh_until: OffsetDateTime = lifetime.fresh_until().into();
773 let valid_until: OffsetDateTime = lifetime.valid_until().into();
774
775 const CONSENSUS_LIFETIME: time::Duration = time::Duration::days(4);
777
778 let expires = valid_until + CONSENSUS_LIFETIME;
781
782 let doctype = format!("con_{}", flavor.name());
783
784 let h = self.save_blob_internal(
785 contents.as_bytes(),
786 &doctype,
787 "sha3-256",
788 &sha3_of_whole[..],
789 expires,
790 )?;
791 h.tx().execute(
792 INSERT_CONSENSUS,
793 params![
794 valid_after,
795 fresh_until,
796 valid_until,
797 flavor.name(),
798 pending,
799 hex::encode(sha3_of_signed),
800 h.digest_string()
801 ],
802 )?;
803 h.commit()?;
804 Ok(())
805 }
806 fn mark_consensus_usable(&mut self, cmeta: &ConsensusMeta) -> Result<()> {
807 let d = hex::encode(cmeta.sha3_256_of_whole());
808 let digest = format!("sha3-256-{}", d);
809
810 let tx = self.conn.transaction()?;
811 let n = tx.execute(MARK_CONSENSUS_NON_PENDING, params![digest])?;
812 trace!("Marked {} consensuses usable", n);
813 tx.commit()?;
814
815 Ok(())
816 }
817 fn delete_consensus(&mut self, cmeta: &ConsensusMeta) -> Result<()> {
818 let d = hex::encode(cmeta.sha3_256_of_whole());
819 let digest = format!("sha3-256-{}", d);
820
821 let tx = self.conn.transaction()?;
824 tx.execute(REMOVE_CONSENSUS, params![digest])?;
825 tx.commit()?;
826
827 Ok(())
828 }
829
830 fn authcerts(&self, certs: &[AuthCertKeyIds]) -> Result<HashMap<AuthCertKeyIds, String>> {
831 let mut result = HashMap::new();
832 let mut stmt = self.conn.prepare(FIND_AUTHCERT)?;
834
835 for ids in certs {
836 let id_digest = hex::encode(ids.id_fingerprint.as_bytes());
837 let sk_digest = hex::encode(ids.sk_fingerprint.as_bytes());
838 if let Some(contents) = stmt
839 .query_row(params![id_digest, sk_digest], |row| row.get::<_, String>(0))
840 .optional()?
841 {
842 result.insert(*ids, contents);
843 }
844 }
845
846 Ok(result)
847 }
848 fn store_authcerts(&mut self, certs: &[(AuthCertMeta, &str)]) -> Result<()> {
849 let tx = self.conn.transaction()?;
850 let mut stmt = tx.prepare(INSERT_AUTHCERT)?;
851 for (meta, content) in certs {
852 let ids = meta.key_ids();
853 let id_digest = hex::encode(ids.id_fingerprint.as_bytes());
854 let sk_digest = hex::encode(ids.sk_fingerprint.as_bytes());
855 let published: OffsetDateTime = meta.published().into();
856 let expires: OffsetDateTime = meta.expires().into();
857 stmt.execute(params![id_digest, sk_digest, published, expires, content])?;
858 }
859 stmt.finalize()?;
860 tx.commit()?;
861 Ok(())
862 }
863
864 fn microdescs(&self, digests: &[MdDigest]) -> Result<HashMap<MdDigest, String>> {
865 let mut result = HashMap::new();
866 let mut stmt = self.conn.prepare(FIND_MD)?;
867
868 for md_digest in digests {
871 let h_digest = hex::encode(md_digest);
872 if let Some(contents) = stmt
873 .query_row(params![h_digest], |row| row.get::<_, String>(0))
874 .optional()?
875 {
876 result.insert(*md_digest, contents);
877 }
878 }
879
880 Ok(result)
881 }
882 fn store_microdescs(&mut self, digests: &[(&str, &MdDigest)], when: SystemTime) -> Result<()> {
883 let when: OffsetDateTime = when.into();
884
885 let tx = self.conn.transaction()?;
886 let mut stmt = tx.prepare(INSERT_MD)?;
887
888 for (content, md_digest) in digests {
889 let h_digest = hex::encode(md_digest);
890 stmt.execute(params![h_digest, when, content])?;
891 }
892 stmt.finalize()?;
893 tx.commit()?;
894 Ok(())
895 }
896 fn update_microdescs_listed(&mut self, digests: &[MdDigest], when: SystemTime) -> Result<()> {
897 let tx = self.conn.transaction()?;
898 let mut stmt = tx.prepare(UPDATE_MD_LISTED)?;
899 let when: OffsetDateTime = when.into();
900
901 for md_digest in digests {
902 let h_digest = hex::encode(md_digest);
903 stmt.execute(params![when, h_digest])?;
904 }
905
906 stmt.finalize()?;
907 tx.commit()?;
908 Ok(())
909 }
910
911 #[cfg(feature = "routerdesc")]
912 fn routerdescs(&self, digests: &[RdDigest]) -> Result<HashMap<RdDigest, String>> {
913 let mut result = HashMap::new();
914 let mut stmt = self.conn.prepare(FIND_RD)?;
915
916 for rd_digest in digests {
919 let h_digest = hex::encode(rd_digest);
920 if let Some(contents) = stmt
921 .query_row(params![h_digest], |row| row.get::<_, String>(0))
922 .optional()?
923 {
924 result.insert(*rd_digest, contents);
925 }
926 }
927
928 Ok(result)
929 }
930 #[cfg(feature = "routerdesc")]
931 fn store_routerdescs(&mut self, digests: &[(&str, SystemTime, &RdDigest)]) -> Result<()> {
932 let tx = self.conn.transaction()?;
933 let mut stmt = tx.prepare(INSERT_RD)?;
934
935 for (content, when, rd_digest) in digests {
936 let when: OffsetDateTime = (*when).into();
937 let h_digest = hex::encode(rd_digest);
938 stmt.execute(params![h_digest, when, content])?;
939 }
940 stmt.finalize()?;
941 tx.commit()?;
942 Ok(())
943 }
944
945 #[cfg(feature = "bridge-client")]
946 fn lookup_bridgedesc(&self, bridge: &BridgeConfig) -> Result<Option<CachedBridgeDescriptor>> {
947 let bridge_line = bridge.to_string();
948 Ok(self
949 .conn
950 .query_row(FIND_BRIDGEDESC, params![bridge_line], |row| {
951 let (fetched, document): (OffsetDateTime, _) = row.try_into()?;
952 let fetched = fetched.into();
953 Ok(CachedBridgeDescriptor { fetched, document })
954 })
955 .optional()?)
956 }
957
958 #[cfg(feature = "bridge-client")]
959 fn store_bridgedesc(
960 &mut self,
961 bridge: &BridgeConfig,
962 entry: CachedBridgeDescriptor,
963 until: SystemTime,
964 ) -> Result<()> {
965 if self.is_readonly() {
966 return Ok(());
970 }
971 let bridge_line = bridge.to_string();
972 let row = params![
973 bridge_line,
974 OffsetDateTime::from(entry.fetched),
975 OffsetDateTime::from(until),
976 entry.document,
977 ];
978 self.conn.execute(INSERT_BRIDGEDESC, row)?;
979 Ok(())
980 }
981
982 #[cfg(feature = "bridge-client")]
983 fn delete_bridgedesc(&mut self, bridge: &BridgeConfig) -> Result<()> {
984 if self.is_readonly() {
985 return Ok(());
989 }
990 let bridge_line = bridge.to_string();
991 self.conn.execute(DELETE_BRIDGEDESC, params![bridge_line])?;
992 Ok(())
993 }
994
995 fn update_protocol_recommendations(
996 &mut self,
997 valid_after: SystemTime,
998 protocols: &tor_netdoc::doc::netstatus::ProtoStatuses,
999 ) -> Result<()> {
1000 let json =
1001 serde_json::to_string(&protocols).map_err(into_internal!("Cannot encode protocols"))?;
1002 let params = params![OffsetDateTime::from(valid_after), json];
1003 self.conn.execute(UPDATE_PROTOCOL_STATUS, params)?;
1004 Ok(())
1005 }
1006
1007 fn cached_protocol_recommendations(
1008 &self,
1009 ) -> Result<Option<(SystemTime, tor_netdoc::doc::netstatus::ProtoStatuses)>> {
1010 let opt_row: Option<(OffsetDateTime, String)> = self
1011 .conn
1012 .query_row(FIND_LATEST_PROTOCOL_STATUS, [], |row| {
1013 Ok((row.get(0)?, row.get(1)?))
1014 })
1015 .optional()?;
1016
1017 let (date, json) = match opt_row {
1018 Some(v) => v,
1019 None => return Ok(None),
1020 };
1021
1022 let date = date.into();
1023 let statuses: tor_netdoc::doc::netstatus::ProtoStatuses =
1024 serde_json::from_str(json.as_str()).map_err(|e| Error::BadJsonInCache(Arc::new(e)))?;
1025
1026 Ok(Some((date, statuses)))
1027 }
1028}
1029
1030mod blob_handle {
1032 use std::path::{Path, PathBuf};
1033
1034 use crate::Result;
1035 use rusqlite::Transaction;
1036 use tor_basic_utils::PathExt as _;
1037 use tor_error::warn_report;
1038
1039 #[must_use]
1046 pub(super) struct SavedBlobHandle<'a> {
1047 tx: Transaction<'a>,
1054 fname: String,
1056 digeststr: String,
1059 unlinker: Unlinker,
1061 }
1062
1063 impl<'a> SavedBlobHandle<'a> {
1064 pub(super) fn new(
1066 tx: Transaction<'a>,
1067 fname: String,
1068 digeststr: String,
1069 unlinker: Unlinker,
1070 ) -> Self {
1071 Self {
1072 tx,
1073 fname,
1074 digeststr,
1075 unlinker,
1076 }
1077 }
1078
1079 pub(super) fn tx(&self) -> &Transaction<'a> {
1081 &self.tx
1082 }
1083 pub(super) fn digest_string(&self) -> &str {
1086 self.digeststr.as_ref()
1087 }
1088 #[allow(unused)] pub(super) fn fname(&self) -> &str {
1091 self.fname.as_ref()
1092 }
1093 pub(super) fn commit(self) -> Result<()> {
1095 self.tx.commit()?;
1101 self.unlinker.forget();
1103 Ok(())
1104 }
1105 }
1106
1107 pub(super) struct Unlinker {
1112 p: Option<PathBuf>,
1115 }
1116 impl Unlinker {
1117 pub(super) fn new<P: AsRef<Path>>(p: P) -> Self {
1119 Unlinker {
1120 p: Some(p.as_ref().to_path_buf()),
1121 }
1122 }
1123 fn forget(mut self) {
1126 self.p = None;
1127 }
1128 }
1129 impl Drop for Unlinker {
1130 fn drop(&mut self) {
1131 if let Some(p) = self.p.take() {
1132 if let Err(e) = std::fs::remove_file(&p) {
1133 warn_report!(
1134 e,
1135 "Couldn't remove rolled-back blob file {}",
1136 p.display_lossy()
1137 );
1138 }
1139 }
1140 }
1141 }
1142}
1143
1144fn digest_from_hex(s: &str) -> Result<[u8; 32]> {
1146 let mut bytes = [0_u8; 32];
1147 hex::decode_to_slice(s, &mut bytes[..]).map_err(Error::BadHexInCache)?;
1148 Ok(bytes)
1149}
1150
1151fn digest_from_dstr(s: &str) -> Result<[u8; 32]> {
1154 if let Some(stripped) = s.strip_prefix("sha3-256-") {
1155 digest_from_hex(stripped)
1156 } else {
1157 Err(Error::CacheCorruption("Invalid digest in database"))
1158 }
1159}
1160
1161fn cmeta_from_row(row: &rusqlite::Row<'_>) -> Result<ConsensusMeta> {
1164 let va: OffsetDateTime = row.get(0)?;
1165 let fu: OffsetDateTime = row.get(1)?;
1166 let vu: OffsetDateTime = row.get(2)?;
1167 let d_signed: String = row.get(3)?;
1168 let d_all: String = row.get(4)?;
1169 let lifetime = Lifetime::new(va.into(), fu.into(), vu.into())
1170 .map_err(|_| Error::CacheCorruption("inconsistent lifetime in database"))?;
1171 Ok(ConsensusMeta::new(
1172 lifetime,
1173 digest_from_hex(&d_signed)?,
1174 digest_from_dstr(&d_all)?,
1175 ))
1176}
1177
1178fn now_utc() -> OffsetDateTime {
1180 SystemTime::get().into()
1181}
1182
1183const INSTALL_V0_SCHEMA: &str = "
1185 -- Helps us version the schema. The schema here corresponds to a
1186 -- version number called 'version', and it should be readable by
1187 -- anybody who is compliant with versions of at least 'readable_by'.
1188 CREATE TABLE TorSchemaMeta (
1189 name TEXT NOT NULL PRIMARY KEY,
1190 version INTEGER NOT NULL,
1191 readable_by INTEGER NOT NULL
1192 );
1193
1194 INSERT INTO TorSchemaMeta (name, version, readable_by) VALUES ( 'TorDirStorage', 0, 0 );
1195
1196 -- Keeps track of external blobs on disk.
1197 CREATE TABLE ExtDocs (
1198 -- Records a digest of the file contents, in the form '<digest_type>-hexstr'
1199 digest TEXT PRIMARY KEY NOT NULL,
1200 -- When was this file created?
1201 created DATE NOT NULL,
1202 -- After what time will this file definitely be useless?
1203 expires DATE NOT NULL,
1204 -- What is the type of this file? Currently supported are 'con_<flavor>'.
1205 -- (Before tor-dirmgr ~0.28.0, we would erroneously record 'con_flavor' as 'sha3-256';
1206 -- Nothing depended on this yet, but will be used in the future
1207 -- as we add more large-document types.)
1208 type TEXT NOT NULL,
1209 -- Filename for this file within our blob directory.
1210 filename TEXT NOT NULL
1211 );
1212
1213 -- All the microdescriptors we know about.
1214 CREATE TABLE Microdescs (
1215 sha256_digest TEXT PRIMARY KEY NOT NULL,
1216 last_listed DATE NOT NULL,
1217 contents BLOB NOT NULL
1218 );
1219
1220 -- All the authority certificates we know.
1221 CREATE TABLE Authcerts (
1222 id_digest TEXT NOT NULL,
1223 sk_digest TEXT NOT NULL,
1224 published DATE NOT NULL,
1225 expires DATE NOT NULL,
1226 contents BLOB NOT NULL,
1227 PRIMARY KEY (id_digest, sk_digest)
1228 );
1229
1230 -- All the consensuses we're storing.
1231 CREATE TABLE Consensuses (
1232 valid_after DATE NOT NULL,
1233 fresh_until DATE NOT NULL,
1234 valid_until DATE NOT NULL,
1235 flavor TEXT NOT NULL,
1236 pending BOOLEAN NOT NULL,
1237 sha3_of_signed_part TEXT NOT NULL,
1238 digest TEXT NOT NULL,
1239 FOREIGN KEY (digest) REFERENCES ExtDocs (digest) ON DELETE CASCADE
1240 );
1241 CREATE INDEX Consensuses_vu on CONSENSUSES(valid_until);
1242
1243";
1244
1245const UPDATE_SCHEMA: &[&str] = &["
1247 -- Update the database schema from version 0 to version 1.
1248 CREATE TABLE RouterDescs (
1249 sha1_digest TEXT PRIMARY KEY NOT NULL,
1250 published DATE NOT NULL,
1251 contents BLOB NOT NULL
1252 );
1253","
1254 -- Update the database schema from version 1 to version 2.
1255 -- We create this table even if the bridge-client feature is disabled, but then don't touch it at all.
1256 CREATE TABLE BridgeDescs (
1257 bridge_line TEXT PRIMARY KEY NOT NULL,
1258 fetched DATE NOT NULL,
1259 until DATE NOT NULL,
1260 contents BLOB NOT NULL
1261 );
1262","
1263 -- Update the database schema from version 2 to version 3.
1264
1265 -- Table to hold our latest ProtocolStatuses object, to tell us if we're obsolete.
1266 -- We hold this independently from our consensus,
1267 -- since we want to read it very early in our startup process,
1268 -- even if the consensus is expired.
1269 CREATE TABLE ProtocolStatus (
1270 -- Enforce that there is only one row in this table.
1271 -- (This is a bit kludgy, but I am assured that it is a common practice.)
1272 zero INTEGER PRIMARY KEY NOT NULL,
1273 -- valid-after date of the consensus from which we got this status
1274 date DATE NOT NULL,
1275 -- ProtoStatuses object, encoded as json
1276 statuses TEXT NOT NULL
1277 );
1278"];
1279
1280const UPDATE_SCHEMA_VERSION: &str = "
1282 UPDATE TorSchemaMeta SET version=? WHERE version<?;
1283";
1284
1285const SCHEMA_VERSION: u32 = UPDATE_SCHEMA.len() as u32;
1287
1288const FIND_CONSENSUS_P: &str = "
1291 SELECT valid_after, valid_until, filename
1292 FROM Consensuses
1293 INNER JOIN ExtDocs ON ExtDocs.digest = Consensuses.digest
1294 WHERE pending = ? AND flavor = ?
1295 ORDER BY valid_until DESC
1296 LIMIT 1;
1297";
1298
1299const FIND_CONSENSUS: &str = "
1302 SELECT valid_after, valid_until, filename
1303 FROM Consensuses
1304 INNER JOIN ExtDocs ON ExtDocs.digest = Consensuses.digest
1305 WHERE flavor = ?
1306 ORDER BY valid_until DESC
1307 LIMIT 1;
1308";
1309
1310const FIND_LATEST_CONSENSUS_META: &str = "
1313 SELECT valid_after, fresh_until, valid_until, sha3_of_signed_part, digest
1314 FROM Consensuses
1315 WHERE pending = 0 AND flavor = ?
1316 ORDER BY valid_until DESC
1317 LIMIT 1;
1318";
1319
1320const FIND_CONSENSUS_AND_META_BY_DIGEST_OF_SIGNED: &str = "
1322 SELECT valid_after, fresh_until, valid_until, sha3_of_signed_part, Consensuses.digest, filename
1323 FROM Consensuses
1324 INNER JOIN ExtDocs on ExtDocs.digest = Consensuses.digest
1325 WHERE Consensuses.sha3_of_signed_part = ?
1326 LIMIT 1;
1327";
1328
1329const MARK_CONSENSUS_NON_PENDING: &str = "
1332 UPDATE Consensuses
1333 SET pending = 0
1334 WHERE digest = ?;
1335";
1336
1337#[allow(dead_code)]
1339const REMOVE_CONSENSUS: &str = "
1340 DELETE FROM Consensuses
1341 WHERE digest = ?;
1342";
1343
1344const FIND_AUTHCERT: &str = "
1346 SELECT contents FROM AuthCerts WHERE id_digest = ? AND sk_digest = ?;
1347";
1348
1349const FIND_MD: &str = "
1351 SELECT contents
1352 FROM Microdescs
1353 WHERE sha256_digest = ?
1354";
1355
1356#[cfg(feature = "routerdesc")]
1358const FIND_RD: &str = "
1359 SELECT contents
1360 FROM RouterDescs
1361 WHERE sha1_digest = ?
1362";
1363
1364const FIND_EXPIRED_EXTDOCS: &str = "
1366 SELECT filename FROM ExtDocs where expires < datetime('now');
1367";
1368
1369const COUNT_EXTDOC_BY_PATH: &str = "
1371 SELECT COUNT(*) FROM ExtDocs WHERE filename = ?;
1372";
1373
1374const INSERT_EXTDOC: &str = "
1376 INSERT OR REPLACE INTO ExtDocs ( digest, created, expires, type, filename )
1377 VALUES ( ?, datetime('now'), ?, ?, ? );
1378";
1379
1380const INSERT_CONSENSUS: &str = "
1382 INSERT OR REPLACE INTO Consensuses
1383 ( valid_after, fresh_until, valid_until, flavor, pending, sha3_of_signed_part, digest )
1384 VALUES ( ?, ?, ?, ?, ?, ?, ? );
1385";
1386
1387const INSERT_AUTHCERT: &str = "
1389 INSERT OR REPLACE INTO Authcerts
1390 ( id_digest, sk_digest, published, expires, contents)
1391 VALUES ( ?, ?, ?, ?, ? );
1392";
1393
1394const INSERT_MD: &str = "
1396 INSERT OR REPLACE INTO Microdescs ( sha256_digest, last_listed, contents )
1397 VALUES ( ?, ?, ? );
1398";
1399
1400#[allow(unused)]
1402#[cfg(feature = "routerdesc")]
1403const INSERT_RD: &str = "
1404 INSERT OR REPLACE INTO RouterDescs ( sha1_digest, published, contents )
1405 VALUES ( ?, ?, ? );
1406";
1407
1408const UPDATE_MD_LISTED: &str = "
1410 UPDATE Microdescs
1411 SET last_listed = max(last_listed, ?)
1412 WHERE sha256_digest = ?;
1413";
1414
1415#[cfg(feature = "bridge-client")]
1417const FIND_BRIDGEDESC: &str = "SELECT fetched, contents FROM BridgeDescs WHERE bridge_line = ?;";
1418#[cfg(feature = "bridge-client")]
1420const INSERT_BRIDGEDESC: &str = "
1421 INSERT OR REPLACE INTO BridgeDescs ( bridge_line, fetched, until, contents )
1422 VALUES ( ?, ?, ?, ? );
1423";
1424#[cfg(feature = "bridge-client")]
1426#[allow(dead_code)]
1427const DELETE_BRIDGEDESC: &str = "DELETE FROM BridgeDescs WHERE bridge_line = ?;";
1428
1429const FIND_UNREFERENCED_CONSENSUS_EXTDOCS: &str = "
1433 SELECT filename FROM ExtDocs WHERE
1434 (type LIKE 'con_%' OR type = 'sha3-256')
1435 AND NOT EXISTS
1436 (SELECT digest FROM Consensuses WHERE Consensuses.digest = ExtDocs.digest);";
1437
1438const DROP_OLD_EXTDOCS: &str = "DELETE FROM ExtDocs WHERE expires < datetime('now');";
1442
1443const DELETE_EXTDOC_BY_FILENAME: &str = "DELETE FROM ExtDocs WHERE filename = ?;";
1445
1446const FIND_ALL_EXTDOC_FILENAMES: &str = "SELECT filename FROM ExtDocs;";
1448
1449const FIND_LATEST_PROTOCOL_STATUS: &str = "SELECT date, statuses FROM ProtocolStatus WHERE zero=0;";
1451const UPDATE_PROTOCOL_STATUS: &str = "INSERT OR REPLACE INTO ProtocolStatus VALUES ( 0, ?, ? );";
1453
1454const DROP_OLD_ROUTERDESCS: &str = "DELETE FROM RouterDescs WHERE published < ?;";
1458const DROP_OLD_MICRODESCS: &str = "DELETE FROM Microdescs WHERE last_listed < ?;";
1461const DROP_OLD_AUTHCERTS: &str = "DELETE FROM Authcerts WHERE expires < ?;";
1463const DROP_OLD_CONSENSUSES: &str = "DELETE FROM Consensuses WHERE valid_until < ?;";
1466#[cfg(feature = "bridge-client")]
1468const DROP_OLD_BRIDGEDESCS: &str = "DELETE FROM BridgeDescs WHERE ? > until OR fetched > ?;";
1469
1470#[cfg(test)]
1471pub(crate) mod test {
1472 #![allow(clippy::unwrap_used)]
1473 use super::*;
1474 use crate::storage::EXPIRATION_DEFAULTS;
1475 use digest::Digest;
1476 use hex_literal::hex;
1477 use tempfile::{TempDir, tempdir};
1478 use time::ext::NumericalDuration;
1479 use tor_llcrypto::d::Sha3_256;
1480
1481 pub(crate) fn new_empty() -> Result<(TempDir, SqliteStore)> {
1482 let tmp_dir = tempdir().unwrap();
1483 let sql_path = tmp_dir.path().join("db.sql");
1484 let conn = rusqlite::Connection::open(sql_path)?;
1485 let blob_path = tmp_dir.path().join("blobs");
1486 let blob_dir = fs_mistrust::Mistrust::builder()
1487 .dangerously_trust_everyone()
1488 .build()
1489 .unwrap()
1490 .verifier()
1491 .make_secure_dir(blob_path)
1492 .unwrap();
1493 let store = SqliteStore::from_conn(conn, blob_dir)?;
1494
1495 Ok((tmp_dir, store))
1496 }
1497
1498 #[test]
1499 fn init() -> Result<()> {
1500 let tmp_dir = tempdir().unwrap();
1501 let blob_dir = fs_mistrust::Mistrust::builder()
1502 .dangerously_trust_everyone()
1503 .build()
1504 .unwrap()
1505 .verifier()
1506 .secure_dir(&tmp_dir)
1507 .unwrap();
1508 let sql_path = tmp_dir.path().join("db.sql");
1509 {
1511 let conn = rusqlite::Connection::open(&sql_path)?;
1512 let _store = SqliteStore::from_conn(conn, blob_dir.clone())?;
1513 }
1514 {
1516 let conn = rusqlite::Connection::open(&sql_path)?;
1517 let _store = SqliteStore::from_conn(conn, blob_dir.clone())?;
1518 }
1519 {
1521 let conn = rusqlite::Connection::open(&sql_path)?;
1522 conn.execute_batch("UPDATE TorSchemaMeta SET version = 9002;")?;
1523 let _store = SqliteStore::from_conn(conn, blob_dir.clone())?;
1524 }
1525 {
1527 let conn = rusqlite::Connection::open(&sql_path)?;
1528 conn.execute_batch("UPDATE TorSchemaMeta SET readable_by = 9001;")?;
1529 let val = SqliteStore::from_conn(conn, blob_dir);
1530 assert!(val.is_err());
1531 }
1532 Ok(())
1533 }
1534
1535 #[test]
1536 fn bad_blob_fname() -> Result<()> {
1537 let (_tmp_dir, store) = new_empty()?;
1538
1539 assert!(store.blob_dir.join("abcd").is_ok());
1540 assert!(store.blob_dir.join("abcd..").is_ok());
1541 assert!(store.blob_dir.join("..abcd..").is_ok());
1542 assert!(store.blob_dir.join(".abcd").is_ok());
1543
1544 assert!(store.blob_dir.join("..").is_err());
1545 assert!(store.blob_dir.join("../abcd").is_err());
1546 assert!(store.blob_dir.join("/abcd").is_err());
1547
1548 Ok(())
1549 }
1550
1551 #[test]
1552 fn blobs() -> Result<()> {
1553 let (_tmp_dir, mut store) = new_empty()?;
1554
1555 let now = now_utc();
1556 let one_week = 1.weeks();
1557
1558 let fname1 = store.save_blob(
1559 b"Hello world",
1560 "greeting",
1561 "sha1",
1562 &hex!("7b502c3a1f48c8609ae212cdfb639dee39673f5e"),
1563 now + one_week,
1564 )?;
1565
1566 let fname2 = store.save_blob(
1567 b"Goodbye, dear friends",
1568 "greeting",
1569 "sha1",
1570 &hex!("2149c2a7dbf5be2bb36fb3c5080d0fb14cb3355c"),
1571 now - one_week,
1572 )?;
1573
1574 assert_eq!(
1575 fname1,
1576 "greeting_sha1-7b502c3a1f48c8609ae212cdfb639dee39673f5e"
1577 );
1578 assert_eq!(
1579 &std::fs::read(store.blob_dir.join(&fname1)?).unwrap()[..],
1580 b"Hello world"
1581 );
1582 assert_eq!(
1583 &std::fs::read(store.blob_dir.join(&fname2)?).unwrap()[..],
1584 b"Goodbye, dear friends"
1585 );
1586
1587 let n: u32 = store
1588 .conn
1589 .query_row("SELECT COUNT(filename) FROM ExtDocs", [], |row| row.get(0))?;
1590 assert_eq!(n, 2);
1591
1592 let blob = store.read_blob(&fname2)?.unwrap();
1593 assert_eq!(blob.as_str().unwrap(), "Goodbye, dear friends");
1594
1595 store.expire_all(&EXPIRATION_DEFAULTS)?;
1597 assert_eq!(
1598 &std::fs::read(store.blob_dir.join(&fname1)?).unwrap()[..],
1599 b"Hello world"
1600 );
1601 assert!(std::fs::read(store.blob_dir.join(&fname2)?).is_err());
1602 let n: u32 = store
1603 .conn
1604 .query_row("SELECT COUNT(filename) FROM ExtDocs", [], |row| row.get(0))?;
1605 assert_eq!(n, 1);
1606
1607 Ok(())
1608 }
1609
1610 #[test]
1611 fn consensus() -> Result<()> {
1612 use tor_netdoc::doc::netstatus;
1613
1614 let (_tmp_dir, mut store) = new_empty()?;
1615 let now = now_utc();
1616 let one_hour = 1.hours();
1617
1618 assert_eq!(
1619 store.latest_consensus_time(ConsensusFlavor::Microdesc)?,
1620 None
1621 );
1622
1623 let cmeta = ConsensusMeta::new(
1624 netstatus::Lifetime::new(
1625 now.into(),
1626 (now + one_hour).into(),
1627 SystemTime::from(now + one_hour * 2),
1628 )
1629 .unwrap(),
1630 [0xAB; 32],
1631 [0xBC; 32],
1632 );
1633
1634 store.store_consensus(
1635 &cmeta,
1636 ConsensusFlavor::Microdesc,
1637 true,
1638 "Pretend this is a consensus",
1639 )?;
1640
1641 {
1642 assert_eq!(
1643 store.latest_consensus_time(ConsensusFlavor::Microdesc)?,
1644 None
1645 );
1646 let consensus = store
1647 .latest_consensus(ConsensusFlavor::Microdesc, None)?
1648 .unwrap();
1649 assert_eq!(consensus.as_str()?, "Pretend this is a consensus");
1650 let consensus = store.latest_consensus(ConsensusFlavor::Microdesc, Some(false))?;
1651 assert!(consensus.is_none());
1652 }
1653
1654 store.mark_consensus_usable(&cmeta)?;
1655
1656 {
1657 assert_eq!(
1658 store.latest_consensus_time(ConsensusFlavor::Microdesc)?,
1659 now.into()
1660 );
1661 let consensus = store
1662 .latest_consensus(ConsensusFlavor::Microdesc, None)?
1663 .unwrap();
1664 assert_eq!(consensus.as_str()?, "Pretend this is a consensus");
1665 let consensus = store
1666 .latest_consensus(ConsensusFlavor::Microdesc, Some(false))?
1667 .unwrap();
1668 assert_eq!(consensus.as_str()?, "Pretend this is a consensus");
1669 }
1670
1671 {
1672 let consensus_text = store.consensus_by_meta(&cmeta)?;
1673 assert_eq!(consensus_text.as_str()?, "Pretend this is a consensus");
1674
1675 let (is, _cmeta2) = store
1676 .consensus_by_sha3_digest_of_signed_part(&[0xAB; 32])?
1677 .unwrap();
1678 assert_eq!(is.as_str()?, "Pretend this is a consensus");
1679
1680 let cmeta3 = ConsensusMeta::new(
1681 netstatus::Lifetime::new(
1682 now.into(),
1683 (now + one_hour).into(),
1684 SystemTime::from(now + one_hour * 2),
1685 )
1686 .unwrap(),
1687 [0x99; 32],
1688 [0x99; 32],
1689 );
1690 assert!(store.consensus_by_meta(&cmeta3).is_err());
1691
1692 assert!(
1693 store
1694 .consensus_by_sha3_digest_of_signed_part(&[0x99; 32])?
1695 .is_none()
1696 );
1697 }
1698
1699 {
1700 assert!(
1701 store
1702 .consensus_by_sha3_digest_of_signed_part(&[0xAB; 32])?
1703 .is_some()
1704 );
1705 store.delete_consensus(&cmeta)?;
1706 assert!(
1707 store
1708 .consensus_by_sha3_digest_of_signed_part(&[0xAB; 32])?
1709 .is_none()
1710 );
1711 }
1712
1713 Ok(())
1714 }
1715
1716 #[test]
1717 fn authcerts() -> Result<()> {
1718 let (_tmp_dir, mut store) = new_empty()?;
1719 let now = now_utc();
1720 let one_hour = 1.hours();
1721
1722 let keyids = AuthCertKeyIds {
1723 id_fingerprint: [3; 20].into(),
1724 sk_fingerprint: [4; 20].into(),
1725 };
1726 let keyids2 = AuthCertKeyIds {
1727 id_fingerprint: [4; 20].into(),
1728 sk_fingerprint: [3; 20].into(),
1729 };
1730
1731 let m1 = AuthCertMeta::new(keyids, now.into(), SystemTime::from(now + one_hour * 24));
1732
1733 store.store_authcerts(&[(m1, "Pretend this is a cert")])?;
1734
1735 let certs = store.authcerts(&[keyids, keyids2])?;
1736 assert_eq!(certs.len(), 1);
1737 assert_eq!(certs.get(&keyids).unwrap(), "Pretend this is a cert");
1738
1739 Ok(())
1740 }
1741
1742 #[test]
1743 fn microdescs() -> Result<()> {
1744 let (_tmp_dir, mut store) = new_empty()?;
1745
1746 let now = now_utc();
1747 let one_day = 1.days();
1748
1749 let d1 = [5_u8; 32];
1750 let d2 = [7; 32];
1751 let d3 = [42; 32];
1752 let d4 = [99; 32];
1753
1754 let long_ago: OffsetDateTime = now - one_day * 100;
1755 store.store_microdescs(
1756 &[
1757 ("Fake micro 1", &d1),
1758 ("Fake micro 2", &d2),
1759 ("Fake micro 3", &d3),
1760 ],
1761 long_ago.into(),
1762 )?;
1763
1764 store.update_microdescs_listed(&[d2], now.into())?;
1765
1766 let mds = store.microdescs(&[d2, d3, d4])?;
1767 assert_eq!(mds.len(), 2);
1768 assert_eq!(mds.get(&d1), None);
1769 assert_eq!(mds.get(&d2).unwrap(), "Fake micro 2");
1770 assert_eq!(mds.get(&d3).unwrap(), "Fake micro 3");
1771 assert_eq!(mds.get(&d4), None);
1772
1773 store.expire_all(&EXPIRATION_DEFAULTS)?;
1775 let mds = store.microdescs(&[d2, d3, d4])?;
1776 assert_eq!(mds.len(), 1);
1777 assert_eq!(mds.get(&d2).unwrap(), "Fake micro 2");
1778
1779 Ok(())
1780 }
1781
1782 #[test]
1783 #[cfg(feature = "routerdesc")]
1784 fn routerdescs() -> Result<()> {
1785 let (_tmp_dir, mut store) = new_empty()?;
1786
1787 let now = now_utc();
1788 let one_day = 1.days();
1789 let long_ago: OffsetDateTime = now - one_day * 100;
1790 let recently = now - one_day;
1791
1792 let d1 = [5_u8; 20];
1793 let d2 = [7; 20];
1794 let d3 = [42; 20];
1795 let d4 = [99; 20];
1796
1797 store.store_routerdescs(&[
1798 ("Fake routerdesc 1", long_ago.into(), &d1),
1799 ("Fake routerdesc 2", recently.into(), &d2),
1800 ("Fake routerdesc 3", long_ago.into(), &d3),
1801 ])?;
1802
1803 let rds = store.routerdescs(&[d2, d3, d4])?;
1804 assert_eq!(rds.len(), 2);
1805 assert_eq!(rds.get(&d1), None);
1806 assert_eq!(rds.get(&d2).unwrap(), "Fake routerdesc 2");
1807 assert_eq!(rds.get(&d3).unwrap(), "Fake routerdesc 3");
1808 assert_eq!(rds.get(&d4), None);
1809
1810 store.expire_all(&EXPIRATION_DEFAULTS)?;
1812 let rds = store.routerdescs(&[d2, d3, d4])?;
1813 assert_eq!(rds.len(), 1);
1814 assert_eq!(rds.get(&d2).unwrap(), "Fake routerdesc 2");
1815
1816 Ok(())
1817 }
1818
1819 #[test]
1820 fn from_path_rw() -> Result<()> {
1821 let tmp = tempdir().unwrap();
1822 let mistrust = fs_mistrust::Mistrust::new_dangerously_trust_everyone();
1823
1824 let r = SqliteStore::from_path_and_mistrust(tmp.path(), &mistrust, true);
1826 assert!(r.is_err());
1827 assert!(!tmp.path().join("dir_blobs").try_exists().unwrap());
1828
1829 {
1831 let mut store = SqliteStore::from_path_and_mistrust(tmp.path(), &mistrust, false)?;
1832 assert!(tmp.path().join("dir_blobs").is_dir());
1833 assert!(matches!(&store.lockfile, LockFile::Locked(_)));
1834 assert!(!store.is_readonly());
1835 assert!(store.upgrade_to_readwrite()?); }
1837
1838 {
1840 let mut store2 = SqliteStore::from_path_and_mistrust(tmp.path(), &mistrust, true)?;
1841 assert!(store2.is_readonly());
1842
1843 assert!(store2.upgrade_to_readwrite()?); assert!(!store2.is_readonly());
1846 }
1847 Ok(())
1848 }
1849
1850 #[test]
1851 fn orphaned_blobs() -> Result<()> {
1852 let (_tmp_dir, mut store) = new_empty()?;
1853 assert_eq!(store.blob_dir.read_directory(".")?.count(), 0);
1859
1860 let now = now_utc();
1861 let one_week = 1.weeks();
1862 let _fname_good = store.save_blob(
1863 b"Goodbye, dear friends",
1864 "greeting",
1865 "sha1",
1866 &hex!("2149c2a7dbf5be2bb36fb3c5080d0fb14cb3355c"),
1867 now + one_week,
1868 )?;
1869 assert_eq!(store.blob_dir.read_directory(".")?.count(), 1);
1870
1871 store
1874 .blob_dir
1875 .write_and_replace("fairly_new", b"new contents will stay")?;
1876 store
1877 .blob_dir
1878 .write_and_replace("fairly_old", b"old contents will be removed")?;
1879 filetime::set_file_mtime(
1880 store.blob_dir.join("fairly_old")?,
1881 SystemTime::from(now - one_week).into(),
1882 )
1883 .expect("Can't adjust mtime");
1884
1885 assert_eq!(store.blob_dir.read_directory(".")?.count(), 3);
1886
1887 store.remove_unreferenced_blobs(now, &EXPIRATION_DEFAULTS)?;
1888 assert_eq!(store.blob_dir.read_directory(".")?.count(), 2);
1889
1890 Ok(())
1891 }
1892
1893 #[test]
1894 fn unreferenced_consensus_blob() -> Result<()> {
1895 let (_tmp_dir, mut store) = new_empty()?;
1896
1897 let now = now_utc();
1898 let one_week = 1.weeks();
1899
1900 let fname = store.save_blob(
1903 b"pretend this is a consensus",
1904 "con_fake",
1905 "sha1",
1906 &hex!("803e5a45eea7766a62a735e051a25a50ffb9b1cf"),
1907 now + one_week,
1908 )?;
1909
1910 assert_eq!(store.blob_dir.read_directory(".")?.count(), 1);
1911 assert_eq!(
1912 &std::fs::read(store.blob_dir.join(&fname)?).unwrap()[..],
1913 b"pretend this is a consensus"
1914 );
1915 let n: u32 = store
1916 .conn
1917 .query_row("SELECT COUNT(filename) FROM ExtDocs", [], |row| row.get(0))?;
1918 assert_eq!(n, 1);
1919
1920 store.expire_all(&EXPIRATION_DEFAULTS)?;
1921 assert_eq!(store.blob_dir.read_directory(".")?.count(), 0);
1922
1923 let n: u32 = store
1924 .conn
1925 .query_row("SELECT COUNT(filename) FROM ExtDocs", [], |row| row.get(0))?;
1926 assert_eq!(n, 0);
1927
1928 Ok(())
1929 }
1930
1931 #[test]
1932 fn vanished_blob_cleanup() -> Result<()> {
1933 let (_tmp_dir, mut store) = new_empty()?;
1934
1935 let now = now_utc();
1936 let one_week = 1.weeks();
1937
1938 let mut fnames = vec![];
1940 for idx in 0..8 {
1941 let content = format!("Example {idx}");
1942 let digest = Sha3_256::digest(content.as_bytes());
1943 let fname = store.save_blob(
1944 content.as_bytes(),
1945 "blob",
1946 "sha3-256",
1947 digest.as_slice(),
1948 now + one_week,
1949 )?;
1950 fnames.push(fname);
1951 }
1952
1953 store.blob_dir.remove_file(&fnames[1])?;
1955 store.blob_dir.remove_file(&fnames[3])?;
1956 store.blob_dir.remove_file(&fnames[5])?;
1957 store.blob_dir.remove_file(&fnames[7])?;
1958
1959 let n_removed = {
1960 let tx = store.conn.transaction()?;
1961 let n = SqliteStore::remove_entries_for_vanished_blobs(&store.blob_dir, &tx)?;
1962 tx.commit()?;
1963 n
1964 };
1965 assert_eq!(n_removed, 4);
1966
1967 let (n_1,): (u32,) =
1969 store
1970 .conn
1971 .query_row(COUNT_EXTDOC_BY_PATH, params![&fnames[1]], |row| {
1972 row.try_into()
1973 })?;
1974 let (n_2,): (u32,) =
1975 store
1976 .conn
1977 .query_row(COUNT_EXTDOC_BY_PATH, params![&fnames[2]], |row| {
1978 row.try_into()
1979 })?;
1980 assert_eq!(n_1, 0);
1981 assert_eq!(n_2, 1);
1982 Ok(())
1983 }
1984
1985 #[test]
1986 fn protocol_statuses() -> Result<()> {
1987 let (_tmp_dir, mut store) = new_empty()?;
1988
1989 let now = SystemTime::get();
1990 let hour = 1.hours();
1991
1992 let valid_after = now;
1993 let protocols = serde_json::from_str(
1994 r#"{
1995 "client":{
1996 "required":"Link=5 LinkAuth=3",
1997 "recommended":"Link=1-5 LinkAuth=2-5"
1998 },
1999 "relay":{
2000 "required":"Wombat=20-22 Knish=25-27",
2001 "recommended":"Wombat=20-30 Knish=20-30"
2002 }
2003 }"#,
2004 )
2005 .unwrap();
2006
2007 let v = store.cached_protocol_recommendations()?;
2008 assert!(v.is_none());
2009
2010 store.update_protocol_recommendations(valid_after, &protocols)?;
2011 let v = store.cached_protocol_recommendations()?.unwrap();
2012 assert_eq!(v.0, now);
2013 assert_eq!(
2014 serde_json::to_string(&protocols).unwrap(),
2015 serde_json::to_string(&v.1).unwrap()
2016 );
2017
2018 let protocols2 = serde_json::from_str(
2019 r#"{
2020 "client":{
2021 "required":"Link=5 ",
2022 "recommended":"Link=1-5"
2023 },
2024 "relay":{
2025 "required":"Wombat=20",
2026 "recommended":"Cons=6"
2027 }
2028 }"#,
2029 )
2030 .unwrap();
2031
2032 let valid_after_2 = now + hour;
2033 store.update_protocol_recommendations(valid_after_2, &protocols2)?;
2034
2035 let v = store.cached_protocol_recommendations()?.unwrap();
2036 assert_eq!(v.0, now + hour);
2037 assert_eq!(
2038 serde_json::to_string(&protocols2).unwrap(),
2039 serde_json::to_string(&v.1).unwrap()
2040 );
2041
2042 Ok(())
2043 }
2044}