public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pbs-devel] [PATCH proxmox-backup 0/5] implement web console
@ 2020-07-21  9:10 Dominik Csapak
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 1/5] server/config: add mechanism to update template Dominik Csapak
                   ` (5 more replies)
  0 siblings, 6 replies; 7+ messages in thread
From: Dominik Csapak @ 2020-07-21  9:10 UTC (permalink / raw)
  To: pbs-devel

this series implements the necessary changes to access a shell via xtermjs

this replaces my previous series 'prerequisites for console' [0]

it needs an installed pve-xtermjs package with my latest patches [1]
as well as an bumped proxmox crate (updated version in Cargo.toml as well)

0: https://lists.proxmox.com/pipermail/pbs-devel/2020-July/000080.html
1: https://lists.proxmox.com/pipermail/pbs-devel/2020-July/000108.html

Dominik Csapak (5):
  server/config: add mechanism to update template
  api2/access: implement term ticket
  api2/nodes: add termproxy and vncwebsocket api calls
  server/rest: add console to index
  ui: add Console Button

 Cargo.toml                      |   6 +-
 src/api2/access.rs              |  79 +++++++--
 src/api2/node.rs                | 276 +++++++++++++++++++++++++++++++-
 src/bin/proxmox-backup-proxy.rs |   7 +-
 src/config/acl.rs               |   2 +
 src/server/config.rs            |  67 +++++++-
 src/server/rest.rs              |  18 ++-
 src/tools/ticket.rs             |  32 ++++
 www/ServerStatus.js             |  10 +-
 9 files changed, 460 insertions(+), 37 deletions(-)

-- 
2.20.1





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [pbs-devel] [PATCH proxmox-backup 1/5] server/config: add mechanism to update template
  2020-07-21  9:10 [pbs-devel] [PATCH proxmox-backup 0/5] implement web console Dominik Csapak
@ 2020-07-21  9:10 ` Dominik Csapak
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 2/5] api2/access: implement term ticket Dominik Csapak
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2020-07-21  9:10 UTC (permalink / raw)
  To: pbs-devel

instead of exposing handlebars itself, offer a register_template and
a render_template ourselves.

render_template checks if the template file was modified since
the last render and reloads it when necessary

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/bin/proxmox-backup-proxy.rs |  6 ++-
 src/server/config.rs            | 67 +++++++++++++++++++++++++++++----
 src/server/rest.rs              | 13 +++----
 3 files changed, 70 insertions(+), 16 deletions(-)

diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs
index 75f53b9..1e93886 100644
--- a/src/bin/proxmox-backup-proxy.rs
+++ b/src/bin/proxmox-backup-proxy.rs
@@ -1,5 +1,5 @@
 use std::sync::Arc;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 
 use anyhow::{bail, format_err, Error};
 use futures::*;
