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