all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox-datacenter-manager 5/9] api: remote shell: add websocket endpoint
Date: Tue, 11 Nov 2025 09:29:28 +0100	[thread overview]
Message-ID: <20251111082938.221008-22-f.gruenbichler@proxmox.com> (raw)
In-Reply-To: <20251111082938.221008-1-f.gruenbichler@proxmox.com>

instead of directly forwarding to a running termproxy instance, PDM needs to
authenticate the incoming connection, create a new VNC ticket (and spawn a
termproxy instance) on the remote side, and forward the incoming connection to
the remote.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---

Notes:
    replaces previous patch which was PVE only

 Cargo.toml                     |   2 +-
 debian/control                 |  12 +--
 server/src/api/remote_shell.rs | 187 ++++++++++++++++++++++++++++++++-
 3 files changed, 191 insertions(+), 10 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 3252ccb..4328402 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,7 +40,7 @@ proxmox-auth-api = "1"
 proxmox-base64 = "1"
 proxmox-client = "1"
 proxmox-daemon = "1"
-proxmox-http = { version = "1", features = [ "client", "http-helpers", "websocket" ] } # see below
+proxmox-http = { version = "1.0.4", features = [ "client", "http-helpers", "websocket" ] } # see below
 proxmox-human-byte = "1"
 proxmox-io = "1.0.1" # tools and client use "tokio" feature
 proxmox-ldap = { version = "1.1", features = ["sync"] }
diff --git a/debian/control b/debian/control
index 19b13c8..e1e8c8f 100644
--- a/debian/control
+++ b/debian/control
@@ -47,12 +47,12 @@ Build-Depends: cargo:native,
                librust-proxmox-daemon-1+default-dev,
                librust-proxmox-dns-api-1+default-dev,
                librust-proxmox-dns-api-1+impl-dev,
-               librust-proxmox-http-1+client-dev,
-               librust-proxmox-http-1+client-trait-dev,
-               librust-proxmox-http-1+default-dev,
-               librust-proxmox-http-1+http-helpers-dev,
-               librust-proxmox-http-1+proxmox-async-dev,
-               librust-proxmox-http-1+websocket-dev,
+               librust-proxmox-http-1+client-dev (>= 1.0.4-~~),
+               librust-proxmox-http-1+client-trait-dev (>= 1.0.4-~~),
+               librust-proxmox-http-1+default-dev (>= 1.0.4-~~),
+               librust-proxmox-http-1+http-helpers-dev (>= 1.0.4-~~),
+               librust-proxmox-http-1+proxmox-async-dev (>= 1.0.4-~~),
+               librust-proxmox-http-1+websocket-dev (>= 1.0.4-~~),
                librust-proxmox-human-byte-1+default-dev,
                librust-proxmox-lang-1+default-dev (>= 1.1-~~),
                librust-proxmox-ldap-1+default-dev (>= 1.1-~~),
diff --git a/server/src/api/remote_shell.rs b/server/src/api/remote_shell.rs
index 380bbba..c617b4d 100644
--- a/server/src/api/remote_shell.rs
+++ b/server/src/api/remote_shell.rs
@@ -1,14 +1,30 @@
 use anyhow::{bail, format_err, Error};
+use futures::{FutureExt, TryFutureExt};
+use http::{
+    header::{SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_VERSION, UPGRADE},
+    request::Parts,
+    Method, Request, StatusCode,
+};
+use hyper::upgrade::Upgraded;
+use hyper_util::rt::TokioIo;
 use serde_json::{json, Value};
 
 use proxmox_auth_api::{
     ticket::{Empty, Ticket},
     Keyring,
 };
-use proxmox_router::{Permission, RpcEnvironment};
-use proxmox_schema::api;
+use proxmox_client::ApiPathBuilder;
+use proxmox_http::{websocket::WebSocket, Body};
+use proxmox_router::{ApiHandler, ApiMethod, ApiResponseFuture, Permission, RpcEnvironment};
+use proxmox_schema::{api, IntegerSchema, ObjectSchema, StringSchema};
+use proxmox_sortable_macro::sortable;
 
-use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, Authid, NODE_SCHEMA, PRIV_SYS_CONSOLE};
+use pdm_api_types::{
+    remotes::{RemoteType, REMOTE_ID_SCHEMA},
+    Authid, NODE_SCHEMA, PRIV_SYS_CONSOLE,
+};
+
+use crate::api::{nodes::vncwebsocket::required_string_param, remotes::get_remote};
 
 fn encode_term_ticket_path(remote: &str, node: &str) -> String {
     format!("/shell/{remote}/{node}")
@@ -78,3 +94,168 @@ pub(crate) async fn shell_ticket(
         "port": 0,
     }))
 }
