]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/node.rs
introduce Ticket struct
[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> {
e7cb4dc5 93 let userid: Userid = rpcenv
1c2f842a 94 .get_user()
e7cb4dc5
WB
95 .ok_or_else(|| format_err!("unknown user"))?
96 .parse()?;
1c2f842a 97
e7cb4dc5 98 if userid.realm() != "pam" {
1c2f842a
DC
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
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()?;
1c2f842a
DC
276 let ticket = tools::required_string_param(&param, "vncticket")?.to_owned();
277 let port: u16 = tools::required_integer_param(&param, "port")? as u16;
278
279 // will be checked again by termproxy
280 tools::ticket::verify_term_ticket(
281 crate::auth_helpers::public_auth_key(),
e7cb4dc5 282 &userid,
e744de0e 283 &"/system",
1c2f842a
DC
284 port,
285 &ticket,
286 )?;
287
288 let (ws, response) = WebSocket::new(parts.headers)?;
289
33a88daf 290 crate::server::spawn_internal_task(async move {
1c2f842a
DC
291 let conn: Upgraded = match req_body.on_upgrade().map_err(Error::from).await {
292 Ok(upgraded) => upgraded,
293 _ => bail!("error"),
294 };
295
296 let local = tokio::net::TcpStream::connect(format!("localhost:{}", port)).await?;
297 ws.serve_connection(conn, local).await
298 });
299
300 Ok(response)
301 }
302 .boxed()
303}
b2b3485d 304
255f378a 305pub const SUBDIRS: SubdirMap = &[
a4e86972 306 ("apt", &apt::ROUTER),
ce8e3de4 307 ("disks", &disks::ROUTER),
255f378a 308 ("dns", &dns::ROUTER),
81cc71c0 309 ("journal", &journal::ROUTER),
255f378a 310 ("network", &network::ROUTER),
a2f862ee 311 ("rrd", &rrd::ROUTER),
255f378a 312 ("services", &services::ROUTER),
2337df7b 313 ("status", &status::ROUTER),
113c9b59 314 ("subscription", &subscription::ROUTER),
255f378a
DM
315 ("syslog", &syslog::ROUTER),
316 ("tasks", &tasks::ROUTER),
1c2f842a 317 ("termproxy", &Router::new().post(&API_METHOD_TERMPROXY)),
255f378a 318 ("time", &time::ROUTER),
1c2f842a
DC
319 (
320 "vncwebsocket",
321 &Router::new().upgrade(&API_METHOD_WEBSOCKET),
322 ),
255f378a
DM
323];
324
325pub const ROUTER: Router = Router::new()
326 .get(&list_subdirs_api_method!(SUBDIRS))
327 .subdirs(SUBDIRS);