1 //! Server/Node Configuration and Administration
3 use std
::net
::TcpListener
;
4 use std
::os
::unix
::io
::AsRawFd
;
6 use anyhow
::{bail, format_err, Error}
;
7 use futures
::future
::{FutureExt, TryFutureExt}
;
9 use hyper
::http
::request
::Parts
;
10 use hyper
::upgrade
::Upgraded
;
12 use serde_json
::{json, Value}
;
13 use tokio
::io
::{AsyncBufReadExt, BufReader}
;
15 use proxmox
::{identity, sortable}
;
17 ApiHandler
, ApiMethod
, ApiResponseFuture
, Permission
, RpcEnvironment
, Router
, SubdirMap
,
19 use proxmox_schema
::*;
20 use proxmox_router
::list_subdirs_api_method
;
21 use proxmox_http
::websocket
::WebSocket
;
23 use proxmox_rest_server
::WorkerTask
;
25 use pbs_api_types
::{Authid, NODE_SCHEMA, PRIV_SYS_CONSOLE}
;
26 use pbs_tools
::ticket
::{self, Empty, Ticket}
;
29 use crate::auth_helpers
::private_auth_key
;
44 pub(crate) mod services
;
49 pub const SHELL_CMD_SCHEMA
: Schema
= StringSchema
::new("The command to run.")
50 .format(&ApiStringFormat
::Enum(&[
51 EnumEntry
::new("login", "Login"),
52 EnumEntry
::new("upgrade", "Upgrade"),
64 schema
: SHELL_CMD_SCHEMA
,
71 description
: "Object with the user, ticket, port and upid",
92 description
: "Restricted to users on realm 'pam'",
93 permission
: &Permission
::Privilege(&["system"], PRIV_SYS_CONSOLE
, false),
96 /// Call termproxy and return shell ticket
97 async
fn termproxy(cmd
: Option
<String
>, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
98 // intentionally user only for now
99 let auth_id
: Authid
= rpcenv
101 .ok_or_else(|| format_err
!("no authid available"))?
104 if auth_id
.is_token() {
105 bail
!("API tokens cannot access this API endpoint");
108 let userid
= auth_id
.user();
110 if userid
.realm() != "pam" {
111 bail
!("only pam users can use the console");
114 let path
= "/system";
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();
120 let ticket
= Ticket
::new(ticket
::TERM_PREFIX
, &Empty
)?
.sign(
122 Some(&tools
::ticket
::term_aad(&userid
, &path
, port
)),
125 let mut command
= Vec
::new();
126 match cmd
.as_deref() {
127 Some("login") | None
=> {
128 command
.push("login");
129 if userid
== "root@pam" {
131 command
.push("root");
135 if userid
!= "root@pam" {
136 bail
!("only root@pam can upgrade");
138 // TODO: add nicer/safer wrapper like in PVE instead
141 command
.push("apt full-upgrade; bash -l");
143 _
=> bail
!("invalid command"),
146 let username
= userid
.name().to_owned();
147 let upid
= WorkerTask
::spawn(
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
155 pbs_tools
::fd
::fd_change_cloexec(listener
.as_raw_fd(), false)?
;
157 let mut arguments
: Vec
<&str> = Vec
::new();
158 let fd_string
= listener
.as_raw_fd().to_string();
159 arguments
.push(&fd_string
);
160 arguments
.extend_from_slice(&[
170 arguments
.extend_from_slice(&command
);
172 let mut cmd
= tokio
::process
::Command
::new("/usr/bin/termproxy");
175 .stdout(std
::process
::Stdio
::piped())
176 .stderr(std
::process
::Stdio
::piped());
178 let mut child
= cmd
.spawn().expect("error executing termproxy");
180 let stdout
= child
.stdout
.take().expect("no child stdout handle");
181 let stderr
= child
.stderr
.take().expect("no child stderr handle");
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_message(line
);
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
.log_warning(line
);
201 let mut needs_kill
= false;
202 let res
= tokio
::select
! {
203 res
= child
.wait() => {
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"),
213 res
= stdout_fut
=> res
,
214 res
= stderr_fut
=> res
,
215 res
= worker
.abort_future() => {
217 res
.map_err(Error
::from
)
227 if let Err(err
) = child
.kill().await
{
228 worker
.log_warning(format
!("error killing termproxy: {}", err
));
229 } else if let Err(err
) = child
.wait().await
{
230 worker
.log_warning(format
!("error awaiting termproxy: {}", err
));
238 // FIXME: We're returning the user NAME only?
248 pub const API_METHOD_WEBSOCKET
: ApiMethod
= ApiMethod
::new(
249 &ApiHandler
::AsyncHttp(&upgrade_to_websocket
),
251 "Upgraded to websocket",
253 ("node", false, &NODE_SCHEMA
),
257 &StringSchema
::new("Terminal ticket").schema()
259 ("port", false, &IntegerSchema
::new("Terminal port").schema()),
264 Some("The user needs Sys.Console on /system."),
265 &Permission
::Privilege(&["system"], PRIV_SYS_CONSOLE
, false),
268 fn upgrade_to_websocket(
273 rpcenv
: Box
<dyn RpcEnvironment
>,
274 ) -> ApiResponseFuture
{
276 // intentionally user only for now
277 let auth_id
: Authid
= rpcenv
279 .ok_or_else(|| format_err
!("no authid available"))?
282 if auth_id
.is_token() {
283 bail
!("API tokens cannot access this API endpoint");
286 let userid
= auth_id
.user();
287 let ticket
= pbs_tools
::json
::required_string_param(¶m
, "vncticket")?
;
288 let port
: u16 = pbs_tools
::json
::required_integer_param(¶m
, "port")?
as u16;
290 // will be checked again by termproxy
291 Ticket
::<Empty
>::parse(ticket
)?
.verify(
292 crate::auth_helpers
::public_auth_key(),
294 Some(&tools
::ticket
::term_aad(&userid
, "/system", port
)),
297 let (ws
, response
) = WebSocket
::new(parts
.headers
.clone())?
;
299 proxmox_rest_server
::spawn_internal_task(async
move {
300 let conn
: Upgraded
= match hyper
::upgrade
::on(Request
::from_parts(parts
, req_body
))
301 .map_err(Error
::from
)
304 Ok(upgraded
) => upgraded
,
308 let local
= tokio
::net
::TcpStream
::connect(format
!("localhost:{}", port
)).await?
;
309 ws
.serve_connection(conn
, local
).await
318 /// List Nodes (only for compatiblity)
319 fn list_nodes() -> Result
<Value
, Error
> {
320 Ok(json
!([ { "node": proxmox::tools::nodename().to_string() }
]))
323 pub const SUBDIRS
: SubdirMap
= &[
324 ("apt", &apt
::ROUTER
),
325 ("certificates", &certificates
::ROUTER
),
326 ("config", &config
::ROUTER
),
327 ("disks", &disks
::ROUTER
),
328 ("dns", &dns
::ROUTER
),
329 ("journal", &journal
::ROUTER
),
330 ("network", &network
::ROUTER
),
331 ("report", &report
::ROUTER
),
332 ("rrd", &rrd
::ROUTER
),
333 ("services", &services
::ROUTER
),
334 ("status", &status
::ROUTER
),
335 ("subscription", &subscription
::ROUTER
),
336 ("syslog", &syslog
::ROUTER
),
337 ("tasks", &tasks
::ROUTER
),
338 ("termproxy", &Router
::new().post(&API_METHOD_TERMPROXY
)),
339 ("time", &time
::ROUTER
),
342 &Router
::new().upgrade(&API_METHOD_WEBSOCKET
),
346 pub const ITEM_ROUTER
: Router
= Router
::new()
347 .get(&list_subdirs_api_method
!(SUBDIRS
))
350 pub const ROUTER
: Router
= Router
::new()
351 .get(&API_METHOD_LIST_NODES
)
352 .match_all("node", &ITEM_ROUTER
);