+
+#[sortable]
+pub const API_METHOD_SHELL_WEBSOCKET: ApiMethod = ApiMethod::new(
+    &ApiHandler::AsyncHttp(&upgrade_to_websocket),
+    &ObjectSchema::new(
+        "Upgraded to websocket",
+        &sorted!([
+            ("remote", false, &REMOTE_ID_SCHEMA),
+            ("node", false, &NODE_SCHEMA),
+            (
+                "vncticket",
+                false,
+                &StringSchema::new("Terminal ticket").schema()
+            ),
+            ("port", false, &IntegerSchema::new("Terminal port").schema()),
+        ]),
+    ),
+)
+.access(
+    Some("The user needs Sys.Console on /resource/{remote}/node/{node}."),
+    &Permission::Privilege(
+        &["resource", "{remote}", "node", "{node}"],
+        PRIV_SYS_CONSOLE,
+        false,
+    ),
+);
+
+fn upgrade_to_websocket(
+    parts: Parts,
+    req_body: hyper::body::Incoming,
+    param: Value,
+    _info: &ApiMethod,
+    rpcenv: Box<dyn RpcEnvironment>,
+) -> ApiResponseFuture {
+    async move {
+        // intentionally user only for now
+        let auth_id: Authid = rpcenv
+            .get_auth_id()
+            .ok_or_else(|| format_err!("no authid available"))?
+            .parse()?;
+
+        if auth_id.is_token() {
+            bail!("API tokens cannot access this API endpoint");
+        }
+
+        let userid = auth_id.user();
+        let ticket = required_string_param(&param, "vncticket")?;
+
+        let public_auth_keyring =
+            Keyring::with_public_key(crate::auth::key::public_auth_key().clone());
+
+        let remote = required_string_param(&param, "remote")?.to_owned();
+        let node = required_string_param(&param, "node")?.to_owned();
+        let ticket_path = encode_term_ticket_path(&remote, &node);
+
+        Ticket::<Empty>::parse(ticket)?.verify(
+            &public_auth_keyring,
+            crate::auth::TERM_PREFIX,
+            Some(&format!("{}{}", userid, ticket_path)),
+        )?;
+
+        let (mut ws, response) = WebSocket::new(parts.headers.clone())?;
+
+        proxmox_rest_server::spawn_internal_task(async move {
+            let incoming_ws: Upgraded =
+                match hyper::upgrade::on(Request::from_parts(parts, req_body))
+                    .map_err(Error::from)
+                    .await
+                {
+                    Ok(upgraded) => upgraded,
+                    _ => bail!("error"),
+                };
+
+            let (remotes, _digest) = pdm_config::remotes::config()?;
+            let remote = get_remote(&remotes, &remote)?;
+            let (ticket, port) = match remote.ty {
+                RemoteType::Pve => {
+                    let pve = crate::connection::make_pve_client(&remote)?;
+                    let pve_term_ticket = pve
+                        .node_shell_termproxy(
+                            &node,
+                            pve_api_types::NodeShellTermproxy {
+                                cmd: None,
+                                cmd_opts: None,
+                            },
+                        )
+                        .await?;
+                    (pve_term_ticket.ticket, pve_term_ticket.port)
+                }
+                RemoteType::Pbs => {
+                    let pbs = crate::connection::make_pbs_client(&remote)?;
+                    let pbs_term_ticket = pbs.node_shell_termproxy().await?;
+                    (pbs_term_ticket.ticket, pbs_term_ticket.port as i64)
+                }
+            };
+
+            let raw_client = crate::connection::make_raw_client(remote)?;
+
+            let ws_key = proxmox_sys::linux::random_data(16)?;
+            let ws_key = proxmox_base64::encode(&ws_key);
+
+            let api_url = raw_client.api_url().clone().into_parts();
+
+            let mut builder = http::uri::Builder::new();
+            if let Some(scheme) = api_url.scheme {
+                builder = builder.scheme(scheme);
+            }
+            if let Some(authority) = api_url.authority {
+                builder = builder.authority(authority)
+            }
+            let api_path = ApiPathBuilder::new(format!("/api2/json/nodes/{node}/vncwebsocket"))
+                .arg("vncticket", ticket.clone())
+                .arg("port", port)
+                .build();
+            let uri = builder
+                .path_and_query(api_path)
+                .build()
+                .map_err(|err| format_err!("failed to build Uri - {err}"))?;
+
+            let auth = raw_client.login_auth()?;
+            let req = Request::builder()
+                .method(Method::GET)
+                .uri(uri)
+                .header(UPGRADE, "websocket")
+                .header(SEC_WEBSOCKET_VERSION, "13")
+                .header(SEC_WEBSOCKET_KEY, ws_key);
+
+            let req = auth.set_auth_headers(req).body(Body::empty())?;
+
+            let res = raw_client.http_client().request(req).await?;
+            if res.status() != StatusCode::SWITCHING_PROTOCOLS {
+                bail!("server didn't upgrade: {}", res.status());
+            }
+
+            let pve_ws = hyper::upgrade::on(res)
+                .await
+                .map_err(|err| format_err!("failed to upgrade - {}", err))?;
+
+            let username = if let proxmox_client::AuthenticationKind::Token(ref token) = *auth {
+                token.userid.clone()
+            } else {
+                bail!("shell not supported with ticket-based authentication")
+            };
+
+            let preamble = format!("{username}:{ticket}\n", ticket = ticket);
+            ws.mask = Some([0, 0, 0, 0]);
+
+            if let Err(err) = ws
+                .proxy_connection(
+                    TokioIo::new(incoming_ws),
+                    TokioIo::new(pve_ws),
+                    preamble.as_bytes(),
+                )
+                .await
+            {
+                log::warn!("error while copying between websockets: {err:?}");
+            }
+
+            Ok(())
+        });
+
+        Ok(response)
+    }
+    .boxed()
+}
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel

  parent reply	other threads:[~2025-11-11  8:30 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-11  8:29 [pdm-devel] [PATCH access-control/manager/proxmox{, -backup, -yew-comp, -datacenter-manager}/xtermjs 00/25] add remote node shell Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH pve-xtermjs 1/2] xtermjs: add support for remote node shells via PDM Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH pve-xtermjs 2/2] termproxy: allow using new vncticket endpoint Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH access-control 1/1] api: ticket: allow token-owned VNC ticket verification Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH manager 1/3] api: termproxy/vncwebsocket: allow tokens Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH manager 2/3] api: termproxy: add description to return schema Fabian Grünbichler
