]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/node.rs
api: apt: use schema default const for quiet param
[proxmox-backup.git] / src / api2 / node.rs
CommitLineData
1c2f842a
DC
1use std::net::TcpListener;
2use std::os::unix::io::AsRawFd;
3
4use anyhow::{bail, format_err, Error};
5use futures::{
6 future::{FutureExt, TryFutureExt},
7 try_join,
8};
9use hyper::body::Body;
10use hyper::http::request::Parts;
11use hyper::upgrade::Upgraded;
12use nix::fcntl::{fcntl, FcntlArg, FdFlag};
13use serde_json::{json, Value};
14use tokio::io::{AsyncBufReadExt, BufReader};
15
3d482025 16use proxmox::api::router::{Router, SubdirMap};
1c2f842a
DC
17use proxmox::api::{
18 api, schema::*, ApiHandler, ApiMethod, ApiResponseFuture, Permission, RpcEnvironment,
19};
9ea4bce4 20use proxmox::list_subdirs_api_method;
1c2f842a
DC
21use proxmox::tools::websocket::WebSocket;
22use proxmox::{identity, sortable};
b2b3485d 23
1c2f842a
DC
24use crate::api2::types::*;
25use crate::config::acl::PRIV_SYS_CONSOLE;
26use crate::server::WorkerTask;
27use crate::tools;
28
29pub mod disks;
550e0d88 30pub mod dns;
81cc71c0 31mod journal;
1c2f842a
DC
32pub mod network;
33pub(crate) mod rrd;
d2ab5f19 34mod services;
2337df7b 35mod status;
113c9b59 36mod subscription;
a4e86972 37mod apt;
1c2f842a
DC
38mod syslog;
39pub mod tasks;
40mod time;
41
42pub const SHELL_CMD_SCHEMA: Schema = StringSchema::new("The command to run.")
43 .format(&ApiStringFormat::Enum(&[
44 EnumEntry::new("login", "Login"),
45 EnumEntry::new("upgrade", "Upgrade"),
46 ]))
47 .schema();
48
49#[api(
50 protected: true,
51 input: {
52 properties: {
53 node: {
54 schema: NODE_SCHEMA,
55 },
56 cmd: {
57 schema: SHELL_CMD_SCHEMA,
58 optional: true,
59 },
60 },
61 },
62 returns: {
63 type: Object,
64 description: "Object with the user, ticket, port and upid",
65 properties: {
66 user: {
67 description: "",
68 type: String,
69 },
70 ticket: {
71 description: "",
72 type: String,
73 },
74 port: {
75 description: "",
76 type: String,
77 },
78 upid: {
79 description: "",
80 type: String,
81 },
82 }
83 },
84 access: {
85 description: "Restricted to users on realm 'pam'",
86 permission: &Permission::Privilege(&["nodes","{node}"], PRIV_SYS_CONSOLE, false),
87 }
88)]
89/// Call termproxy and return shell ticket
90async fn termproxy(
91 node: String,
92 cmd: Option<String>,
93 _param: Value,
94 rpcenv: &mut dyn RpcEnvironment,
95) -> Result<Value, Error> {
96 let userid = rpcenv
97 .get_user()
98 .ok_or_else(|| format_err!("unknown user"))?;
99 let (username, realm) = crate::auth::parse_userid(&userid)?;
100
101 if realm != "pam" {
102 bail!("only pam users can use the console");
103 }
104
105 let path = format!("/nodes/{}", node);
106
107 // use port 0 and let the kernel decide which port is free
108 let listener = TcpListener::bind("localhost:0")?;
109 let port = listener.local_addr()?.port();
110
111 let ticket = tools::ticket::assemble_term_ticket(
112 crate::auth_helpers::private_auth_key(),
113 &userid,
114 &path,
115 port,
116 )?;
117
118 let mut command = Vec::new();
119 match cmd.as_ref().map(|x| x.as_str()) {
120 Some("login") | None => {
121 command.push("login");
122 if userid == "root@pam" {
123 command.push("-f");
124 command.push("root");
125 }
126 }
127 Some("upgrade") => {
128 bail!("upgrade is not supported yet");
129 }
130 _ => bail!("invalid command"),
131 };
132
133 let upid = WorkerTask::spawn(
134 "termproxy",
135 None,
136 &username,
137 false,
138 move |worker| async move {
139 // move inside the worker so that it survives and does not close the port
140 // remove CLOEXEC from listenere so that we can reuse it in termproxy
141 let fd = listener.as_raw_fd();
142 let mut flags = match fcntl(fd, FcntlArg::F_GETFD) {
143 Ok(bits) => FdFlag::from_bits_truncate(bits),
144 Err(err) => bail!("could not get fd: {}", err),
145 };
146 flags.remove(FdFlag::FD_CLOEXEC);
147 if let Err(err) = fcntl(fd, FcntlArg::F_SETFD(flags)) {
148 bail!("could not set fd: {}", err);
149 }
150
151 let mut arguments: Vec<&str> = Vec::new();
152 let fd_string = fd.to_string();
153 arguments.push(&fd_string);
154 arguments.extend_from_slice(&[
155 "--path",
156 &path,
157 "--perm",
158 "Sys.Console",
159 "--authport",
160 "82",
161 "--port-as-fd",
162 "--",
163 ]);
164 arguments.extend_from_slice(&command);
165
166 let mut cmd = tokio::process::Command::new("/usr/bin/termproxy");
167
168 cmd.args(&arguments);
169 cmd.stdout(std::process::Stdio::piped());
170 cmd.stderr(std::process::Stdio::piped());
171
172 let mut child = cmd.spawn().expect("error executing termproxy");
173
174 let stdout = child.stdout.take().expect("no child stdout handle");
175 let stderr = child.stderr.take().expect("no child stderr handle");
176
177 let worker_stdout = worker.clone();
178 let stdout_fut = async move {
179 let mut reader = BufReader::new(stdout).lines();
180 while let Some(line) = reader.next_line().await? {
181 worker_stdout.log(line);
182 }
183 Ok(())
184 };
185
186 let worker_stderr = worker.clone();
187 let stderr_fut = async move {
188 let mut reader = BufReader::new(stderr).lines();
189 while let Some(line) = reader.next_line().await? {
190 worker_stderr.warn(line);
191 }
192 Ok(())
193 };
194
195 let (exit_code, _, _) = try_join!(child, stdout_fut, stderr_fut)?;
196 if !exit_code.success() {
197 match exit_code.code() {
198 Some(code) => bail!("termproxy exited with {}", code),
199 None => bail!("termproxy exited by signal"),
200 }
201 }
202
203 Ok(())
204 },
205 )?;
206
207 Ok(json!({
208 "user": username,
209 "ticket": ticket,
210 "port": port,
211 "upid": upid,
212 }))
213}
214
215#[sortable]
216pub const API_METHOD_WEBSOCKET: ApiMethod = ApiMethod::new(
217 &ApiHandler::AsyncHttp(&upgrade_to_websocket),
218 &ObjectSchema::new(
219 "Upgraded to websocket",
220 &sorted!([
221 ("node", false, &NODE_SCHEMA),
222 (
223 "vncticket",
224 false,
225 &StringSchema::new("Terminal ticket").schema()
226 ),
227 ("port", false, &IntegerSchema::new("Terminal port").schema()),
228 ]),
229 ),
230)
231.access(
232 Some("The user needs Sys.Console on /nodes/{node}."),
233 &Permission::Privilege(&["nodes", "{node}"], PRIV_SYS_CONSOLE, false),
234);
235
236fn upgrade_to_websocket(
237 parts: Parts,
238 req_body: Body,
239 param: Value,
240 _info: &ApiMethod,
241 rpcenv: Box<dyn RpcEnvironment>,
242) -> ApiResponseFuture {
243 async move {
244 let username = rpcenv.get_user().unwrap();
245 let node = tools::required_string_param(&param, "node")?.to_owned();
246 let path = format!("/nodes/{}", node);
247 let ticket = tools::required_string_param(&param, "vncticket")?.to_owned();
248 let port: u16 = tools::required_integer_param(&param, "port")? as u16;
249
250 // will be checked again by termproxy
251 tools::ticket::verify_term_ticket(
252 crate::auth_helpers::public_auth_key(),
253 &username,
254 &path,
255 port,
256 &ticket,
257 )?;
258
259 let (ws, response) = WebSocket::new(parts.headers)?;
260
261 tokio::spawn(async move {
262 let conn: Upgraded = match req_body.on_upgrade().map_err(Error::from).await {
263 Ok(upgraded) => upgraded,
264 _ => bail!("error"),
265 };
266
267 let local = tokio::net::TcpStream::connect(format!("localhost:{}", port)).await?;
268 ws.serve_connection(conn, local).await
269 });
270
271 Ok(response)
272 }
273 .boxed()
274}
b2b3485d 275
255f378a 276pub const SUBDIRS: SubdirMap = &[
a4e86972 277 ("apt", &apt::ROUTER),
ce8e3de4 278 ("disks", &disks::ROUTER),
255f378a 279 ("dns", &dns::ROUTER),
81cc71c0 280 ("journal", &journal::ROUTER),
255f378a 281 ("network", &network::ROUTER),
a2f862ee 282 ("rrd", &rrd::ROUTER),
255f378a 283 ("services", &services::ROUTER),
2337df7b 284 ("status", &status::ROUTER),
113c9b59 285 ("subscription", &subscription::ROUTER),
255f378a
DM
286 ("syslog", &syslog::ROUTER),
287 ("tasks", &tasks::ROUTER),
1c2f842a 288 ("termproxy", &Router::new().post(&API_METHOD_TERMPROXY)),
255f378a 289 ("time", &time::ROUTER),
1c2f842a
DC
290 (
291 "vncwebsocket",
292 &Router::new().upgrade(&API_METHOD_WEBSOCKET),
293 ),
255f378a
DM
294];
295
296pub const ROUTER: Router = Router::new()
297 .get(&list_subdirs_api_method!(SUBDIRS))
298 .subdirs(SUBDIRS);