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