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}
;
16 use proxmox
::tools
::fd
::fd_change_cloexec
;
19 ApiHandler
, ApiMethod
, ApiResponseFuture
, Permission
, RpcEnvironment
, Router
, SubdirMap
,
21 use proxmox_schema
::*;
22 use proxmox_router
::list_subdirs_api_method
;
23 use proxmox_http
::websocket
::WebSocket
;
25 use proxmox_rest_server
::WorkerTask
;
27 use pbs_api_types
::{Authid, NODE_SCHEMA, PRIV_SYS_CONSOLE}
;
28 use pbs_tools
::ticket
::{self, Empty, Ticket}
;
31 use crate::auth_helpers
::private_auth_key
;
46 pub(crate) mod services
;
51 pub const SHELL_CMD_SCHEMA
: Schema
= StringSchema
::new("The command to run.")
52 .format(&ApiStringFormat
::Enum(&[
53 EnumEntry
::new("login", "Login"),
54 EnumEntry
::new("upgrade", "Upgrade"),
66 schema
: SHELL_CMD_SCHEMA
,
73 description
: "Object with the user, ticket, port and upid",
94 description
: "Restricted to users on realm 'pam'",
95 permission
: &Permission
::Privilege(&["system"], PRIV_SYS_CONSOLE
, false),
98 /// Call termproxy and return shell ticket
99 async
fn termproxy(cmd
: Option
<String
>, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
100 // intentionally user only for now
101 let auth_id
: Authid
= rpcenv
103 .ok_or_else(|| format_err
!("no authid available"))?
106 if auth_id
.is_token() {
107 bail
!("API tokens cannot access this API endpoint");
110 let userid
= auth_id
.user();
112 if userid
.realm() != "pam" {
113 bail
!("only pam users can use the console");
116 let path
= "/system";
118 // use port 0 and let the kernel decide which port is free
119 let listener
= TcpListener
::bind("localhost:0")?
;
120 let port
= listener
.local_addr()?
.port();
122 let ticket
= Ticket
::new(ticket
::TERM_PREFIX
, &Empty
)?
.sign(
124 Some(&tools
::ticket
::term_aad(&userid
, &path
, port
)),
127 let mut command
= Vec
::new();
128 match cmd
.as_deref() {
129 Some("login") | None
=> {
130 command
.push("login");
131 if userid
== "root@pam" {
133 command
.push("root");
137 if userid
!= "root@pam" {
138 bail
!("only root@pam can upgrade");
140 // TODO: add nicer/safer wrapper like in PVE instead
143 command
.push("apt full-upgrade; bash -l");
145 _
=> bail
!("invalid command"),
148 let username
= userid
.name().to_owned();
149 let upid
= WorkerTask
::spawn(
154 move |worker
| async
move {
155 // move inside the worker so that it survives and does not close the port
156 // remove CLOEXEC from listenere so that we can reuse it in termproxy
157 fd_change_cloexec(listener
.as_raw_fd(), false)?
;
159 let mut arguments
: Vec
<&str> = Vec
::new();
160 let fd_string
= listener
.as_raw_fd().to_string();
161 arguments
.push(&fd_string
);
162 arguments
.extend_from_slice(&[
172 arguments
.extend_from_slice(&command
);
174 let mut cmd
= tokio
::process
::Command
::new("/usr/bin/termproxy");
177 .stdout(std
::process
::Stdio
::piped())
178 .stderr(std
::process
::Stdio
::piped());
180 let mut child
= cmd
.spawn().expect("error executing termproxy");
182 let stdout
= child
.stdout
.take().expect("no child stdout handle");
183 let stderr
= child
.stderr
.take().expect("no child stderr handle");
185 let worker_stdout
= worker
.clone();
186 let stdout_fut
= async
move {
187 let mut reader
= BufReader
::new(stdout
).lines();
188 while let Some(line
) = reader
.next_line().await?
{
189 worker_stdout
.log_message(line
);
194 let worker_stderr
= worker
.clone();
195 let stderr_fut
= async
move {
196 let mut reader
= BufReader
::new(stderr
).lines();
197 while let Some(line
) = reader
.next_line().await?
{
198 worker_stderr
.log_warning(line
);
203 let mut needs_kill
= false;
204 let res
= tokio
::select
! {
205 res
= child
.wait() => {
206 let exit_code
= res?
;
207 if !exit_code
.success() {
208 match exit_code
.code() {
209 Some(code
) => bail
!("termproxy exited with {}", code
),
210 None
=> bail
!("termproxy exited by signal"),
215 res
= stdout_fut
=> res
,
216 res
= stderr_fut
=> res
,
217 res
= worker
.abort_future() => {
219 res
.map_err(Error
::from
)
229 if let Err(err
) = child
.kill().await
{
230 worker
.log_warning(format
!("error killing termproxy: {}", err
));
231 } else if let Err(err
) = child
.wait().await
{
232 worker
.log_warning(format
!("error awaiting termproxy: {}", err
));
240 // FIXME: We're returning the user NAME only?
250 pub const API_METHOD_WEBSOCKET
: ApiMethod
= ApiMethod
::new(
251 &ApiHandler
::AsyncHttp(&upgrade_to_websocket
),
253 "Upgraded to websocket",
255 ("node", false, &NODE_SCHEMA
),
259 &StringSchema
::new("Terminal ticket").schema()
261 ("port", false, &IntegerSchema
::new("Terminal port").schema()),
266 Some("The user needs Sys.Console on /system."),
267 &Permission
::Privilege(&["system"], PRIV_SYS_CONSOLE
, false),
270 fn upgrade_to_websocket(
275 rpcenv
: Box
<dyn RpcEnvironment
>,
276 ) -> ApiResponseFuture
{
278 // intentionally user only for now
279 let auth_id
: Authid
= rpcenv
281 .ok_or_else(|| format_err
!("no authid available"))?
284 if auth_id
.is_token() {
285 bail
!("API tokens cannot access this API endpoint");
288 let userid
= auth_id
.user();
289 let ticket
= pbs_tools
::json
::required_string_param(¶m
, "vncticket")?
;
290 let port
: u16 = pbs_tools
::json
::required_integer_param(¶m
, "port")?
as u16;
292 // will be checked again by termproxy
293 Ticket
::<Empty
>::parse(ticket
)?
.verify(
294 crate::auth_helpers
::public_auth_key(),
296 Some(&tools
::ticket
::term_aad(&userid
, "/system", port
)),
299 let (ws
, response
) = WebSocket
::new(parts
.headers
.clone())?
;
301 proxmox_rest_server
::spawn_internal_task(async
move {
302 let conn
: Upgraded
= match hyper
::upgrade
::on(Request
::from_parts(parts
, req_body
))
303 .map_err(Error
::from
)
306 Ok(upgraded
) => upgraded
,
310 let local
= tokio
::net
::TcpStream
::connect(format
!("localhost:{}", port
)).await?
;
311 ws
.serve_connection(conn
, local
).await
320 /// List Nodes (only for compatiblity)
321 fn list_nodes() -> Result
<Value
, Error
> {
322 Ok(json
!([ { "node": proxmox::tools::nodename().to_string() }
]))
325 pub const SUBDIRS
: SubdirMap
= &[
326 ("apt", &apt
::ROUTER
),
327 ("certificates", &certificates
::ROUTER
),
328 ("config", &config
::ROUTER
),
329 ("disks", &disks
::ROUTER
),
330 ("dns", &dns
::ROUTER
),
331 ("journal", &journal
::ROUTER
),
332 ("network", &network
::ROUTER
),
333 ("report", &report
::ROUTER
),
334 ("rrd", &rrd
::ROUTER
),
335 ("services", &services
::ROUTER
),
336 ("status", &status
::ROUTER
),
337 ("subscription", &subscription
::ROUTER
),
338 ("syslog", &syslog
::ROUTER
),
339 ("tasks", &tasks
::ROUTER
),
340 ("termproxy", &Router
::new().post(&API_METHOD_TERMPROXY
)),
341 ("time", &time
::ROUTER
),
344 &Router
::new().upgrade(&API_METHOD_WEBSOCKET
),
348 pub const ITEM_ROUTER
: Router
= Router
::new()
349 .get(&list_subdirs_api_method
!(SUBDIRS
))
352 pub const ROUTER
: Router
= Router
::new()
353 .get(&API_METHOD_LIST_NODES
)
354 .match_all("node", &ITEM_ROUTER
);