]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/node.rs
verify: keep track and log which dirs failed the verification
[proxmox-backup.git] / src / api2 / node.rs
CommitLineData
1c2f842a
DC
1use std::net::TcpListener;
2use std::os::unix::io::AsRawFd;
3
4use anyhow::{bail, format_err, Error};
5use futures::{
6 future::{FutureExt, TryFutureExt},
224c65f8 7 select,
1c2f842a
DC
8};
9use hyper::body::Body;
10use hyper::http::request::Parts;
11use hyper::upgrade::Upgraded;
12use nix::fcntl::{fcntl, FcntlArg, FdFlag};
13use serde_json::{json, Value};
14use tokio::io::{AsyncBufReadExt, BufReader};
15
3d482025 16use proxmox::api::router::{Router, SubdirMap};
1c2f842a
DC
17use proxmox::api::{
18 api, schema::*, ApiHandler, ApiMethod, ApiResponseFuture, Permission, RpcEnvironment,
19};
9ea4bce4 20use proxmox::list_subdirs_api_method;
1c2f842a
DC
21use proxmox::tools::websocket::WebSocket;
22use proxmox::{identity, sortable};
b2b3485d 23
1c2f842a
DC
24use crate::api2::types::*;
25use crate::config::acl::PRIV_SYS_CONSOLE;
26use crate::server::WorkerTask;
27use crate::tools;
28
29pub mod disks;
550e0d88 30pub mod dns;
1c2f842a 31pub mod network;
3865e27e
WB
32pub mod tasks;
33
1c2f842a 34pub(crate) mod rrd;
3865e27e
WB
35
36mod apt;
37mod journal;
d2ab5f19 38mod services;
2337df7b 39mod status;
113c9b59 40mod subscription;
1c2f842a 41mod syslog;
1c2f842a
DC
42mod time;
43
44pub const SHELL_CMD_SCHEMA: Schema = StringSchema::new("The command to run.")
45 .format(&ApiStringFormat::Enum(&[
46 EnumEntry::new("login", "Login"),
47 EnumEntry::new("upgrade", "Upgrade"),
48 ]))
49 .schema();
50
51#[api(
52 protected: true,
53 input: {
54 properties: {
55 node: {
56 schema: NODE_SCHEMA,
57 },
58 cmd: {
59 schema: SHELL_CMD_SCHEMA,
60 optional: true,
61 },
62 },
63 },
64 returns: {
65 type: Object,
66 description: "Object with the user, ticket, port and upid",
67 properties: {
68 user: {
69 description: "",
70 type: String,
71 },
72 ticket: {
73 description: "",
74 type: String,
75 },
76 port: {
77 description: "",
78 type: String,
79 },
80 upid: {
81 description: "",
82 type: String,
83 },
84 }
85 },
86 access: {
87 description: "Restricted to users on realm 'pam'",
e744de0e 88 permission: &Permission::Privilege(&["system"], PRIV_SYS_CONSOLE, false),
1c2f842a
DC
89 }
90)]
91/// Call termproxy and return shell ticket
92async fn termproxy(
1c2f842a 93 cmd: Option<String>,
1c2f842a
DC
94 rpcenv: &mut dyn RpcEnvironment,
95) -> Result<Value, Error> {
96 let userid = rpcenv
97 .get_user()
98 .ok_or_else(|| format_err!("unknown user"))?;
99 let (username, realm) = crate::auth::parse_userid(&userid)?;
100
101 if realm != "pam" {
102 bail!("only pam users can use the console");
103 }
104
e744de0e 105 let path = "/system";
1c2f842a
DC
106
107 // use port 0 and let the kernel decide which port is free
108 let listener = TcpListener::bind("localhost:0")?;
109 let port = listener.local_addr()?.port();
110
111 let ticket = tools::ticket::assemble_term_ticket(
112 crate::auth_helpers::private_auth_key(),
113 &userid,
114 &path,
115 port,
116 )?;
117
118 let mut command = Vec::new();
119 match cmd.as_ref().map(|x| x.as_str()) {
120 Some("login") | None => {
121 command.push("login");
122 if userid == "root@pam" {
123 command.push("-f");
124 command.push("root");
125 }
126 }
127 Some("upgrade") => {
3d3670d7
TL
128 if userid != "root@pam" {
129 bail!("only root@pam can upgrade");
130 }
131 // TODO: add nicer/safer wrapper like in PVE instead
132 command.push("sh");
133 command.push("-c");
134 command.push("apt full-upgrade; bash -l");
1c2f842a
DC
135 }
136 _ => bail!("invalid command"),
137 };
138
139 let upid = WorkerTask::spawn(
140 "termproxy",
141 None,
142 &username,
143 false,
144 move |worker| async move {
145 // move inside the worker so that it survives and does not close the port
146 // remove CLOEXEC from listenere so that we can reuse it in termproxy
147 let fd = listener.as_raw_fd();
148 let mut flags = match fcntl(fd, FcntlArg::F_GETFD) {
149 Ok(bits) => FdFlag::from_bits_truncate(bits),
150 Err(err) => bail!("could not get fd: {}", err),
151 };
152 flags.remove(FdFlag::FD_CLOEXEC);
153 if let Err(err) = fcntl(fd, FcntlArg::F_SETFD(flags)) {
154 bail!("could not set fd: {}", err);
155 }
156
157 let mut arguments: Vec<&str> = Vec::new();
158 let fd_string = fd.to_string();
159 arguments.push(&fd_string);
160 arguments.extend_from_slice(&[
161 "--path",
162 &path,
163 "--perm",
164 "Sys.Console",
165 "--authport",
166 "82",
167 "--port-as-fd",
168 "--",
169 ]);
170 arguments.extend_from_slice(&command);
171
172 let mut cmd = tokio::process::Command::new("/usr/bin/termproxy");
173
224c65f8
DC
174 cmd.args(&arguments)
175 .kill_on_drop(true)
176 .stdout(std::process::Stdio::piped())
177 .stderr(std::process::Stdio::piped());
1c2f842a
DC
178
179 let mut child = cmd.spawn().expect("error executing termproxy");
180
181 let stdout = child.stdout.take().expect("no child stdout handle");
182 let stderr = child.stderr.take().expect("no child stderr handle");
183
184 let worker_stdout = worker.clone();
185 let stdout_fut = async move {
186 let mut reader = BufReader::new(stdout).lines();
187 while let Some(line) = reader.next_line().await? {
188 worker_stdout.log(line);
189 }
224c65f8 190 Ok::<(), Error>(())
1c2f842a
DC
191 };
192
193 let worker_stderr = worker.clone();
194 let stderr_fut = async move {
195 let mut reader = BufReader::new(stderr).lines();
196 while let Some(line) = reader.next_line().await? {
197 worker_stderr.warn(line);
198 }
224c65f8 199 Ok::<(), Error>(())
1c2f842a
DC
200 };
201
224c65f8
DC
202 select!{
203 res = child.fuse() => {
204 let exit_code = res?;
205 if !exit_code.success() {
206 match exit_code.code() {
207 Some(code) => bail!("termproxy exited with {}", code),
208 None => bail!("termproxy exited by signal"),
209 }
210 }
211 Ok(())
212 },
213 res = stdout_fut.fuse() => res,
214 res = stderr_fut.fuse() => res,
215 res = worker.abort_future().fuse() => res.map_err(Error::from),
1c2f842a 216 }
1c2f842a
DC
217 },
218 )?;
219
220 Ok(json!({
221 "user": username,
222 "ticket": ticket,
223 "port": port,
224 "upid": upid,
225 }))
226}
227
228#[sortable]
229pub const API_METHOD_WEBSOCKET: ApiMethod = ApiMethod::new(
230 &ApiHandler::AsyncHttp(&upgrade_to_websocket),
231 &ObjectSchema::new(
232 "Upgraded to websocket",
233 &sorted!([
234 ("node", false, &NODE_SCHEMA),
235 (
236 "vncticket",
237 false,
238 &StringSchema::new("Terminal ticket").schema()
239 ),
240 ("port", false, &IntegerSchema::new("Terminal port").schema()),
241 ]),
242 ),
243)
244.access(
e744de0e
TL
245 Some("The user needs Sys.Console on /system."),
246 &Permission::Privilege(&["system"], PRIV_SYS_CONSOLE, false),
1c2f842a
DC
247);
248
249fn upgrade_to_websocket(
250 parts: Parts,
251 req_body: Body,
252 param: Value,
253 _info: &ApiMethod,
254 rpcenv: Box<dyn RpcEnvironment>,
255) -> ApiResponseFuture {
256 async move {
257 let username = rpcenv.get_user().unwrap();
1c2f842a
DC
258 let ticket = tools::required_string_param(&param, "vncticket")?.to_owned();
259 let port: u16 = tools::required_integer_param(&param, "port")? as u16;
260
261 // will be checked again by termproxy
262 tools::ticket::verify_term_ticket(
263 crate::auth_helpers::public_auth_key(),
264 &username,
e744de0e 265 &"/system",
1c2f842a
DC
266 port,
267 &ticket,
268 )?;
269
270 let (ws, response) = WebSocket::new(parts.headers)?;
271
33a88daf 272 crate::server::spawn_internal_task(async move {
1c2f842a
DC
273 let conn: Upgraded = match req_body.on_upgrade().map_err(Error::from).await {
274 Ok(upgraded) => upgraded,
275 _ => bail!("error"),
276 };
277
278 let local = tokio::net::TcpStream::connect(format!("localhost:{}", port)).await?;
279 ws.serve_connection(conn, local).await
280 });
281
282 Ok(response)
283 }
284 .boxed()
285}
b2b3485d 286
255f378a 287pub const SUBDIRS: SubdirMap = &[
a4e86972 288 ("apt", &apt::ROUTER),
ce8e3de4 289 ("disks", &disks::ROUTER),
255f378a 290 ("dns", &dns::ROUTER),
81cc71c0 291 ("journal", &journal::ROUTER),
255f378a 292 ("network", &network::ROUTER),
a2f862ee 293 ("rrd", &rrd::ROUTER),
255f378a 294 ("services", &services::ROUTER),
2337df7b 295 ("status", &status::ROUTER),
113c9b59 296 ("subscription", &subscription::ROUTER),
255f378a
DM
297 ("syslog", &syslog::ROUTER),
298 ("tasks", &tasks::ROUTER),
1c2f842a 299 ("termproxy", &Router::new().post(&API_METHOD_TERMPROXY)),
255f378a 300 ("time", &time::ROUTER),
1c2f842a
DC
301 (
302 "vncwebsocket",
303 &Router::new().upgrade(&API_METHOD_WEBSOCKET),
304 ),
255f378a
DM
305];
306
307pub const ROUTER: Router = Router::new()
308 .get(&list_subdirs_api_method!(SUBDIRS))
309 .subdirs(SUBDIRS);