More blips of work, including CA certificate generation
This commit is contained in:
parent
95a9f56f64
commit
eb55bf4cf3
95
Cargo.lock
generated
95
Cargo.lock
generated
@ -71,7 +71,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bare-metrics-core",
|
||||
"crossbeam-channel",
|
||||
"dashmap",
|
||||
"dashmap 4.0.2",
|
||||
"fxhash",
|
||||
"hdrhistogram",
|
||||
"log",
|
||||
@ -293,6 +293,47 @@ dependencies = [
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b799062aaf67eb976af3bdca031ee6f846d2f0a5710ddbb0d2efee33f3cc4760"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"num_cpus",
|
||||
"parking_lot 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
|
||||
|
||||
[[package]]
|
||||
name = "der-oid-macro"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c73af209b6a5dc8ca7cbaba720732304792cddc933cfea3d74509c2b1ef2f436"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9807efb310ce4ea172924f3a69d82f9fd6c9c3a19336344591153e665b31c43e"
|
||||
dependencies = [
|
||||
"der-oid-macro",
|
||||
"nom",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "1.3.0"
|
||||
@ -748,6 +789,17 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
@ -777,6 +829,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe554cb2393bc784fd678c82c84cc0599c31ceadc7f03a594911f822cb8d1815"
|
||||
dependencies = [
|
||||
"der-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "olivefs"
|
||||
version = "0.1.0"
|
||||
@ -789,7 +850,6 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"quinn",
|
||||
"rcgen",
|
||||
"serde",
|
||||
"serde_bare",
|
||||
"sodiumoxide",
|
||||
@ -821,10 +881,13 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bare-metrics-recorder",
|
||||
"clap",
|
||||
"dashmap 5.0.0",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"log",
|
||||
"quinn",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_bare",
|
||||
@ -1115,6 +1178,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"pem",
|
||||
"ring",
|
||||
"x509-parser",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
@ -1174,6 +1238,15 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c52377bb2288aa522a0c8208947fada1e0c76397f108cc08f57efe6077b50d"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.20.2"
|
||||
@ -1888,6 +1961,24 @@ dependencies = [
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc90836a84cb72e6934137b1504d0cae304ef5d83904beb0c8d773bbfe256ed"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"data-encoding",
|
||||
"der-parser",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"oid-registry",
|
||||
"ring",
|
||||
"rusticata-macros",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.4.0"
|
||||
|
@ -29,7 +29,6 @@ quinn = { version = "0.8.0", features = [] }
|
||||
# Compression and Encryption
|
||||
zstd = "0.9.2+zstd.1.5.1"
|
||||
sodiumoxide = "0.2.7"
|
||||
rcgen = "0.8.14"
|
||||
|
||||
# Filesystem
|
||||
fuser = "0.10.0"
|
||||
|
@ -1,16 +1,26 @@
|
||||
use fuser::{
|
||||
Filesystem, KernelConfig, ReplyAttr, ReplyBmap, ReplyCreate, ReplyData,
|
||||
ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyIoctl, ReplyLock, ReplyLseek,
|
||||
ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow,
|
||||
};
|
||||
use libc::{ENOSYS, EPERM};
|
||||
use log::{debug, warn};
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use std::os::raw::c_int;
|
||||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
use fuser::{Filesystem, KernelConfig, ReplyAttr, ReplyBmap, ReplyCreate, ReplyData, ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyIoctl, ReplyLock, ReplyLseek, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow};
|
||||
use libc::{ENOSYS, EPERM};
|
||||
use log::{warn, debug, info};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
pub mod encryption;
|
||||
|
||||
pub const ROOT_INODE: u64 = 0x01;
|
||||
|
||||
pub struct OliveFilesystemSettings {}
|
||||
|
||||
// TODO support multiple filesystems per FUSE fs so that operations between multiple of them
|
||||
// can be made efficient.
|
||||
pub struct OliveFilesystem {
|
||||
|
||||
pub settings: OliveFilesystemSettings,
|
||||
}
|
||||
|
||||
impl Filesystem for OliveFilesystem {
|
||||
@ -18,6 +28,7 @@ impl Filesystem for OliveFilesystem {
|
||||
/// Called before any other filesystem method.
|
||||
/// The kernel module connection can be configured using the KernelConfig object
|
||||
fn init(&mut self, _req: &Request<'_>, _config: &mut KernelConfig) -> Result<(), c_int> {
|
||||
// TODO config has some interesting values.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -31,6 +42,29 @@ impl Filesystem for OliveFilesystem {
|
||||
"[Not Implemented] lookup(parent: {:#x?}, name {:?})",
|
||||
parent, name
|
||||
);
|
||||
|
||||
let _ttl = Duration::from_secs(5);
|
||||
|
||||
// let file_attr = FileAttr {
|
||||
// ino: 0,
|
||||
// size: 0,
|
||||
// blocks: 0,
|
||||
// atime: (),
|
||||
// mtime: (),
|
||||
// ctime: (),
|
||||
// crtime: (),
|
||||
// kind: FileType::NamedPipe,
|
||||
// perm: 0,
|
||||
// nlink: 0,
|
||||
// uid: 0,
|
||||
// gid: 0,
|
||||
// rdev: 0,
|
||||
// blksize: 0,
|
||||
// flags: 0
|
||||
// };
|
||||
|
||||
// TODO reply.entry(&ttl, )
|
||||
|
||||
reply.error(ENOSYS);
|
||||
}
|
||||
|
||||
@ -690,4 +724,4 @@ impl Filesystem for OliveFilesystem {
|
||||
debug!("[Not Implemented] getxtimes(ino: {:#x?})", ino);
|
||||
reply.error(ENOSYS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,5 @@
|
||||
|
||||
// TODO Trait needed to provide encryption
|
||||
pub trait EncryptionProvider {
|
||||
|
||||
}
|
||||
pub trait EncryptionProvider {}
|
||||
|
||||
// No encryption
|
||||
impl EncryptionProvider for () {
|
||||
|
||||
}
|
||||
impl EncryptionProvider for () {}
|
||||
|
@ -1,18 +1,17 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::filesystem::OliveFilesystem;
|
||||
use crate::filesystem::{OliveFilesystem, OliveFilesystemSettings};
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use std::path::{PathBuf};
|
||||
|
||||
pub mod filesystem;
|
||||
pub mod requester;
|
||||
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum OlivefsCommands {
|
||||
Mount {
|
||||
configuration_file: PathBuf,
|
||||
mount_at: PathBuf
|
||||
}
|
||||
mount_at: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@ -23,9 +22,12 @@ fn main() {
|
||||
let command: OlivefsCommands = OlivefsCommands::parse();
|
||||
|
||||
match command {
|
||||
OlivefsCommands::Mount { configuration_file, mount_at } => {
|
||||
OlivefsCommands::Mount {
|
||||
configuration_file: _,
|
||||
mount_at,
|
||||
} => {
|
||||
let fs = OliveFilesystem {
|
||||
|
||||
settings: OliveFilesystemSettings {},
|
||||
};
|
||||
//fuser::MountOption::
|
||||
fuser::mount2(fs, mount_at, &[]).unwrap();
|
||||
|
@ -1,28 +1,31 @@
|
||||
use quinn::{Endpoint};
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use quinn::{Connection, Endpoint, EndpointConfig};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub struct Requester {
|
||||
|
||||
}
|
||||
pub struct Requester {}
|
||||
|
||||
pub struct RequesterInternal {
|
||||
rx: mpsc::Receiver<()>,
|
||||
connection: quinn::Connection
|
||||
connection: quinn::Connection,
|
||||
}
|
||||
|
||||
impl RequesterInternal {
|
||||
//pub async fn connect()
|
||||
pub async fn send(&mut self) -> anyhow::Result<()> {
|
||||
// TODO use with_roots and only use the desired CA! ...
|
||||
let x = quinn::ClientConfig::with_native_roots();
|
||||
let _x = quinn::ClientConfig::with_native_roots();
|
||||
let ep = Endpoint::client(SocketAddr::from_str("0.0.0.0:0").unwrap()).unwrap();
|
||||
let conn = ep.connect(SocketAddr::from_str("127.0.0.1:5051").unwrap(), "blah").unwrap().await.unwrap();
|
||||
let conn = ep
|
||||
.connect(SocketAddr::from_str("127.0.0.1:5051").unwrap(), "blah")
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
// conn.bi_streams and uni_streams are streams needed to listen out for new streams from the server.
|
||||
// conn.datagrams is similar but for unreliable datagrams
|
||||
|
||||
let bi = conn.connection.open_bi().await?;
|
||||
let _bi = conn.connection.open_bi().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
pub mod messages;
|
||||
pub mod networking;
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::fmt::Debug;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::fs::FileType;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub const COMMON_VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@ -22,20 +24,16 @@ impl HelloMessage {
|
||||
pub fn new(software_version: String) -> HelloMessage {
|
||||
HelloMessage {
|
||||
protocol_version: COMMON_VERSION.to_string(),
|
||||
software_version
|
||||
software_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
/// Sent by the client on any data stream.
|
||||
pub enum DataCommand {
|
||||
pub enum DataCommand {}
|
||||
|
||||
}
|
||||
|
||||
pub trait DataResponseBase: Serialize + DeserializeOwned + Debug + Clone + 'static {
|
||||
|
||||
}
|
||||
pub trait DataResponseBase: Serialize + DeserializeOwned + Debug + Clone + 'static {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
/// Sent by the server in response to a DataCommand on the same data stream.
|
||||
@ -50,3 +48,38 @@ pub enum DataResponse<R: DataResponseBase> {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Copy of fuser's FileAttr. Used to describe a file.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FileMetadata {
|
||||
/// Inode number
|
||||
pub ino: u64,
|
||||
/// Size in bytes
|
||||
pub size: u64,
|
||||
/// Size in blocks
|
||||
pub blocks: u64,
|
||||
/// Time of last access
|
||||
pub atime: SystemTime,
|
||||
/// Time of last modification
|
||||
pub mtime: SystemTime,
|
||||
/// Time of last change
|
||||
pub ctime: SystemTime,
|
||||
// /// Time of creation (macOS only)
|
||||
// pub crtime: SystemTime,
|
||||
/// Kind of file (directory, file, pipe, etc)
|
||||
pub kind: FileType,
|
||||
/// Permissions
|
||||
pub perm: u16,
|
||||
/// Number of hard links
|
||||
pub nlink: u32,
|
||||
/// User id
|
||||
pub uid: u32,
|
||||
/// Group id
|
||||
pub gid: u32,
|
||||
/// Rdev
|
||||
pub rdev: u32,
|
||||
/// Block size
|
||||
pub blksize: u32,
|
||||
// /// Flags (macOS only, see chflags(2))
|
||||
// pub flags: u32,
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
use quinn::{ReadExactError, RecvStream};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
|
||||
pub async fn read_control_message<M: DeserializeOwned>(recv_stream: &mut RecvStream) -> anyhow::Result<Option<M>> {
|
||||
pub async fn read_control_message<M: DeserializeOwned>(
|
||||
recv_stream: &mut RecvStream,
|
||||
) -> anyhow::Result<Option<M>> {
|
||||
let mut u16_buf = [0u8; 2];
|
||||
if let Err(err) = recv_stream.read_exact(&mut u16_buf).await {
|
||||
return if err == ReadExactError::FinishedEarly {
|
||||
@ -16,7 +17,9 @@ pub async fn read_control_message<M: DeserializeOwned>(recv_stream: &mut RecvStr
|
||||
|
||||
let mut control_message_bytes: Vec<u8> = vec![0u8; control_message_length];
|
||||
|
||||
recv_stream.read_exact(&mut control_message_bytes[..]).await?;
|
||||
recv_stream
|
||||
.read_exact(&mut control_message_bytes[..])
|
||||
.await?;
|
||||
|
||||
Ok(todo!())
|
||||
}
|
||||
}
|
||||
|
@ -17,15 +17,18 @@ tracing-futures = { version = "0.2.5", features = ["tokio"] }
|
||||
# Asynchronous
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
futures-util = "0.3.19"
|
||||
dashmap = "5.0.0"
|
||||
|
||||
# Serialisation
|
||||
serde = { version = "1.0.133", features = ["derive"] }
|
||||
serde_bare = "0.5.0"
|
||||
toml = "0.5.8"
|
||||
clap = { version = "3.0.7", features = ["derive"] }
|
||||
|
||||
# Networking
|
||||
quinn = { version = "0.8.0", features = [] }
|
||||
rustls = "0.20.2"
|
||||
|
||||
# Compression and Encryption
|
||||
zstd = "0.9.2+zstd.1.5.1"
|
||||
rustls = "0.20.2"
|
||||
rcgen = { version = "0.8.14", features = ["pem", "x509-parser"] }
|
||||
|
@ -1,11 +1,95 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use futures_util::StreamExt;
|
||||
use quinn::Endpoint;
|
||||
use rcgen::{BasicConstraints, Certificate, CertificateParams, IsCa, KeyPair};
|
||||
use rustls::internal::msgs::codec::Codec;
|
||||
use std::io::Read;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::thread::yield_now;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
pub mod server;
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
pub enum Command {
|
||||
Serve {},
|
||||
/// Generates a certificate
|
||||
GenerateCertificate {
|
||||
/// The certificate authority directory
|
||||
ca_dir: PathBuf,
|
||||
/// Name of the client or server to generate a certificate for
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn write_file(path: &Path, content: &str) -> anyhow::Result<()> {
|
||||
let mut file = File::create(path).await?;
|
||||
file.write_all(content.as_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_file(path: &Path) -> anyhow::Result<String> {
|
||||
let mut file = File::open(path).await?;
|
||||
let mut string = String::new();
|
||||
file.read_to_string(&mut string).await?;
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let command: Command = Command::parse();
|
||||
|
||||
match command {
|
||||
Command::Serve { .. } => {}
|
||||
Command::GenerateCertificate { ca_dir, name } => {
|
||||
let ca_key_path = ca_dir.join("ca.key");
|
||||
let ca_cert_path = ca_dir.join("ca.pem");
|
||||
if !ca_key_path.exists() {
|
||||
// Generate a CA first
|
||||
let mut ca_params = CertificateParams::new(Vec::with_capacity(0));
|
||||
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||
// TODO don't hard code a date :-).
|
||||
ca_params.not_after = rcgen::date_time_ymd(2042, 1, 1);
|
||||
let ca_cert = Certificate::from_params(ca_params)?;
|
||||
let cert_pem = ca_cert.serialize_pem()?;
|
||||
let key_pem = ca_cert.serialize_private_key_pem();
|
||||
|
||||
write_file(&ca_key_path, &key_pem).await?;
|
||||
write_file(&ca_cert_path, &cert_pem).await?;
|
||||
}
|
||||
|
||||
let ca_key_pem = read_file(&ca_key_path).await?;
|
||||
let ca_cert_pem = read_file(&ca_cert_path).await?;
|
||||
|
||||
let ca_keypair = KeyPair::from_pem(&ca_key_pem)?;
|
||||
let ca_cert = Certificate::from_params(CertificateParams::from_ca_cert_pem(
|
||||
&ca_cert_pem,
|
||||
ca_keypair,
|
||||
)?)?;
|
||||
|
||||
let mut cert_params = CertificateParams::new(vec![name.clone()]);
|
||||
// TODO don't hard code a date :-).
|
||||
cert_params.not_after = rcgen::date_time_ymd(2042, 1, 1);
|
||||
|
||||
let leaf_cert = Certificate::from_params(cert_params)?;
|
||||
write_file(
|
||||
&ca_dir.join(format!("{}.key", &name)),
|
||||
&leaf_cert.serialize_private_key_pem(),
|
||||
)
|
||||
.await?;
|
||||
write_file(
|
||||
&ca_dir.join(format!("{}.pem", &name)),
|
||||
&leaf_cert.serialize_pem_with_signer(&ca_cert)?,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Hello, world!");
|
||||
|
||||
//let x = quinn::ServerConfig::with_native_roots();
|
||||
@ -15,7 +99,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
.with_single_cert(todo!(), todo!())?;
|
||||
let crypto = Arc::new(crypto);
|
||||
let x = quinn::ServerConfig::with_crypto(crypto);
|
||||
let (ep, mut incoming) = Endpoint::server(x, SocketAddr::from_str("127.0.0.1:5051").unwrap()).unwrap();
|
||||
let (ep, mut incoming) =
|
||||
Endpoint::server(x, SocketAddr::from_str("127.0.0.1:5051").unwrap()).unwrap();
|
||||
|
||||
loop {
|
||||
let next = incoming.next().await;
|
||||
|
11
olivefsd/src/server.rs
Normal file
11
olivefsd/src/server.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct FileDescriptor {}
|
||||
|
||||
pub struct InodeInfo {}
|
||||
|
||||
pub struct ServerHandle {
|
||||
pub inode_map: Arc<DashMap<u32, InodeInfo>>,
|
||||
pub file_descriptors: Arc<DashMap<u32, FileDescriptor>>,
|
||||
}
|
Loading…
Reference in New Issue
Block a user