* [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(¶m, "node")?.to_owned();
+ let path = format!("/nodes/{}", node);
+ let ticket = tools::required_string_param(¶m, "vncticket")?.to_owned();
+ let port: u16 = tools::required_integer_param(¶m, "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