1
use std::{fs, path::PathBuf, time::Duration};
2

            
3
use anyhow::{Context, anyhow};
4
use clap_verbosity_flag::Verbosity;
5
use nix::libc::{EXIT_SUCCESS, exit};
6
use sqlx::mysql::MySqlPoolOptions;
7
use std::os::unix::net::UnixStream as StdUnixStream;
8
use tokio::net::UnixStream as TokioUnixStream;
9

            
10
use crate::{
11
    core::common::{
12
        DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH, UnixUser, executable_is_suid_or_sgid,
13
    },
14
    server::{
15
        config::{MysqlConfig, read_config_from_path},
16
        session_handler,
17
    },
18
};
19

            
20
/// Determine whether we will make a connection to an external server
21
/// or start an internal server with elevated privileges.
22
///
23
/// If neither is feasible, an error is returned.
24
fn will_connect_to_external_server(
25
    server_socket_path: Option<&PathBuf>,
26
    // This parameter is only used in suid-sgid-mode
27
    #[allow(unused_variables)] config_path: Option<&PathBuf>,
28
) -> anyhow::Result<bool> {
29
    if server_socket_path.is_some() {
30
        return Ok(true);
31
    }
32

            
33
    #[cfg(feature = "suid-sgid-mode")]
34
    if config_path.is_some() {
35
        return Ok(false);
36
    }
37

            
38
    if fs::metadata(DEFAULT_SOCKET_PATH).is_ok() {
39
        return Ok(true);
40
    }
41

            
42
    #[cfg(feature = "suid-sgid-mode")]
43
    if fs::metadata(DEFAULT_CONFIG_PATH).is_ok() {
44
        return Ok(false);
45
    }
46

            
47
    #[cfg(feature = "suid-sgid-mode")]
48
    anyhow::bail!("No socket path or config path provided, and no default socket or config found");
49

            
50
    #[cfg(not(feature = "suid-sgid-mode"))]
51
    anyhow::bail!("No socket path provided, and no default socket found");
52
}
53

            
54
/// This function is used to bootstrap the connection to the server.
55
/// This can happen in two ways:
56
///
57
/// 1. If a socket path is provided, or exists in the default location,
58
///    the function will connect to the socket and authenticate with the
59
///    server to ensure that the server knows the uid of the client.
60
///
61
/// 2. If a config path is provided, or exists in the default location,
62
///    and the config is readable, the function will assume it is either
63
///    setuid or setgid, and will fork a child process to run the server
64
///    with the provided config. The server will exit silently by itself
65
///    when it is done, and this function will only return for the client
66
///    with the socket for the server.
67
///
68
/// If neither of these options are available, the function will fail.
69
///
70
/// Note that this function is also responsible for setting up logging,
71
/// because in the case of an internal server, we need to drop privileges
72
/// before we can initialize logging.
73
///
74
/// **WARNING:** This function may be run with elevated privileges.
75
pub fn bootstrap_server_connection_and_drop_privileges(
76
    server_socket_path: Option<PathBuf>,
77
    config: Option<PathBuf>,
78
    verbose: Verbosity,
79
) -> anyhow::Result<StdUnixStream> {
80
    if will_connect_to_external_server(server_socket_path.as_ref(), config.as_ref())? {
81
        assert!(
82
            !executable_is_suid_or_sgid()?,
83
            "The executable should not be SUID or SGID when connecting to an external server"
84
        );
85

            
86
        env_logger::Builder::new()
87
            .filter_level(verbose.log_level_filter())
88
            .init();
89

            
90
        connect_to_external_server(server_socket_path)
91
    } else if cfg!(feature = "suid-sgid-mode") {
92
        // NOTE: We need to be really careful with the code up until this point,
93
        //       as we might be running with elevated privileges.
94
        let server_connection = bootstrap_internal_server_and_drop_privs(config)?;
95

            
96
        env_logger::Builder::new()
97
            .filter_level(verbose.log_level_filter())
98
            .init();
99

            
100
        Ok(server_connection)
101
    } else {
102
        anyhow::bail!("SUID/SGID support is not enabled, cannot start internal server");
103
    }
104
}
105

            
106
fn connect_to_external_server(
107
    server_socket_path: Option<PathBuf>,
108
) -> anyhow::Result<StdUnixStream> {
109
    // TODO: ensure this is both readable and writable
110
    if let Some(socket_path) = server_socket_path {
111
        log::debug!("Connecting to socket at {:?}", socket_path);
112
        return match StdUnixStream::connect(socket_path) {
113
            Ok(socket) => Ok(socket),
114
            Err(e) => match e.kind() {
115
                std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")),
116
                std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")),
117
                _ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)),
118
            },
119
        };
120
    }
121

            
122
    if fs::metadata(DEFAULT_SOCKET_PATH).is_ok() {
123
        log::debug!("Connecting to default socket at {:?}", DEFAULT_SOCKET_PATH);
124
        return match StdUnixStream::connect(DEFAULT_SOCKET_PATH) {
125
            Ok(socket) => Ok(socket),
126
            Err(e) => match e.kind() {
127
                std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")),
128
                std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")),
129
                _ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)),
130
            },
131
        };
132
    }
133

            
134
    anyhow::bail!("No socket path provided, and no default socket found");
