]>
Commit | Line | Data |
---|---|---|
1 | //! Server/Node Configuration and Administration | |
2 | ||
3 | use std::net::TcpListener; | |
4 | use std::os::unix::io::AsRawFd; | |
5 | ||
6 | use anyhow::{bail, format_err, Error}; | |
7 | use futures::future::{FutureExt, TryFutureExt}; | |
8 | use hyper::body::Body; | |
9 | use hyper::http::request::Parts; | |
10 | use hyper::upgrade::Upgraded; | |
11 | use hyper::Request; | |
12 | use serde_json::{json, Value}; | |
13 | use tokio::io::{AsyncBufReadExt, BufReader}; | |
14 | ||
15 | use proxmox::api::router::{Router, SubdirMap}; | |
16 | use proxmox::api::{ | |
17 | api, schema::*, ApiHandler, ApiMethod, ApiResponseFuture, Permission, RpcEnvironment, | |
18 | }; | |
19 | use proxmox::list_subdirs_api_method; | |
20 | use proxmox::tools::websocket::WebSocket; | |
21 | use proxmox::{identity, sortable}; | |
22 | ||
23 | use crate::api2::types::*; | |
24 | use crate::config::acl::PRIV_SYS_CONSOLE; | |
25 | use crate::server::WorkerTask; | |
26 | use crate::tools; | |
27 | use crate::tools::ticket::{self, Empty, Ticket}; | |
28 | ||
29 | pub mod apt; | |
30 | pub mod disks; | |
31 | pub mod dns; | |
32 | pub mod network; | |
33 | pub mod tasks; | |
34 | pub mod subscription; | |
35 | ||
36 | pub(crate) mod rrd; | |
37 | ||
38 | mod journal; | |
39 | pub(crate) mod services; | |
40 | mod status; | |
41 | mod syslog; | |
42 | mod time; | |
43 | mod report; | |
44 | ||
45 | pub const SHELL_CMD_SCHEMA: Schema = StringSchema::new("The command to run.") | |
46 | .format(&ApiStringFormat::Enum(&[ | |
47 | EnumEntry::new("login", "Login"), | |
48 | EnumEntry::new("upgrade", "Upgrade"), | |
49 | ])) | |
50 | .schema(); | |
51 | ||
52 | #[api( | |
53 | protected: true, | |
54 | input: { | |
55 | properties: { | |
56 | node: { | |
57 | schema: NODE_SCHEMA, | |
58 | }, | |
59 | cmd: { | |
60 | schema: SHELL_CMD_SCHEMA, | |
61 | optional: true, | |
62 | }, | |
63 | }, | |
64 | }, | |
65 | returns: { | |
66 | type: Object, | |
67 | description: "Object with the user, ticket, port and upid", | |
68 | properties: { | |
69 | user: { | |
70 | description: "", | |
71 | type: String, | |
72 | }, | |
73 | ticket: { | |
74 | description: "", | |
75 | type: String, | |
76 | }, | |
77 | port: { | |
78 | description: "", | |
79 | type: String, | |
80 | }, | |
81 | upid: { | |
82 | description: "", | |
83 | type: String, | |
84 | }, | |
85 | } | |
86 | }, | |
87 | access: { | |
88 | description: "Restricted to users on realm 'pam'", | |
89 | permission: &Permission::Privilege(&["system"], PRIV_SYS_CONSOLE, false), | |
90 | } | |
91 | )] | |
92 | /// Call termproxy and return shell ticket | |
93 | async fn termproxy( | |
94 | cmd: Option<String>, | |
95 | rpcenv: &mut dyn RpcEnvironment, | |
96 | ) -> Result<Value, Error> { | |
97 | // intentionally user only for now | |
98 | let auth_id: Authid = rpcenv | |
99 | .get_auth_id() | |
100 | .ok_or_else(|| format_err!("no authid available"))? | |
101 | .parse()?; | |
102 | ||
103 | if auth_id.is_token() { | |
104 | bail!("API tokens cannot access this API endpoint"); | |
105 | } | |
106 | ||
107 | let userid = auth_id.user(); | |
108 | ||
109 | if userid.realm() != "pam" { | |
110 | bail!("only pam users can use the console"); | |
111 | } | |
112 | ||
113 | let path = "/system"; | |
114 | ||
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(); | |
118 | ||
119 | let ticket = Ticket::new(ticket::TERM_PREFIX, &Empty)? | |
120 | .sign( | |
121 | crate::auth_helpers::private_auth_key(), | |
122 | Some(&ticket::term_aad(&userid, &path, port)), | |
123 | )?; | |
124 | ||
125 | let mut command = Vec::new(); | |
126 | match cmd.as_deref() { | |
127 | Some("login") | None => { | |
128 | command.push("login"); | |
129 | if userid == "root@pam" { | |
130 | command.push("-f"); | |
131 | command.push("root"); | |
132 | } | |
133 | } | |
134 | Some("upgrade") => { | |
135 | if userid != "root@pam" { | |
136 | bail!("only root@pam can upgrade"); | |
137 | } | |
138 | // TODO: add nicer/safer wrapper like in PVE instead | |
139 | command.push("sh"); | |
140 | command.push("-c"); | |
141 | command.push("apt full-upgrade; bash -l"); | |
142 | } | |
143 | _ => bail!("invalid command"), | |
144 | }; | |
145 | ||
146 | let username = userid.name().to_owned(); | |
147 | let upid = WorkerTask::spawn( | |
148 | "termproxy", | |
149 | None, | |
150 | auth_id, | |
151 | false, | |
152 | move |worker| async move { | |
153 | // move inside the worker so that it survives and does not close the port | |
154 | // remove CLOEXEC from listenere so that we can reuse it in termproxy | |
155 | tools::fd_change_cloexec(listener.as_raw_fd(), false)?; | |
156 | ||
157 | let mut arguments: Vec<&str> = Vec::new(); | |
158 | let fd_string = listener.as_raw_fd().to_string(); | |
159 | arguments.push(&fd_string); | |
160 | arguments.extend_from_slice(&[ | |
161 | "--path", | |
162 | &path, | |
163 | "--perm", | |
164 | "Sys.Console", | |
165 | "--authport", | |
166 | "82", | |
167 | "--port-as-fd", | |
168 | "--", | |
169 | ]); | |
170 | arguments.extend_from_slice(&command); | |
171 | ||
172 | let mut cmd = tokio::process::Command::new("/usr/bin/termproxy"); | |
173 | ||
174 | cmd.args(&arguments) | |
175 | .stdout(std::process::Stdio::piped()) | |
176 | .stderr(std::process::Stdio::piped()); | |
177 | ||
178 | let mut child = cmd.spawn().expect("error executing termproxy"); | |
179 | ||
180 | let stdout = child.stdout.take().expect("no child stdout handle"); | |
181 | let stderr = child.stderr.take().expect("no child stderr handle"); | |
182 | ||
183 | let worker_stdout = worker.clone(); | |
184 | let stdout_fut = async move { | |
185 | let mut reader = BufReader::new(stdout).lines(); | |
186 | while let Some(line) = reader.next_line().await? { | |
187 | worker_stdout.log(line); | |
188 | } | |
189 | Ok::<(), Error>(()) | |
190 | }; | |
191 | ||
192 | let worker_stderr = worker.clone(); | |
193 | let stderr_fut = async move { | |
194 | let mut reader = BufReader::new(stderr).lines(); | |
195 | while let Some(line) = reader.next_line().await? { | |
196 | worker_stderr.warn(line); | |
197 | } | |
198 | Ok::<(), Error>(()) | |
199 | }; | |
200 | ||
201 | let mut needs_kill = false; | |
202 | let res = tokio::select!{ | |
203 | res = child.wait() => { | |
204 | let exit_code = res?; | |
205 | if !exit_code.success() { | |
206 | match exit_code.code() { | |
207 | Some(code) => bail!("termproxy exited with {}", code), | |
208 | None => bail!("termproxy exited by signal"), | |
209 | } | |
210 | } | |
211 | Ok(()) | |
212 | }, | |
213 | res = stdout_fut => res, | |
214 | res = stderr_fut => res, | |
215 | res = worker.abort_future() => { | |
216 | needs_kill = true; | |
217 | res.map_err(Error::from) | |
218 | } | |
219 | }; | |
220 | ||
221 | if needs_kill { | |
222 | if res.is_ok() { | |
223 | child.kill().await?; | |
224 | return Ok(()); | |
225 | } | |
226 | ||
227 | if let Err(err) = child.kill().await { | |
228 | worker.warn(format!("error killing termproxy: {}", err)); | |
229 | } else if let Err(err) = child.wait().await { | |
230 | worker.warn(format!("error awaiting termproxy: {}", err)); | |
231 | } | |
232 | } | |
233 | ||
234 | res | |
235 | }, | |
236 | )?; | |
237 | ||
238 | // FIXME: We're returning the user NAME only? | |
239 | Ok(json!({ | |
240 | "user": username, | |
241 | "ticket": ticket, | |
242 | "port": port, | |
243 | "upid": upid, | |
244 | })) | |
245 | } | |
246 | ||
247 | #[sortable] | |
248 | pub const API_METHOD_WEBSOCKET: ApiMethod = ApiMethod::new( | |
249 | &ApiHandler::AsyncHttp(&upgrade_to_websocket), | |
250 | &ObjectSchema::new( | |
251 | "Upgraded to websocket", | |
252 | &sorted!([ | |
253 | ("node", false, &NODE_SCHEMA), | |
254 | ( | |
255 | "vncticket", | |
256 | false, | |
257 | &StringSchema::new("Terminal ticket").schema() | |
258 | ), | |
259 | ("port", false, &IntegerSchema::new("Terminal port").schema()), | |
260 | ]), | |
261 | ), | |
262 | ) | |
263 | .access( | |
264 | Some("The user needs Sys.Console on /system."), | |
265 | &Permission::Privilege(&["system"], PRIV_SYS_CONSOLE, false), | |
266 | ); | |
267 | ||
268 | fn upgrade_to_websocket( | |
269 | parts: Parts, | |
270 | req_body: Body, | |
271 | param: Value, | |
272 | _info: &ApiMethod, | |
273 | rpcenv: Box<dyn RpcEnvironment>, | |
274 | ) -> ApiResponseFuture { | |
275 | async move { | |
276 | // intentionally user only for now | |
277 | let auth_id: Authid = rpcenv | |
278 | .get_auth_id() | |
279 | .ok_or_else(|| format_err!("no authid available"))? | |
280 | .parse()?; | |
281 | ||
282 | if auth_id.is_token() { | |
283 | bail!("API tokens cannot access this API endpoint"); | |
284 | } | |
285 | ||
286 | let userid = auth_id.user(); | |
287 | let ticket = tools::required_string_param(¶m, "vncticket")?; | |
288 | let port: u16 = tools::required_integer_param(¶m, "port")? as u16; | |
289 | ||
290 | // will be checked again by termproxy | |
291 | Ticket::<Empty>::parse(ticket)? | |
292 | .verify( | |
293 | crate::auth_helpers::public_auth_key(), | |
294 | ticket::TERM_PREFIX, | |
295 | Some(&ticket::term_aad(&userid, "/system", port)), | |
296 | )?; | |
297 | ||
298 | let (ws, response) = WebSocket::new(parts.headers.clone())?; | |
299 | ||
300 | crate::server::spawn_internal_task(async move { | |
301 | let conn: Upgraded = match hyper::upgrade::on(Request::from_parts(parts, req_body)).map_err(Error::from).await { | |
302 | Ok(upgraded) => upgraded, | |
303 | _ => bail!("error"), | |
304 | }; | |
305 | ||
306 | let local = tokio::net::TcpStream::connect(format!("localhost:{}", port)).await?; | |
307 | ws.serve_connection(conn, local).await | |
308 | }); | |
309 | ||
310 | Ok(response) | |
311 | } | |
312 | .boxed() | |
313 | } | |
314 | ||
315 | pub const SUBDIRS: SubdirMap = &[ | |
316 | ("apt", &apt::ROUTER), | |
317 | ("disks", &disks::ROUTER), | |
318 | ("dns", &dns::ROUTER), | |
319 | ("journal", &journal::ROUTER), | |
320 | ("network", &network::ROUTER), | |
321 | ("report", &report::ROUTER), | |
322 | ("rrd", &rrd::ROUTER), | |
323 | ("services", &services::ROUTER), | |
324 | ("status", &status::ROUTER), | |
325 | ("subscription", &subscription::ROUTER), | |
326 | ("syslog", &syslog::ROUTER), | |
327 | ("tasks", &tasks::ROUTER), | |
328 | ("termproxy", &Router::new().post(&API_METHOD_TERMPROXY)), | |
329 | ("time", &time::ROUTER), | |
330 | ( | |
331 | "vncwebsocket", | |
332 | &Router::new().upgrade(&API_METHOD_WEBSOCKET), | |
333 | ), | |
334 | ]; | |
335 | ||
336 | pub const ROUTER: Router = Router::new() | |
337 | .get(&list_subdirs_api_method!(SUBDIRS)) | |
338 | .subdirs(SUBDIRS); |