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