2025-11-13 10:38   ` Stefan Hanreich
2025-11-11  8:29 ` [pdm-devel] [PATCH manager 3/3] http server: allow unauthenticated access to /access/vncticket Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox 1/3] pbs-api-types: add NodeShellTicket Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox 2/3] auth-api: use Authid for path ticket validation Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox 3/3] auth-api: add vncticket verification endpoint and type Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-backup 1/4] tree-wide: user Userid::root_user() instead of hard-coded root@pam Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-backup 2/4] api: access: add vncticket verification endpoint Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-backup 3/4] api: node shell: allow access for tokens Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-backup 4/4] api: termproxy: use NodeShellTicket type from pbs-api-types Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-yew-comp 1/3] xtermjs: add remote PVE support Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-yew-comp 2/3] xtermjs: merge ConsoleType to parameters conversion Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-yew-comp 3/3] xtermjs: add remote PBS console type Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/9] auth: allow tokens in term tickets Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/9] connection: add access to "raw" client Fabian Grünbichler
2025-11-13 10:39   ` Stefan Hanreich
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/9] pbs client: add termproxy wrapper Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/9] api: add remote_shell module with termproxy endpoint Fabian Grünbichler
2025-11-11  8:29 ` Fabian Grünbichler [this message]
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-datacenter-manager 6/9] api: pve: wire up remote shell support Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-datacenter-manager 7/9] ui: pve: node: add shell tab Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-datacenter-manager 8/9] api: pbs: wire up node shell endpoints Fabian Grünbichler
2025-11-11  8:29 ` [pdm-devel] [PATCH proxmox-datacenter-manager 9/9] ui: add PBS remote shell button Fabian Grünbichler
2025-11-13 10:40 ` [pdm-devel] [PATCH access-control/manager/proxmox{, -backup, -yew-comp, -datacenter-manager}/xtermjs 00/25] add remote node shell Stefan Hanreich
2025-11-14 11:04 ` [pdm-devel] partially-applied: " Fabian Grünbichler

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251111082938.221008-22-f.gruenbichler@proxmox.com \
    --to=f.gruenbichler@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal