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