]>
Commit | Line | Data |
---|---|---|
1c2f842a DC |
1 | use std::net::TcpListener; |
2 | use std::os::unix::io::AsRawFd; | |
3 | ||
4 | use anyhow::{bail, format_err, Error}; | |
5 | use futures::{ | |
6 | future::{FutureExt, TryFutureExt}, | |
7 | try_join, | |
8 | }; | |
9 | use hyper::body::Body; | |
10 | use hyper::http::request::Parts; | |
11 | use hyper::upgrade::Upgraded; | |
12 | use nix::fcntl::{fcntl, FcntlArg, FdFlag}; | |
13 | use serde_json::{json, Value}; | |
14 | use tokio::io::{AsyncBufReadExt, BufReader}; | |
15 | ||
3d482025 | 16 | use proxmox::api::router::{Router, SubdirMap}; |
1c2f842a DC |
17 | use proxmox::api::{ |
18 | api, schema::*, ApiHandler, ApiMethod, ApiResponseFuture, Permission, RpcEnvironment, | |
19 | }; | |
9ea4bce4 | 20 | use proxmox::list_subdirs_api_method; |
1c2f842a DC |
21 | use proxmox::tools::websocket::WebSocket; |
22 | use proxmox::{identity, sortable}; | |
b2b3485d | 23 | |
1c2f842a DC |
24 | use crate::api2::types::*; |
25 | use crate::config::acl::PRIV_SYS_CONSOLE; | |
26 | use crate::server::WorkerTask; | |
27 | use crate::tools; | |
28 | ||
29 | pub mod disks; | |
550e0d88 | 30 | pub mod dns; |
81cc71c0 | 31 | mod journal; |
1c2f842a DC |
32 | pub mod network; |
33 | pub(crate) mod rrd; | |
d2ab5f19 | 34 | mod services; |
2337df7b | 35 | mod status; |
113c9b59 | 36 | mod subscription; |
a4e86972 | 37 | mod apt; |
1c2f842a DC |
38 | mod syslog; |
39 | pub mod tasks; | |
40 | mod time; | |
41 | ||
42 | pub 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 | |
90 | async 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] | |
216 | pub 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 | ||
236 | fn 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(¶m, "node")?.to_owned(); | |
246 | let path = format!("/nodes/{}", node); | |
247 | let ticket = tools::required_string_param(¶m, "vncticket")?.to_owned(); | |
248 | let port: u16 = tools::required_integer_param(¶m, "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 | 276 | pub 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 | ||
296 | pub const ROUTER: Router = Router::new() | |
297 | .get(&list_subdirs_api_method!(SUBDIRS)) | |
298 | .subdirs(SUBDIRS); |