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
::api
::router
::{Router, SubdirMap}
;
17 api
, schema
::*, ApiHandler
, ApiMethod
, ApiResponseFuture
, Permission
, RpcEnvironment
,
19 use proxmox
::list_subdirs_api_method
;
20 use proxmox
::{identity, sortable}
;
21 use proxmox_http
::websocket
::WebSocket
;
23 use pbs_api_types
::{Authid, NODE_SCHEMA, PRIV_SYS_CONSOLE}
;
24 use pbs_tools
::auth
::private_auth_key
;
25 use pbs_tools
::ticket
::{self, Empty, Ticket}
;
27 use crate::server
::WorkerTask
;
43 pub(crate) mod services
;
48 pub const SHELL_CMD_SCHEMA
: Schema
= StringSchema
::new("The command to run.")
49 .format(&ApiStringFormat
::Enum(&[
50 EnumEntry
::new("login", "Login"),
51 EnumEntry
::new("upgrade", "Upgrade"),
63 schema
: SHELL_CMD_SCHEMA
,
70 description
: "Object with the user, ticket, port and upid",
91 description
: "Restricted to users on realm 'pam'",
92 permission
: &Permission
::Privilege(&["system"], PRIV_SYS_CONSOLE
, false),
95 /// Call termproxy and return shell ticket
96 async
fn termproxy(cmd
: Option
<String
>, rpcenv
: &mut dyn RpcEnvironment
) -> Result
<Value
, Error
> {
97 // intentionally user only for now
98 let auth_id
: Authid
= rpcenv
100 .ok_or_else(|| format_err
!("no authid available"))?
103 if auth_id
.is_token() {
104 bail
!("API tokens cannot access this API endpoint");
107 let userid
= auth_id
.user();
109 if userid
.realm() != "pam" {
110 bail
!("only pam users can use the console");
113 let path
= "/system";
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();
119 let ticket
= Ticket
::new(ticket
::TERM_PREFIX
, &Empty
)?
.sign(
121 Some(&tools
::ticket
::term_aad(&userid
, &path
, port
)),
124 let mut command
= Vec
::new();
125 match cmd
.as_deref() {
126 Some("login") | None
=> {
127 command
.push("login");
128 if userid
== "root@pam" {
130 command
.push("root");
134 if userid
!= "root@pam" {
135 bail
!("only root@pam can upgrade");
137 // TODO: add nicer/safer wrapper like in PVE instead
140 command
.push("apt full-upgrade; bash -l");
142 _
=> bail
!("invalid command"),
145 let username
= userid
.name().to_owned();
146 let upid
= WorkerTask
::spawn(
151 move |worker
| async
move {
152 // move inside the worker so that it survives and does not close the port
153 // remove CLOEXEC from listenere so that we can reuse it in termproxy
154 tools
::fd_change_cloexec(listener
.as_raw_fd(), false)?
;
156 let mut arguments
: Vec
<&str> = Vec
::new();
157 let fd_string
= listener
.as_raw_fd().to_string();
158 arguments
.push(&fd_string
);
159 arguments
.extend_from_slice(&[
169 arguments
.extend_from_slice(&command
);
171 let mut cmd
= tokio
::process
::Command
::new("/usr/bin/termproxy");
174 .stdout(std
::process
::Stdio
::piped())
175 .stderr(std
::process
::Stdio
::piped());
177 let mut child
= cmd
.spawn().expect("error executing termproxy");
179 let stdout
= child
.stdout
.take().expect("no child stdout handle");
180 let stderr
= child
.stderr
.take().expect("no child stderr handle");
182 let worker_stdout
= worker
.clone();
183 let stdout_fut
= async
move {
184 let mut reader
= BufReader
::new(stdout
).lines();
185 while let Some(line
) = reader
.next_line().await?
{
186 worker_stdout
.log(line
);
191 let worker_stderr
= worker
.clone();
192 let stderr_fut
= async
move {
193 let mut reader
= BufReader
::new(stderr
).lines();
194 while let Some(line
) = reader
.next_line().await?
{
195 worker_stderr
.warn(line
);
200 let mut needs_kill
= false;
201 let res
= tokio
::select
! {
202 res
= child
.wait() => {
203 let exit_code
= res?
;
204 if !exit_code
.success() {
205 match exit_code
.code() {
206 Some(code
) => bail
!("termproxy exited with {}", code
),
207 None
=> bail
!("termproxy exited by signal"),
212 res
= stdout_fut
=> res
,
213 res
= stderr_fut
=> res
,
214 res
= worker
.abort_future() => {
216 res
.map_err(Error
::from
)
226 if let Err(err
) = child
.kill().await
{
227 worker
.warn(format
!("error killing termproxy: {}", err
));
228 } else if let Err(err
) = child
.wait().await
{
229 worker
.warn(format
!("error awaiting termproxy: {}", err
));
237 // FIXME: We're returning the user NAME only?
247 pub const API_METHOD_WEBSOCKET
: ApiMethod
= ApiMethod
::new(
248 &ApiHandler
::AsyncHttp(&upgrade_to_websocket
),
250 "Upgraded to websocket",
252 ("node", false, &NODE_SCHEMA
),
256 &StringSchema
::new("Terminal ticket").schema()
258 ("port", false, &IntegerSchema
::new("Terminal port").schema()),
263 Some("The user needs Sys.Console on /system."),
264 &Permission
::Privilege(&["system"], PRIV_SYS_CONSOLE
, false),
267 fn upgrade_to_websocket(
272 rpcenv
: Box
<dyn RpcEnvironment
>,
273 ) -> ApiResponseFuture
{
275 // intentionally user only for now
276 let auth_id
: Authid
= rpcenv
278 .ok_or_else(|| format_err
!("no authid available"))?
281 if auth_id
.is_token() {
282 bail
!("API tokens cannot access this API endpoint");
285 let userid
= auth_id
.user();
286 let ticket
= pbs_tools
::json
::required_string_param(¶m
, "vncticket")?
;
287 let port
: u16 = pbs_tools
::json
::required_integer_param(¶m
, "port")?
as u16;
289 // will be checked again by termproxy
290 Ticket
::<Empty
>::parse(ticket
)?
.verify(
291 crate::auth_helpers
::public_auth_key(),
293 Some(&tools
::ticket
::term_aad(&userid
, "/system", port
)),
296 let (ws
, response
) = WebSocket
::new(parts
.headers
.clone())?
;
298 proxmox_rest_server
::spawn_internal_task(async
move {
299 let conn
: Upgraded
= match hyper
::upgrade
::on(Request
::from_parts(parts
, req_body
))
300 .map_err(Error
::from
)
303 Ok(upgraded
) => upgraded
,
307 let local
= tokio
::net
::TcpStream
::connect(format
!("localhost:{}", port
)).await?
;
308 ws
.serve_connection(conn
, local
).await
317 /// List Nodes (only for compatiblity)
318 fn list_nodes() -> Result
<Value
, Error
> {
319 Ok(json
!([ { "node": proxmox::tools::nodename().to_string() }
]))
322 pub const SUBDIRS
: SubdirMap
= &[
323 ("apt", &apt
::ROUTER
),
324 ("certificates", &certificates
::ROUTER
),
325 ("config", &config
::ROUTER
),
326 ("disks", &disks
::ROUTER
),
327 ("dns", &dns
::ROUTER
),
328 ("journal", &journal
::ROUTER
),
329 ("network", &network
::ROUTER
),
330 ("report", &report
::ROUTER
),
331 ("rrd", &rrd
::ROUTER
),
332 ("services", &services
::ROUTER
),
333 ("status", &status
::ROUTER
),
334 ("subscription", &subscription
::ROUTER
),
335 ("syslog", &syslog
::ROUTER
),
336 ("tasks", &tasks
::ROUTER
),
337 ("termproxy", &Router
::new().post(&API_METHOD_TERMPROXY
)),
338 ("time", &time
::ROUTER
),
341 &Router
::new().upgrade(&API_METHOD_WEBSOCKET
),
345 pub const ITEM_ROUTER
: Router
= Router
::new()
346 .get(&list_subdirs_api_method
!(SUBDIRS
))
349 pub const ROUTER
: Router
= Router
::new()
350 .get(&API_METHOD_LIST_NODES
)
351 .match_all("node", &ITEM_ROUTER
);