135
}
136

            
137
// TODO: this function is security critical, it should be integration tested
138
//       in isolation.
139
/// Drop privileges to the real user and group of the process.
140
/// If the process is not running with elevated privileges, this function
141
/// is a no-op.
142
fn drop_privs() -> anyhow::Result<()> {
143
    log::debug!("Dropping privileges");
144
    let real_uid = nix::unistd::getuid();
145
    let real_gid = nix::unistd::getgid();
146

            
147
    nix::unistd::setuid(real_uid).context("Failed to drop privileges")?;
148
    nix::unistd::setgid(real_gid).context("Failed to drop privileges")?;
149

            
150
    debug_assert_eq!(nix::unistd::getuid(), real_uid);
151
    debug_assert_eq!(nix::unistd::getgid(), real_gid);
152

            
153
    log::debug!("Privileges dropped successfully");
154
    Ok(())
155
}
156

            
157
fn bootstrap_internal_server_and_drop_privs(
158
    config_path: Option<PathBuf>,
159
) -> anyhow::Result<StdUnixStream> {
160
    if let Some(config_path) = config_path {
161
        if !executable_is_suid_or_sgid()? {
162
            anyhow::bail!("Executable is not SUID/SGID - refusing to start internal sever");
163
        }
164

            
165
        // ensure config exists and is readable
166
        if fs::metadata(&config_path).is_err() {
167
            return Err(anyhow::anyhow!("Config file not found or not readable"));
168
        }
169

            
170
        log::debug!("Starting server with config at {:?}", config_path);
171
        let socket = invoke_server_with_config(config_path)?;
172
        drop_privs()?;
173
        return Ok(socket);
174
    };
175

            
176
    let config_path = PathBuf::from(DEFAULT_CONFIG_PATH);
177
    if fs::metadata(&config_path).is_ok() {
178
        if !executable_is_suid_or_sgid()? {
179
            anyhow::bail!("Executable is not SUID/SGID - refusing to start internal sever");
180
        }
181
        log::debug!("Starting server with default config at {:?}", config_path);
182
        let socket = invoke_server_with_config(config_path)?;
183
        drop_privs()?;
184
        return Ok(socket);
185
    };
186

            
187
    anyhow::bail!("No config path provided, and no default config found");
188
}
189

            
190
// TODO: we should somehow ensure that the forked process is killed on completion,
191
//       just in case the client does not behave properly.
192
/// Fork a child process to run the server with the provided config.
193
/// The server will exit silently by itself when it is done, and this function
194
/// will only return for the client with the socket for the server.
195
fn invoke_server_with_config(config_path: PathBuf) -> anyhow::Result<StdUnixStream> {
196
    let (server_socket, client_socket) = StdUnixStream::pair()?;
197
    let unix_user = UnixUser::from_uid(nix::unistd::getuid().as_raw())?;
198

            
199
    match (unsafe { nix::unistd::fork() }).context("Failed to fork")? {
200
        nix::unistd::ForkResult::Parent { child } => {
201
            log::debug!("Forked child process with PID {}", child);
202
            Ok(client_socket)
203
        }
204
        nix::unistd::ForkResult::Child => {
205
            log::debug!("Running server in child process");
206

            
207
            match run_forked_server(config_path, server_socket, unix_user) {
208
                Err(e) => Err(e),
209
                Ok(_) => unreachable!(),
210
            }
211
        }
212
    }
213
}
214

            
215
async fn construct_single_connection_mysql_pool(
216
    config: &MysqlConfig,
217
) -> anyhow::Result<sqlx::MySqlPool> {
218
    let mysql_config = config.as_mysql_connect_options()?;
219

            
220
    let pool_opts = MySqlPoolOptions::new()
221
        .max_connections(1)
222
        .min_connections(1);
223

            
224
    config.log_connection_notice();
225

            
226
    let pool = match tokio::time::timeout(
227
        Duration::from_secs(config.timeout),
228
        pool_opts.connect_with(mysql_config),
229
    )
230
    .await
231
    {
232
        Ok(connection) => connection.context("Failed to connect to the database"),
233
        Err(_) => Err(anyhow!("Timed out after {} seconds", config.timeout))
234
            .context("Failed to connect to the database"),
235
    }?;
236

            
237
    Ok(pool)
238
}
239

            
240
/// Run the server in the forked child process.
241
/// This function will not return, but will exit the process with a success code.
242
fn run_forked_server(
243
    config_path: PathBuf,
244
    server_socket: StdUnixStream,
245
    unix_user: UnixUser,
246
) -> anyhow::Result<()> {
247
    let config = read_config_from_path(Some(config_path))?;
248

            
249
    let result: anyhow::Result<()> = tokio::runtime::Builder::new_current_thread()
250
        .enable_all()
251
        .build()
252
        .unwrap()
253
        .block_on(async {
254
            let socket = TokioUnixStream::from_std(server_socket)?;
255
            let db_pool = construct_single_connection_mysql_pool(&config.mysql).await?;
256
            session_handler::session_handler(socket, &unix_user, db_pool).await?;
257
            Ok(())
258
        });
259

            
260
    result?;
261

            
262
    unsafe {
263
        exit(EXIT_SUCCESS);
264
    }
265
}