// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. mod common; use crate::common::*; use std::fs; use std::path::Path; use rkv::Rkv; use rkv::StoreOptions; use uuid::Uuid; use glean_core::metrics::*; use glean_core::CommonMetricData; use glean_core::Lifetime; fn clientid_metric() -> UuidMetric { UuidMetric::new(CommonMetricData { name: "client_id".into(), category: "".into(), send_in_pings: vec!["glean_client_info".into()], lifetime: Lifetime::User, disabled: false, dynamic_label: None, }) } fn clientid_from_file(data_path: &Path) -> Option { let path = data_path.join("client_id.txt"); let uuid_str = fs::read_to_string(path).ok()?; Uuid::parse_str(uuid_str.trim_end()).ok() } fn write_clientid_to_file(data_path: &Path, uuid: &Uuid) -> Option<()> { let mut buffer = Uuid::encode_buffer(); let uuid_str = uuid.hyphenated().encode_lower(&mut buffer); write_clientid_to_file_str(data_path, uuid_str) } fn write_clientid_to_file_str(data_path: &Path, s: &str) -> Option<()> { let path = data_path.join("client_id.txt"); fs::write(path, s.as_bytes()).ok()?; Some(()) } #[test] fn writes_clientid_file() { let (glean, temp) = new_glean(None); let db_client_id = clientid_metric().get_value(&glean, None).unwrap(); let file_client_id = clientid_from_file(temp.path()).unwrap(); assert_eq!(file_client_id, db_client_id); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = &payload["metrics"]["string"]["glean.health.exception_state"]; assert_eq!(&serde_json::Value::Null, state); } #[test] fn reused_clientid_from_file() { let temp = tempfile::tempdir().unwrap(); let new_uuid = Uuid::new_v4(); write_clientid_to_file(temp.path(), &new_uuid).unwrap(); let (glean, temp) = new_glean(Some(temp)); // TODO(bug 1996862): We don't run the mitigation yet //let db_client_id = clientid_metric().get_value(&glean, None).unwrap(); //assert_eq!(new_uuid, db_client_id); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = payload["metrics"]["string"]["glean.health.exception_state"].as_str(); assert_eq!(Some("empty-db"), state); } #[test] fn restores_clientid_file_from_db() { let (db_client_id, temp) = { // Ensure we initialize once to get a client_id let (glean, temp) = new_glean(None); let db_client_id = clientid_metric().get_value(&glean, None).unwrap(); drop(glean); (db_client_id, temp) }; // Removing the file. `Glean::new` should restore it fs::remove_file(temp.path().join("client_id.txt")).unwrap(); let (glean, temp) = new_glean(Some(temp)); let file_client_id = clientid_from_file(temp.path()).unwrap(); assert_eq!(file_client_id, db_client_id); let db_client_id2 = clientid_metric().get_value(&glean, None).unwrap(); assert_eq!(db_client_id, db_client_id2); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = payload["metrics"]["string"]["glean.health.exception_state"].as_str(); assert_eq!(None, state); } #[test] fn clientid_regen_issue_with_existing_db() { let (file_client_id, temp) = { // Ensure we initialize once to get a client_id let (glean, temp) = new_glean(None); let file_client_id = clientid_from_file(temp.path()).unwrap(); drop(glean); (file_client_id, temp) }; // We modify the database and ONLY clear out the client id. { let path = temp.path().join("db"); let rkv = Rkv::new::(&path).unwrap(); let user_store = rkv.open_single("user", StoreOptions::create()).unwrap(); // We know this. let client_id_key = "glean_client_info#client_id"; let mut writer = rkv.write().unwrap(); user_store.delete(&mut writer, client_id_key).unwrap(); writer.commit().unwrap(); } let (glean, temp) = new_glean(Some(temp)); // TODO(bug 1996862): We don't run the mitigation yet _ = file_client_id; //let db_client_id = clientid_metric().get_value(&glean, None).unwrap(); //assert_eq!(file_client_id, db_client_id); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = payload["metrics"]["string"]["glean.health.exception_state"].as_str(); assert_eq!(Some("regen-db"), state); } #[test] fn db_client_id_prefered_over_file_client_id() { let (db_client_id, temp) = { // Ensure we initialize once to get a client_id let (glean, temp) = new_glean(None); let db_client_id = clientid_metric().get_value(&glean, None).unwrap(); drop(glean); (db_client_id, temp) }; // We modify the client id file let new_uuid = Uuid::new_v4(); write_clientid_to_file(temp.path(), &new_uuid).unwrap(); let (glean, temp) = new_glean(Some(temp)); let db_client_id2 = clientid_metric().get_value(&glean, None).unwrap(); assert_eq!(db_client_id, db_client_id2); let file_client_id = clientid_from_file(temp.path()).unwrap(); assert_eq!(file_client_id, db_client_id); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = payload["metrics"]["string"]["glean.health.exception_state"].as_str(); assert_eq!(Some("client-id-mismatch"), state); let exception_uuid = &payload["metrics"]["uuid"]["glean.health.recovered_client_id"].as_str(); let mut buffer = Uuid::encode_buffer(); let expected_uuid = &*new_uuid.hyphenated().encode_lower(&mut buffer); assert_eq!(&Some(expected_uuid), exception_uuid); } #[test] fn c0ffee_in_db_gets_overwritten_by_stored_client_id() { let (file_client_id, temp) = { // Ensure we initialize once to get a client_id let (glean, temp) = new_glean(None); let file_client_id = clientid_from_file(temp.path()).unwrap(); drop(glean); (file_client_id, temp) }; // We modify the database and ONLY set the client id to c0ffee. { let path = temp.path().join("db"); let rkv = Rkv::new::(&path).unwrap(); let user_store = rkv.open_single("user", StoreOptions::create()).unwrap(); // We know this. let client_id_key = "glean_client_info#client_id"; let mut writer = rkv.write().unwrap(); let encoded = bincode::serialize(&Metric::Uuid(String::from( "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0", ))) .unwrap(); let known_client_id = rkv::Value::Blob(&encoded); user_store .put(&mut writer, client_id_key, &known_client_id) .unwrap(); writer.commit().unwrap(); } let (glean, temp) = new_glean(Some(temp)); // TODO(bug 1996862): We don't run the mitigation yet _ = file_client_id; //let db_client_id = clientid_metric().get_value(&glean, None).unwrap(); //assert_eq!(file_client_id, db_client_id); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = payload["metrics"]["string"]["glean.health.exception_state"].as_str(); assert_eq!(Some("c0ffee-in-db"), state); let mut buffer = Uuid::encode_buffer(); let file_client_id = &*file_client_id.hyphenated().encode_lower(&mut buffer); let exception_uuid = &payload["metrics"]["uuid"]["glean.health.recovered_client_id"].as_str(); assert_eq!(&Some(file_client_id), exception_uuid); // TODO(bug 1996862): We don't run the mitigation yet //let exception_uuid = &payload["client_info"]["client_id"].as_str(); //assert_eq!(&Some(file_client_id), exception_uuid); // instead we ensure we don't see the c0ffee ID either. let client_id = payload["client_info"]["client_id"].as_str().unwrap(); assert_ne!("c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0", client_id); } #[test] fn clientid_file_gets_deleted() { let (mut glean, temp) = new_glean(None); let path = temp.path().join("client_id.txt"); assert!(path.exists()); let file_client_id = clientid_from_file(temp.path()); assert!(file_client_id.is_some()); glean.set_upload_enabled(false); assert!(!path.exists()); } #[test] fn clientid_file_casing_doesnt_matter() { let (file_client_id, temp) = { // Ensure we initialize once to get a client_id let (glean, temp) = new_glean(None); let file_client_id = clientid_from_file(temp.path()).unwrap(); drop(glean); (file_client_id, temp) }; { let mut buffer = Uuid::encode_buffer(); let uuid_str = file_client_id.hyphenated().encode_lower(&mut buffer); write_clientid_to_file_str(temp.path(), &uuid_str.to_uppercase()); } let (glean, temp) = new_glean(Some(temp)); let db_client_id = clientid_metric().get_value(&glean, None).unwrap(); assert_eq!(file_client_id, db_client_id); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = &payload["metrics"]["string"]["glean.health.exception_state"]; assert_eq!(&serde_json::Value::Null, state); } #[test] fn invalid_client_id_file_doesnt_crash() { let (glean, mut temp) = new_glean(None); drop(glean); let test_values = &[ "", "abc", "e9f87afa-48b6-489e-841a-401522aaf1f", // one byte short "e9f87afa-48b6-489e-841a-401522aaf1ff\n", // explicit newline! "k9f87afa-48b6-489e-841a-401522aaf1ff", // k is not a valid char "\0\0\0", ]; for value in test_values { write_clientid_to_file_str(temp.path(), value); let (glean, new_temp) = new_glean(Some(temp)); drop(glean); temp = new_temp; } // Now testing with a directory in place. { let p = temp.path().join("client_id.txt"); fs::remove_file(&p).unwrap(); fs::create_dir_all(&p).unwrap(); let (glean, new_temp) = new_glean(Some(temp)); drop(glean); temp = new_temp; } drop(temp); } mod read_errors { use super::*; #[test] fn parse_error_is_reported() { let (glean, temp) = new_glean(None); drop(glean); write_clientid_to_file_str(temp.path(), "abc"); let (glean, temp) = new_glean(Some(temp)); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = &payload["metrics"]["string"]["glean.health.exception_state"]; assert_eq!(&serde_json::Value::Null, state); let state = payload["metrics"]["labeled_counter"]["glean.health.file_read_error"]["parse"] .as_i64() .unwrap(); assert_eq!(1, state); } #[test] fn io_error_is_reported() { let (glean, temp) = new_glean(None); drop(glean); let p = temp.path().join("client_id.txt"); fs::remove_file(&p).unwrap(); fs::create_dir_all(&p).unwrap(); let (glean, temp) = new_glean(Some(temp)); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = &payload["metrics"]["string"]["glean.health.exception_state"]; assert_eq!(&serde_json::Value::Null, state); let state = payload["metrics"]["labeled_counter"]["glean.health.file_read_error"]["io"] .as_i64() .unwrap(); assert_eq!(1, state); } #[cfg(unix)] #[test] fn permission_error_is_reported() { use std::os::unix::fs::PermissionsExt; let (glean, temp) = new_glean(None); drop(glean); let p = temp.path().join("client_id.txt"); // Remove write permissions on the file. let attr = fs::metadata(&p).unwrap(); let original_permissions = attr.permissions(); let mut permissions = original_permissions.clone(); // No permissions at all, no read, no write, for anyone. permissions.set_mode(0o0); fs::set_permissions(&p, permissions).unwrap(); let (glean, temp) = new_glean(Some(temp)); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = &payload["metrics"]["string"]["glean.health.exception_state"]; assert_eq!(&serde_json::Value::Null, state); let state = payload["metrics"]["labeled_counter"]["glean.health.file_read_error"] ["permission-denied"] .as_i64() .unwrap(); assert_eq!(1, state); } #[test] fn c0ffee_is_reported() { let (glean, temp) = new_glean(None); drop(glean); write_clientid_to_file_str(temp.path(), "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0"); let (glean, temp) = new_glean(Some(temp)); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = &payload["metrics"]["string"]["glean.health.exception_state"]; assert_eq!(&serde_json::Value::Null, state); let state = payload["metrics"]["labeled_counter"]["glean.health.file_read_error"] ["c0ffee-in-file"] .as_i64() .unwrap(); assert_eq!(1, state); } } mod write_errors { use super::*; #[cfg(unix)] #[test] fn permission_error_is_reported() { let (glean, temp) = new_glean(None); drop(glean); // Force empty file, so that it will be written again later. write_clientid_to_file_str(temp.path(), ""); let p = temp.path().join("client_id.txt"); // Remove write permissions on the file. let attr = fs::metadata(&p).unwrap(); let original_permissions = attr.permissions(); let mut permissions = original_permissions.clone(); // No permissions at all, no read, no write, for anyone. permissions.set_readonly(true); fs::set_permissions(&p, permissions).unwrap(); let (glean, temp) = new_glean(Some(temp)); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = &payload["metrics"]["string"]["glean.health.exception_state"]; assert_eq!(&serde_json::Value::Null, state); let state = payload["metrics"]["labeled_counter"]["glean.health.file_read_error"]["parse"] .as_i64() .unwrap(); assert_eq!(1, state); let state = payload["metrics"]["labeled_counter"]["glean.health.file_write_error"] ["permission-denied"] .as_i64() .unwrap(); assert_eq!(1, state); } #[cfg(unix)] #[test] fn permission_error_on_later_regeneration() { let (mut glean, temp) = new_glean(None); glean.submit_ping_by_name("health", Some("pre_init")); let pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); glean.set_upload_enabled(false); let path = temp.path().join("client_id.txt"); assert!(!path.exists()); // Force empty file, so that it will be written again later. write_clientid_to_file_str(temp.path(), ""); let p = temp.path().join("client_id.txt"); // Remove write permissions on the file. let attr = fs::metadata(&p).unwrap(); let original_permissions = attr.permissions(); let mut permissions = original_permissions.clone(); // No permissions at all, no read, no write, for anyone. permissions.set_readonly(true); fs::set_permissions(&p, permissions).unwrap(); glean.set_upload_enabled(true); glean.submit_ping_by_name("health", Some("pre_init")); let mut pending = get_queued_pings(temp.path()).unwrap(); assert_eq!(1, pending.len()); let payload = pending.pop().unwrap().1; let state = &payload["metrics"]["string"]["glean.health.exception_state"]; assert_eq!(&serde_json::Value::Null, state); let state = &payload["metrics"]["labeled_counter"]["glean.health.file_read_error"]["parse"]; assert_eq!(&serde_json::Value::Null, state); let state = payload["metrics"]["labeled_counter"]["glean.health.file_write_error"] ["permission-denied"] .as_i64() .unwrap(); assert_eq!(1, state); } }