@@ -53,6 +53,10 @@ async fn run() -> Result<(), Error> {
     config.add_alias("css", "/usr/share/javascript/proxmox-backup/css");
     config.add_alias("docs", "/usr/share/doc/proxmox-backup/html");
 
+    let mut indexpath = PathBuf::from(buildcfg::JS_DIR);
+    indexpath.push("index.hbs");
+    config.register_template("index", &indexpath)?;
+
     let rest_server = RestServer::new(config);
 
     //openssl req -x509 -newkey rsa:4096 -keyout /etc/proxmox-backup/proxy.key -out /etc/proxmox-backup/proxy.pem -nodes
diff --git a/src/server/config.rs b/src/server/config.rs
index e8b3c94..3ee4ea1 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -1,9 +1,13 @@
 use std::collections::HashMap;
-use std::path::{PathBuf};
-use anyhow::Error;
+use std::path::PathBuf;
+use std::time::SystemTime;
+use std::fs::metadata;
+use std::sync::RwLock;
 
+use anyhow::{bail, Error, format_err};
 use hyper::Method;
 use handlebars::Handlebars;
+use serde::Serialize;
 
 use proxmox::api::{ApiMethod, Router, RpcEnvironmentType};
 
@@ -12,21 +16,20 @@ pub struct ApiConfig {
     router: &'static Router,
     aliases: HashMap<String, PathBuf>,
     env_type: RpcEnvironmentType,
-    pub templates: Handlebars<'static>,
+    templates: RwLock<Handlebars<'static>>,
+    template_files: RwLock<HashMap<String, (SystemTime, PathBuf)>>,
 }
 
 impl ApiConfig {
 
     pub fn new<B: Into<PathBuf>>(basedir: B, router: &'static Router, env_type: RpcEnvironmentType) -> Result<Self, Error> {
-        let mut templates = Handlebars::new();
-        let basedir = basedir.into();
-        templates.register_template_file("index", basedir.join("index.hbs"))?;
         Ok(Self {
-            basedir,
+            basedir: basedir.into(),
             router,
             aliases: HashMap::new(),
             env_type,
-            templates
+            templates: RwLock::new(Handlebars::new()),
+            template_files: RwLock::new(HashMap::new()),
         })
     }
 
@@ -67,4 +70,52 @@ impl ApiConfig {
     pub fn env_type(&self) -> RpcEnvironmentType {
         self.env_type
     }
+
+    pub fn register_template<P>(&self, name: &str, path: P) -> Result<(), Error>
+    where
+        P: Into<PathBuf>
+    {
+        if self.template_files.read().unwrap().contains_key(name) {
+            bail!("template already registered");
+        }
+
+        let path: PathBuf = path.into();
+        let metadata = metadata(&path)?;
+        let mtime = metadata.modified()?;
+
+        self.templates.write().unwrap().register_template_file(name, &path)?;
+        self.template_files.write().unwrap().insert(name.to_string(), (mtime, path));
+
+        Ok(())
+    }
+
+    /// Checks if the template was modified since the last rendering
+    /// if yes, it loads a the new version of the template
+    pub fn render_template<T>(&self, name: &str, data: &T) -> Result<String, Error>
+    where
+        T: Serialize,
+    {
+        let path;
+        let mtime;
+        {
+            let template_files = self.template_files.read().unwrap();
+            let (old_mtime, old_path) = template_files.get(name).ok_or_else(|| format_err!("template not found"))?;
+
+            mtime = metadata(old_path)?.modified()?;
+            if mtime <= *old_mtime {
+                return self.templates.read().unwrap().render(name, data).map_err(|err| format_err!("{}", err));
+            }
+            path = old_path.to_path_buf();
+        }
+
+        {
+            let mut template_files = self.template_files.write().unwrap();
+            let mut templates = self.templates.write().unwrap();
+
+            templates.register_template_file(name, &path)?;
+            template_files.insert(name.to_string(), (mtime, path));
+
+            templates.render(name, data).map_err(|err| format_err!("{}", err))
+        }
+    }
 }
diff --git a/src/server/rest.rs b/src/server/rest.rs
index d05e51a..a7b0a23 100644
--- a/src/server/rest.rs
+++ b/src/server/rest.rs
@@ -16,7 +16,6 @@ use serde_json::{json, Value};
 use tokio::fs::File;
 use tokio::time::Instant;
 use url::form_urlencoded;
-use handlebars::Handlebars;
 
 use proxmox::http_err;
 use proxmox::api::{ApiHandler, ApiMethod, HttpError};
@@ -312,7 +311,7 @@ pub async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher +
     Ok(resp)
 }
 
-fn get_index(username: Option<String>, token: Option<String>, template: &Handlebars, parts: Parts) ->  Response<Body> {
+fn get_index(username: Option<String>, token: Option<String>, api: &Arc<ApiConfig>, parts: Parts) ->  Response<Body> {
 
     let nodename = proxmox::tools::nodename();
     let username = username.unwrap_or_else(|| String::from(""));
@@ -338,11 +337,11 @@ fn get_index(username: Option<String>, token: Option<String>, template: &Handleb
 
     let mut ct = "text/html";
 
-    let index = match template.render("index", &data) {
+    let index = match api.render_template("index", &data) {
         Ok(index) => index,
         Err(err) => {
             ct = "text/plain";
-            format!("Error rendering template: {}", err.desc)
+            format!("Error rendering template: {}", err)
         },
     };
 
@@ -580,15 +579,15 @@ pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<R
                 match check_auth(&method, &ticket, &token, &user_info) {
                     Ok(username) => {
                         let new_token = assemble_csrf_prevention_token(csrf_secret(), &username);
-                        return Ok(get_index(Some(username), Some(new_token), &api.templates, parts));
+                        return Ok(get_index(Some(username), Some(new_token), &api, parts));
                     }
                     _ => {
                         tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await;
-                        return Ok(get_index(None, None, &api.templates, parts));
+                        return Ok(get_index(None, None, &api, parts));
                     }
                 }
             } else {
-                return Ok(get_index(None, None, &api.templates, parts));
+                return Ok(get_index(None, None, &api, parts));
             }
         } else {
             let filename = api.find_alias(&components);
-- 
2.20.1





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [pbs-devel] [PATCH proxmox-backup 2/5] api2/access: implement term ticket
  2020-07-21  9:10 [pbs-devel] [PATCH proxmox-backup 0/5] implement web console Dominik Csapak
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 1/5] server/config: add mechanism to update template Dominik Csapak
@ 2020-07-21  9:10 ` Dominik Csapak
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 3/5] api2/nodes: add termproxy and vncwebsocket api calls Dominik Csapak
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2020-07-21  9:10 UTC (permalink / raw)
  To: pbs-devel

modeled after pves/pmgs vncticket (i substituted the vnc with term)
by putting the path and username as secret data in the ticket

when sending the ticket to /access/ticket it only verifies it,
checks the privs on the path and does not generate a new ticket

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from last version:
* include the port in the termticket
* use rstfmt
 src/api2/access.rs  | 79 +++++++++++++++++++++++++++++++++++++++------
 src/tools/ticket.rs | 32 ++++++++++++++++++
 2 files changed, 101 insertions(+), 10 deletions(-)

diff --git a/src/api2/access.rs b/src/api2/access.rs
index f5855ed..46fbd99 100644
--- a/src/api2/access.rs
+++ b/src/api2/access.rs
@@ -13,15 +13,22 @@ use crate::auth_helpers::*;
 use crate::api2::types::*;
 
 use crate::config::cached_user_info::CachedUserInfo;
-use crate::config::acl::PRIV_PERMISSIONS_MODIFY;
+use crate::config::acl::{PRIVILEGES, PRIV_PERMISSIONS_MODIFY};
 
 pub mod user;
 pub mod domain;
 pub mod acl;
 pub mod role;
 
-fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
-
+/// returns Ok(true) if a ticket has to be created
+/// and Ok(false) if not
+fn authenticate_user(
+    username: &str,
+    password: &str,
+    path: Option<String>,
+    privs: Option<String>,
+    port: Option<u16>,
+) -> Result<bool, Error> {
     let user_info = CachedUserInfo::new()?;
 
     if !user_info.is_active_user(&username) {
@@ -33,14 +40,43 @@ fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
     if password.starts_with("PBS:") {
         if let Ok((_age, Some(ticket_username))) = tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", password, None, -300, ticket_lifetime) {
             if ticket_username == username {
-                return Ok(());
+                return Ok(true);
             } else {
                 bail!("ticket login failed - wrong username");
             }
         }
+    } else if password.starts_with("PBSTERM:") {
+        if path.is_none() || privs.is_none() || port.is_none() {
+            bail!("cannot check termnal ticket without path, priv and port");
+        }
+
+        let path = path.unwrap();
+        let privilege_name = privs.unwrap();
+        let port = port.unwrap();
+
+        if let Ok((_age, _data)) =
+            tools::ticket::verify_term_ticket(public_auth_key(), &username, &path, port, password)
+        {
+            for (name, privilege) in PRIVILEGES {
+                if *name == privilege_name {
+                    let mut path_vec = Vec::new();
+                    for part in path.split('/') {
+                        if part != "" {
+                            path_vec.push(part);
+                        }
+                    }
+
+                    user_info.check_privs(username, &path_vec, *privilege, false)?;
+                    return Ok(false);
+                }
+            }
+
+            bail!("No such privilege");
+        }
     }
 
-    crate::auth::authenticate_user(username, password)
+    let _ = crate::auth::authenticate_user(username, password)?;
+    Ok(true)
 }
 
 #[api(
@@ -52,6 +88,21 @@ fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
             password: {
                 schema: PASSWORD_SCHEMA,
             },
+            path: {
+                type: String,
+                description: "Path for verifying terminal tickets.",
+                optional: true,
+            },
+            privs: {
+                type: String,
+                description: "Privilege for verifying terminal tickets.",
+                optional: true,
+            },
+            port: {
+                type: Integer,
+                description: "Port for verifying terminal tickets.",
+                optional: true,
+            },
         },
     },
     returns: {
@@ -78,11 +129,16 @@ fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
 /// Create or verify authentication ticket.
 ///
 /// Returns: An authentication ticket with additional infos.
-fn create_ticket(username: String, password: String) -> Result<Value, Error> {
-    match authenticate_user(&username, &password) {
-        Ok(_) => {
-
-            let ticket = assemble_rsa_ticket( private_auth_key(), "PBS", Some(&username), None)?;
+fn create_ticket(
+    username: String,
+    password: String,
+    path: Option<String>,
+    privs: Option<String>,
+    port: Option<u16>,
+) -> Result<Value, Error> {
+    match authenticate_user(&username, &password, path, privs, port) {
+        Ok(true) => {
+            let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some(&username), None)?;
 
             let token = assemble_csrf_prevention_token(csrf_secret(), &username);
 
@@ -94,6 +150,9 @@ fn create_ticket(username: String, password: String) -> Result<Value, Error> {
                 "CSRFPreventionToken": token,
             }))
         }
+        Ok(false) => Ok(json!({
+            "username": username,
+        })),
         Err(err) => {
             let client_ip = "unknown"; // $rpcenv->get_client_ip() || '';
             log::error!("authentication failure; rhost={} user={} msg={}", client_ip, username, err.to_string());
diff --git a/src/tools/ticket.rs b/src/tools/ticket.rs
index 4727b1e..ff639ce 100644
--- a/src/tools/ticket.rs
+++ b/src/tools/ticket.rs
@@ -11,6 +11,38 @@ use crate::tools::epoch_now_u64;
 
 pub const TICKET_LIFETIME: i64 = 3600*2; // 2 hours
 
+const TERM_PREFIX: &str = "PBSTERM";
+
+pub fn assemble_term_ticket(
+    keypair: &PKey<Private>,
+    username: &str,
+    path: &str,
+    port: u16,
+) -> Result<String, Error> {
+    assemble_rsa_ticket(
+        keypair,
+        TERM_PREFIX,
+        None,
+        Some(&format!("{}{}{}", username, path, port)),
+    )
+}
+
+pub fn verify_term_ticket(
+    keypair: &PKey<Public>,
+    username: &str,
+    path: &str,
+    port: u16,
+    ticket: &str,
+) -> Result<(i64, Option<String>), Error> {
+    verify_rsa_ticket(
+        keypair,
+        TERM_PREFIX,
+        ticket,
+        Some(&format!("{}{}{}", username, path, port)),
+        -300,
+        TICKET_LIFETIME,
+    )
+}
 
 pub fn assemble_rsa_ticket(
     keypair: &PKey<Private>,
-- 
2.20.1





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [pbs-devel] [PATCH proxmox-backup 3/5] api2/nodes: add termproxy and vncwebsocket api calls
  2020-07-21  9:10 [pbs-devel] [PATCH proxmox-backup 0/5] implement web console Dominik Csapak
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 1/5] server/config: add mechanism to update template Dominik Csapak
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 2/5] api2/access: implement term ticket Dominik Csapak
@ 2020-07-21  9:10 ` Dominik Csapak
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 4/5] server/rest: add console to index Dominik Csapak
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2020-07-21  9:10 UTC (permalink / raw)
  To: pbs-devel

Even though it has nothing to do with vnc, we keep the name of the api
call for compatibility with our xtermjs client.

termproxy:
verifies that the user is allowed to open a console and starts
termproxy with the correct parameters

starts a TcpListener on "localhost:0" so that the kernel decides the
port (instead of trying to rerserving like in pve). Then it
leaves the fd open for termproxy and gives the number as port
and tells it via '--port-as-fd' that it should interpret this
as an open fd

the vncwebsocket api call checks the 'vncticket' (name for compatibility)
and connects the remote side (after an Upgrade) with a local TcpStream
connecting to the port given via WebSocket from the proxmox crate

to make sure that only the client can connect that called termproxy and
no one can connect to an arbitrary port on the host we have to include
the port in the ticket data

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 Cargo.toml        |   6 +-
 src/api2/node.rs  | 276 +++++++++++++++++++++++++++++++++++++++++++++-
 src/config/acl.rs |   2 +
 3 files changed, 275 insertions(+), 9 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 355217e..a077a6c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,9 +38,9 @@ pam-sys = "0.5"
 percent-encoding = "2.1"
 pin-utils = "0.1.0"
 pathpatterns = "0.1.2"
-proxmox = { version = "0.2.0", features = [ "sortable-macro", "api-macro" ] }
+proxmox = { version = "0.2.0", features = [ "sortable-macro", "api-macro", "websocket" ] }
 #proxmox = { git = "ssh://gitolite3@proxdev.maurer-it.com/rust/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
-#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro" ] }
+#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro", "websocket" ] }
 proxmox-fuse = "0.1.0"
 pxar = { version = "0.2.1", features = [ "tokio-io", "futures-io" ] }
 #pxar = { path = "../pxar", features = [ "tokio-io", "futures-io" ] }
@@ -50,7 +50,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 siphasher = "0.3"
 syslog = "4.0"
-tokio = { version = "0.2.9", features = [ "blocking", "fs", "io-util", "macros", "rt-threaded", "signal", "stream", "tcp", "time", "uds" ] }
+tokio = { version = "0.2.9", features = [ "blocking", "fs", "dns", "io-util", "macros", "process", "rt-threaded", "signal", "stream", "tcp", "time", "uds" ] }
 tokio-openssl = "0.4.0"
 tokio-util = { version = "0.3", features = [ "codec" ] }
 tower-service = "0.3.0"
diff --git a/src/api2/node.rs b/src/api2/node.rs
index 13ff282..a688694 100644
--- a/src/api2/node.rs
+++ b/src/api2/node.rs
@@ -1,16 +1,275 @@
+use std::net::TcpListener;
+use std::os::unix::io::AsRawFd;
+
+use anyhow::{bail, format_err, Error};
+use futures::{
+    future::{FutureExt, TryFutureExt},
+    try_join,
+};
+use hyper::body::Body;
+use hyper::http::request::Parts;
+use hyper::upgrade::Upgraded;
+use nix::fcntl::{fcntl, FcntlArg, FdFlag};
+use serde_json::{json, Value};
+use tokio::io::{AsyncBufReadExt, BufReader};
+
 use proxmox::api::router::{Router, SubdirMap};
+use proxmox::api::{
+    api, schema::*, ApiHandler, ApiMethod, ApiResponseFuture, Permission, RpcEnvironment,
+};
 use proxmox::list_subdirs_api_method;
+use proxmox::tools::websocket::WebSocket;
+use proxmox::{identity, sortable};
 
-pub mod tasks;
-mod time;
-pub mod network;
+use crate::api2::types::*;
+use crate::config::acl::PRIV_SYS_CONSOLE;
+use crate::server::WorkerTask;
+use crate::tools;
+
+pub mod disks;
 pub mod dns;
-mod syslog;
 mod journal;
+pub mod network;
+pub(crate) mod rrd;
 mod services;
 mod status;
-pub(crate) mod rrd;
-pub mod disks;
+mod syslog;
+pub mod tasks;
+mod time;
+
+pub const SHELL_CMD_SCHEMA: Schema = StringSchema::new("The command to run.")
+    .format(&ApiStringFormat::Enum(&[
+        EnumEntry::new("login", "Login"),
+        EnumEntry::new("upgrade", "Upgrade"),
+    ]))
+    .schema();
+
+#[api(
+    protected: true,
+    input: {
+        properties: {
+            node: {
+                schema: NODE_SCHEMA,
+            },
+            cmd: {
+                schema: SHELL_CMD_SCHEMA,
+                optional: true,
+            },
+        },
+    },
+    returns: {
+        type: Object,
+        description: "Object with the user, ticket, port and upid",
+        properties: {
+            user: {
+                description: "",
+                type: String,
+            },
+            ticket: {
+                description: "",
+                type: String,
+            },
+            port: {
+                description: "",
+                type: String,
+            },
+            upid: {
+                description: "",
+                type: String,
+            },
+        }
+    },
+    access: {
+        description: "Restricted to users on realm 'pam'",
+        permission: &Permission::Privilege(&["nodes","{node}"], PRIV_SYS_CONSOLE, false),
+    }
+)]
+/// Call termproxy and return shell ticket
+async fn termproxy(
+    node: String,
+    cmd: Option<String>,
+    _param: Value,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+    let userid = rpcenv
+        .get_user()
+        .ok_or_else(|| format_err!("unknown user"))?;
+    let (username, realm) = crate::auth::parse_userid(&userid)?;
+
+    if realm != "pam" {
+        bail!("only pam users can use the console");
+    }
+
+    let path = format!("/nodes/{}", node);
+
+    // use port 0 and let the kernel decide which port is free
+    let listener = TcpListener::bind("localhost:0")?;
+    let port = listener.local_addr()?.port();
+
+    let ticket = tools::ticket::assemble_term_ticket(
+        crate::auth_helpers::private_auth_key(),
+        &userid,
+        &path,
+        port,
+    )?;
+
+    let mut command = Vec::new();
+    match cmd.as_ref().map(|x| x.as_str()) {
+        Some("login") | None => {
+            command.push("login");
+            if userid == "root@pam" {
+                command.push("-f");
+                command.push("root");
+            }
+        }
+        Some("upgrade") => {
+            bail!("upgrade is not supported yet");
+        }
+        _ => bail!("invalid command"),
+    };
+
+    let upid = WorkerTask::spawn(
+        "termproxy",
+        None,
+        &username,
+        false,
+        move |worker| async move {
+            // move inside the worker so that it survives and does not close the port
+            // remove CLOEXEC from listenere so that we can reuse it in termproxy
+            let fd = listener.as_raw_fd();
+            let mut flags = match fcntl(fd, FcntlArg::F_GETFD) {
+                Ok(bits) => FdFlag::from_bits_truncate(bits),
+                Err(err) => bail!("could not get fd: {}", err),
+            };
+            flags.remove(FdFlag::FD_CLOEXEC);
+            if let Err(err) = fcntl(fd, FcntlArg::F_SETFD(flags)) {
+                bail!("could not set fd: {}", err);
+            }
+
+            let mut arguments: Vec<&str> = Vec::new();
+            let fd_string = fd.to_string();
+            arguments.push(&fd_string);
+            arguments.extend_from_slice(&[
+                "--path",
+                &path,
+                "--perm",
+                "Sys.Console",
+                "--authport",
+                "82",
+                "--port-as-fd",
+                "--",
+            ]);
+            arguments.extend_from_slice(&command);
+
+            let mut cmd = tokio::process::Command::new("/usr/bin/termproxy");
+
+            cmd.args(&arguments);
+            cmd.stdout(std::process::Stdio::piped());
+            cmd.stderr(std::process::Stdio::piped());
+
+            let mut child = cmd.spawn().expect("error executing termproxy");
+
+            let stdout = child.stdout.take().expect("no child stdout handle");
+            let stderr = child.stderr.take().expect("no child stderr handle");
+
+            let worker_stdout = worker.clone();
+            let stdout_fut = async move {
+                let mut reader = BufReader::new(stdout).lines();
+                while let Some(line) = reader.next_line().await? {
+                    worker_stdout.log(line);
+                }
+                Ok(())
+            };
+
+            let worker_stderr = worker.clone();
+            let stderr_fut = async move {
+                let mut reader = BufReader::new(stderr).lines();
+                while let Some(line) = reader.next_line().await? {
+                    worker_stderr.warn(line);
+                }
+                Ok(())
+            };
+
+            let (exit_code, _, _) = try_join!(child, stdout_fut, stderr_fut)?;
+            if !exit_code.success() {
+                match exit_code.code() {
+                    Some(code) => bail!("termproxy exited with {}", code),
+                    None => bail!("termproxy exited by signal"),
+                }
+            }
+
+            Ok(())
+        },
+    )?;
+
+    Ok(json!({
+        "user": username,
+        "ticket": ticket,
+        "port": port,
+        "upid": upid,
+    }))
+}
+
+#[sortable]
+pub const API_METHOD_WEBSOCKET: ApiMethod = ApiMethod::new(
+    &ApiHandler::AsyncHttp(&upgrade_to_websocket),
+    &ObjectSchema::new(
+        "Upgraded to websocket",
+        &sorted!([
+            ("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 /nodes/{node}."),
+    &Permission::Privilege(&["nodes", "{node}"], PRIV_SYS_CONSOLE, false),
+);
+
+fn upgrade_to_websocket(
+    parts: Parts,
+    req_body: Body,
+    param: Value,
+    _info: &ApiMethod,
+    rpcenv: Box<dyn RpcEnvironment>,
+) -> ApiResponseFuture {
+    async move {
+        let username = rpcenv.get_user().unwrap();
+        let node = tools::required_string_param(&param, "node")?.to_owned();
+        let path = format!("/nodes/{}", node);
+        let ticket = tools::required_string_param(&param, "vncticket")?.to_owned();
+        let port: u16 = tools::required_integer_param(&param, "port")? as u16;
+
+        // will be checked again by termproxy
+        tools::ticket::verify_term_ticket(
+            crate::auth_helpers::public_auth_key(),
+            &username,
+            &path,
+            port,
+            &ticket,
+        )?;
+
+        let (ws, response) = WebSocket::new(parts.headers)?;
+
+        tokio::spawn(async move {
+            let conn: Upgraded = match req_body.on_upgrade().map_err(Error::from).await {
+                Ok(upgraded) => upgraded,
+                _ => bail!("error"),
+            };
+
+            let local = tokio::net::TcpStream::connect(format!("localhost:{}", port)).await?;
+            ws.serve_connection(conn, local).await
+        });
+
+        Ok(response)
+    }
+    .boxed()
+}
 
 pub const SUBDIRS: SubdirMap = &[
     ("disks", &disks::ROUTER),
@@ -22,7 +281,12 @@ pub const SUBDIRS: SubdirMap = &[
     ("status", &status::ROUTER),
     ("syslog", &syslog::ROUTER),
     ("tasks", &tasks::ROUTER),
+    ("termproxy", &Router::new().post(&API_METHOD_TERMPROXY)),
     ("time", &time::ROUTER),
+    (
+        "vncwebsocket",
+        &Router::new().upgrade(&API_METHOD_WEBSOCKET),
+    ),
 ];
 
 pub const ROUTER: Router = Router::new()
diff --git a/src/config/acl.rs b/src/config/acl.rs
index e44b016..d5d9c0a 100644
--- a/src/config/acl.rs
+++ b/src/config/acl.rs
@@ -39,6 +39,8 @@ constnamemap! {
         PRIV_REMOTE_MODIFY("Remote.Modify")                 = 1 << 10;
         PRIV_REMOTE_READ("Remote.Read")                     = 1 << 11;
         PRIV_REMOTE_PRUNE("Remote.Prune")                   = 1 << 12;
+
+        PRIV_SYS_CONSOLE("Sys.Console")                     = 1 << 13;
     }
 }
 
-- 
2.20.1





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [pbs-devel] [PATCH proxmox-backup 4/5] server/rest: add console to index
  2020-07-21  9:10 [pbs-devel] [PATCH proxmox-backup 0/5] implement web console Dominik Csapak
                   ` (2 preceding siblings ...)
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 3/5] api2/nodes: add termproxy and vncwebsocket api calls Dominik Csapak
@ 2020-07-21  9:10 ` Dominik Csapak
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 5/5] ui: add Console Button Dominik Csapak
  2020-07-23 11:24 ` [pbs-devel] applied-series: [PATCH proxmox-backup 0/5] implement web console Thomas Lamprecht
  5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2020-07-21  9:10 UTC (permalink / raw)
  To: pbs-devel

register the console template and render it when the 'console' parameter
is given

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/bin/proxmox-backup-proxy.rs | 1 +
 src/server/rest.rs              | 7 +++++--
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs
index 1e93886..a303391 100644
--- a/src/bin/proxmox-backup-proxy.rs
+++ b/src/bin/proxmox-backup-proxy.rs
@@ -56,6 +56,7 @@ async fn run() -> Result<(), Error> {
     let mut indexpath = PathBuf::from(buildcfg::JS_DIR);
     indexpath.push("index.hbs");
     config.register_template("index", &indexpath)?;
+    config.register_template("console", "/usr/share/pve-xtermjs/index.html.hbs")?;
 
     let rest_server = RestServer::new(config);
 
diff --git a/src/server/rest.rs b/src/server/rest.rs
index a7b0a23..09b6848 100644
--- a/src/server/rest.rs
+++ b/src/server/rest.rs
@@ -319,11 +319,14 @@ fn get_index(username: Option<String>, token: Option<String>, api: &Arc<ApiConfi
     let token = token.unwrap_or_else(|| String::from(""));
 
     let mut debug = false;
+    let mut template_file = "index";
 
     if let Some(query_str) = parts.uri.query() {
         for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
             if k == "debug" && v != "0" && v != "false" {
                 debug = true;
+            } else if k == "console" {
+                template_file = "console";
             }
         }
     }
@@ -337,12 +340,12 @@ fn get_index(username: Option<String>, token: Option<String>, api: &Arc<ApiConfi
 
     let mut ct = "text/html";
 
-    let index = match api.render_template("index", &data) {
+    let index = match api.render_template(template_file, &data) {
         Ok(index) => index,
         Err(err) => {
             ct = "text/plain";
             format!("Error rendering template: {}", err)
-        },
+        }
     };
 
     Response::builder()
-- 
2.20.1





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [pbs-devel] [PATCH proxmox-backup 5/5] ui: add Console Button
  2020-07-21  9:10 [pbs-devel] [PATCH proxmox-backup 0/5] implement web console Dominik Csapak
                   ` (3 preceding siblings ...)
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 4/5] server/rest: add console to index Dominik Csapak
@ 2020-07-21  9:10 ` Dominik Csapak
  2020-07-23 11:24 ` [pbs-devel] applied-series: [PATCH proxmox-backup 0/5] implement web console Thomas Lamprecht
  5 siblings, 0 replies; 7+ messages in thread
From: Dominik Csapak @ 2020-07-21  9:10 UTC (permalink / raw)
  To: pbs-devel

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/ServerStatus.js | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/www/ServerStatus.js b/www/ServerStatus.js
index 51e6c4e..3752820 100644
--- a/www/ServerStatus.js
+++ b/www/ServerStatus.js
@@ -86,7 +86,15 @@ Ext.define('PBS.ServerStatus', {
 	    iconCls: 'fa fa-power-off'
 	});
 
-	me.tbar = [ restartBtn, shutdownBtn, '->', { xtype: 'proxmoxRRDTypeSelector' } ];
+	var consoleBtn = Ext.create('Proxmox.button.Button', {
+	    text: gettext('Console'),
+	    iconCls: 'fa fa-terminal',
+	    handler: function() {
+		Proxmox.Utils.openXtermJsViewer('shell', 0, Proxmox.NodeName);
+	    }
+	});
+
+	me.tbar = [ consoleBtn, restartBtn, shutdownBtn, '->', { xtype: 'proxmoxRRDTypeSelector' } ];
 
 	var rrdstore = Ext.create('Proxmox.data.RRDStore', {
 	    rrdurl: "/api2/json/nodes/localhost/rrd",
-- 
2.20.1





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [pbs-devel] applied-series: [PATCH proxmox-backup 0/5] implement web console
  2020-07-21  9:10 [pbs-devel] [PATCH proxmox-backup 0/5] implement web console Dominik Csapak
                   ` (4 preceding siblings ...)
  2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 5/5] ui: add Console Button Dominik Csapak
@ 2020-07-23 11:24 ` Thomas Lamprecht
  5 siblings, 0 replies; 7+ messages in thread
From: Thomas Lamprecht @ 2020-07-23 11:24 UTC (permalink / raw)
  To: Proxmox Backup Server development discussion, Dominik Csapak

On 21.07.20 11:10, Dominik Csapak wrote:
> this series implements the necessary changes to access a shell via xtermjs
> 
> this replaces my previous series 'prerequisites for console' [0]
> 
> it needs an installed pve-xtermjs package with my latest patches [1]
> as well as an bumped proxmox crate (updated version in Cargo.toml as well)
> 
> 0: https://lists.proxmox.com/pipermail/pbs-devel/2020-July/000080.html
> 1: https://lists.proxmox.com/pipermail/pbs-devel/2020-July/000108.html
> 
> Dominik Csapak (5):
>   server/config: add mechanism to update template
>   api2/access: implement term ticket
>   api2/nodes: add termproxy and vncwebsocket api calls
>   server/rest: add console to index
>   ui: add Console Button
> 
>  Cargo.toml                      |   6 +-
>  src/api2/access.rs              |  79 +++++++--
>  src/api2/node.rs                | 276 +++++++++++++++++++++++++++++++-
>  src/bin/proxmox-backup-proxy.rs |   7 +-
>  src/config/acl.rs               |   2 +
>  src/server/config.rs            |  67 +++++++-
>  src/server/rest.rs              |  18 ++-
>  src/tools/ticket.rs             |  32 ++++
>  www/ServerStatus.js             |  10 +-
>  9 files changed, 460 insertions(+), 37 deletions(-)
> 

applied series, with the ACL fix for "/nodes/{node}" -> "/system" followup we
talked, much thanks!




^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2020-07-23 11:24 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-07-21  9:10 [pbs-devel] [PATCH proxmox-backup 0/5] implement web console Dominik Csapak
2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 1/5] server/config: add mechanism to update template Dominik Csapak
2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 2/5] api2/access: implement term ticket Dominik Csapak
2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 3/5] api2/nodes: add termproxy and vncwebsocket api calls Dominik Csapak
2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 4/5] server/rest: add console to index Dominik Csapak
2020-07-21  9:10 ` [pbs-devel] [PATCH proxmox-backup 5/5] ui: add Console Button Dominik Csapak
2020-07-23 11:24 ` [pbs-devel] applied-series: [PATCH proxmox-backup 0/5] implement web console Thomas Lamprecht

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal