]>
Commit | Line | Data |
---|---|---|
1 | package PVE::APIClient::Commands::lxc; | |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use Errno qw(EINTR EAGAIN); | |
6 | use JSON; | |
7 | use URI::Escape; | |
8 | use IO::Select; | |
9 | use IO::Socket::SSL; | |
10 | use MIME::Base64; | |
11 | use Digest::SHA; | |
12 | use HTTP::Response; | |
13 | ||
14 | use PVE::Tools; | |
15 | use PVE::JSONSchema qw(get_standard_option); | |
16 | use PVE::CLIHandler; | |
17 | use PVE::PTY; | |
18 | ||
19 | use base qw(PVE::CLIHandler); | |
20 | use PVE::APIClient::Config; | |
21 | ||
22 | my $CRLF = "\x0D\x0A"; | |
23 | my $max_payload_size = 128*1024; | |
24 | ||
25 | my $build_web_socket_request = sub { | |
26 | my ($host, $path, $ticket, $termproxy) = @_; | |
27 | ||
28 | my $key = ''; | |
29 | $key .= chr(int(rand(256))) for 1 .. 16; | |
30 | my $enckey = MIME::Base64::encode_base64($key, ''); | |
31 | ||
32 | my $encticket = uri_escape($ticket); | |
33 | my $cookie = "PVEAuthCookie=$encticket; path=/; secure;"; | |
34 | ||
35 | $path .= "?port=$termproxy->{port}" . | |
36 | "&vncticket=" . uri_escape($termproxy->{ticket}); | |
37 | ||
38 | my $request = "GET $path HTTP/1.1$CRLF" | |
39 | . "Upgrade: WebSocket$CRLF" | |
40 | . "Connection: Upgrade$CRLF" | |
41 | . "Host: $host$CRLF" | |
42 | . "Sec-WebSocket-Key: $enckey$CRLF" | |
43 | . "Sec-WebSocket-Version: 13$CRLF" | |
44 | . "Sec-WebSocket-Protocol: binary$CRLF" | |
45 | . "Cookie: $cookie$CRLF" | |
46 | . "$CRLF"; | |
47 | ||
48 | return ($request, $enckey); | |
49 | }; | |
50 | ||
51 | my $create_websockt_frame = sub { | |
52 | my ($payload) = @_; | |
53 | ||
54 | my $string = "\x82"; # binary frame | |
55 | my $payload_len = length($payload); | |
56 | if ($payload_len <= 125) { | |
57 | $string .= pack 'C', $payload_len | 128; | |
58 | } elsif ($payload_len <= 0xffff) { | |
59 | $string .= pack 'C', 126 | 128; | |
60 | $string .= pack 'n', $payload_len; | |
61 | } else { | |
62 | $string .= pack 'C', 127 | 128; | |
63 | $string .= pack 'Q>', $payload_len; | |
64 | } | |
65 | ||
66 | $string .= pack 'N', 0; # we simply use 0 as mask | |
67 | $string .= $payload; | |
68 | ||
69 | return $string; | |
70 | }; | |
71 | ||
72 | my $parse_web_socket_frame = sub { | |
73 | my ($wsbuf_ref) = @_; | |
74 | ||
75 | my $payload; | |
76 | my $req_close = 0; | |
77 | ||
78 | while (my $len = length($$wsbuf_ref)) { | |
79 | last if $len < 2; | |
80 | ||
81 | my $hdr = unpack('C', substr($$wsbuf_ref, 0, 1)); | |
82 | my $opcode = $hdr & 0b00001111; | |
83 | my $fin = $hdr & 0b10000000; | |
84 | ||
85 | die "received fragmented websocket frame\n" if !$fin; | |
86 | ||
87 | my $rsv = $hdr & 0b01110000; | |
88 | die "received websocket frame with RSV flags\n" if $rsv; | |
89 | ||
90 | my $payload_len = unpack 'C', substr($$wsbuf_ref, 1, 1); | |
91 | ||
92 | my $masked = $payload_len & 0b10000000; | |
93 | die "received masked websocket frame from server\n" if $masked; | |
94 | ||
95 | my $offset = 2; | |
96 | $payload_len = $payload_len & 0b01111111; | |
97 | if ($payload_len == 126) { | |
98 | last if $len < 4; | |
99 | $payload_len = unpack('n', substr($$wsbuf_ref, $offset, 2)); | |
100 | $offset += 2; | |
101 | } elsif ($payload_len == 127) { | |
102 | last if $len < 10; | |
103 | $payload_len = unpack('Q>', substr($$wsbuf_ref, $offset, 8)); | |
104 | $offset += 8; | |
105 | } | |
106 | ||
107 | die "received too large websocket frame (len = $payload_len)\n" | |
108 | if ($payload_len > $max_payload_size) || ($payload_len < 0); | |
109 | ||
110 | last if $len < ($offset + $payload_len); | |
111 | ||
112 | my $data = substr($$wsbuf_ref, 0, $offset + $payload_len, ''); # now consume data | |
113 | ||
114 | my $frame_data = substr($data, $offset, $payload_len); | |
115 | ||
116 | $payload = '' if !defined($payload); | |
117 | $payload .= $frame_data; | |
118 | ||
119 | if ($opcode == 1 || $opcode == 2) { | |
120 | # continue | |
121 | } elsif ($opcode == 8) { | |
122 | my $statuscode = unpack ("n", $frame_data); | |
123 | $req_close = 1; | |
124 | } else { | |
125 | die "received unhandled websocket opcode $opcode\n"; | |
126 | } | |
127 | } | |
128 | ||
129 | return ($payload, $req_close); | |
130 | }; | |
131 | ||
132 | my $full_write = sub { | |
133 | my ($fh, $data) = @_; | |
134 | ||
135 | my $len = length($data); | |
136 | my $todo = $len; | |
137 | my $offset = 0; | |
138 | while(1) { | |
139 | my $nr = syswrite($fh, $data, $todo, $offset); | |
140 | if (!defined($nr)) { | |
141 | next if $! == EINTR || $! == EAGAIN; | |
142 | die "console write error - $!\n" | |
143 | } | |
144 | $offset += $nr; | |
145 | $todo -= $nr; | |
146 | last if $todo <= 0; | |
147 | } | |
148 | ||
149 | return $len; | |
150 | }; | |
151 | ||
152 | __PACKAGE__->register_method ({ | |
153 | name => 'enter', | |
154 | path => 'enter', | |
155 | method => 'POST', | |
156 | description => "Enter container console.", | |
157 | parameters => { | |
158 | additionalProperties => 0, | |
159 | properties => { | |
160 | remote => get_standard_option('pveclient-remote-name'), | |
161 | vmid => { | |
162 | description => "The container ID", | |
163 | type => 'string', | |
164 | }, | |
165 | }, | |
166 | }, | |
167 | returns => { type => 'null'}, | |
168 | code => sub { | |
169 | my ($param) = @_; | |
170 | ||
171 | my $config = PVE::APIClient::Config->load(); | |
172 | my $conn = PVE::APIClient::Config->remote_conn($config, $param->{remote}); | |
173 | ||
174 | # Get the real node from the resources endpoint | |
175 | my $resource_list = $conn->get("api2/json/cluster/resources", { type => 'vm'}); | |
176 | my ($resource) = grep { $_->{type} eq "lxc" && $_->{vmid} eq $param->{vmid}} @$resource_list; | |
177 | ||
178 | die "container '$param->{vmid}' does not exist\n" | |
179 | if !(defined($resource) && defined($resource->{node})); | |
180 | ||
181 | my $node = $resource->{node}; | |
182 | ||
183 | my $api_path = "api2/json/nodes/$node/lxc/$param->{vmid}"; | |
184 | ||
185 | my $termproxy = $conn->post("${api_path}/termproxy", {}); | |
186 | ||
187 | my $web_socket = IO::Socket::SSL->new( | |
188 | PeerHost => $conn->{host}, | |
189 | PeerPort => $conn->{port}, | |
190 | SSL_verify_mode => SSL_VERIFY_NONE, # fixme: ??? | |
191 | timeout => 30) || | |
192 | die "failed to connect: $!\n"; | |
193 | ||
194 | # WebSocket Handshake | |
195 | ||
196 | my ($request, $wskey) = $build_web_socket_request->( | |
197 | $conn->{host}, "/$api_path/vncwebsocket", $conn->{ticket}, $termproxy); | |
198 | ||
199 | $full_write->($web_socket, $request); | |
200 | ||
201 | my $wsbuf = ''; | |
202 | ||
203 | my $wb_socket_read_available_bytes = sub { | |
204 | my $nr = $web_socket->sysread($wsbuf, $max_payload_size, length($wsbuf)); | |
205 | die "web socket read error - $!\n" if $nr < 0; | |
206 | return $nr; | |
207 | }; | |
208 | ||
209 | my $raw_response = ''; | |
210 | ||
211 | while(1) { | |
212 | my $nr = $wb_socket_read_available_bytes->(); | |
213 | if ($wsbuf =~ s/^(.*?)$CRLF$CRLF//s) { | |
214 | $raw_response = $1; | |
215 | last; | |
216 | } | |
217 | last if !$nr; | |
218 | }; | |
219 | ||
220 | # Note: we keep any remaining data in $wsbuf | |
221 | ||
222 | my $response = HTTP::Response->parse($raw_response); | |
223 | ||
224 | # Note: Digest::SHA::sha1_base64 has wrong padding | |
225 | my $wsaccept = Digest::SHA::sha1_base64("${wskey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11") . "="; | |
226 | ||
227 | die "got invalid websocket reponse: $raw_response\n" | |
228 | if !(($response->code == 101) && | |
229 | (lc $response->header('connection') eq 'upgrade') && | |
230 | (lc $response->header('upgrade') eq 'websocket') && | |
231 | ($response->header('sec-websocket-protocol') eq 'binary') && | |
232 | ($response->header('sec-websocket-accept') eq $wsaccept)); | |
233 | ||
234 | # send auth again... | |
235 | my $frame = $create_websockt_frame->($termproxy->{user} . ":" . $termproxy->{ticket} . "\n"); | |
236 | $full_write->($web_socket, $frame); | |
237 | ||
238 | # Send resize command | |
239 | my ($columns, $rows) = PVE::PTY::tcgetsize(*STDIN); | |
240 | $frame = $create_websockt_frame->("1:$columns:$rows:"); | |
241 | $full_write->($web_socket, $frame); | |
242 | ||
243 | # Set STDIN to "raw -echo" mode | |
244 | my $old_termios = PVE::PTY::tcgetattr(*STDIN); | |
245 | my $raw_termios = {%$old_termios}; | |
246 | ||
247 | my $select = IO::Select->new; | |
248 | ||
249 | eval { | |
250 | $SIG{TERM} = $SIG{INT} = $SIG{KILL} = sub { die "received interrupt\n"; }; | |
251 | ||
252 | PVE::PTY::cfmakeraw($raw_termios); | |
253 | PVE::PTY::tcsetattr(*STDIN, $raw_termios); | |
254 | ||
255 | # And set it to non-blocking so we can every char with IO::Select. | |
256 | STDIN->blocking(0); | |
257 | ||
258 | $web_socket->blocking(1); | |
259 | $select->add($web_socket); | |
260 | my $input_fh = fileno(STDIN); | |
261 | $select->add($input_fh); | |
262 | ||
263 | my $ctrl_a_pressed_before = 0; | |
264 | ||
265 | my $winch_received = 0; | |
266 | $SIG{WINCH} = sub { $winch_received = 1; }; | |
267 | ||
268 | my $check_terminal_size = sub { | |
269 | my ($ncols, $nrows) = PVE::PTY::tcgetsize(*STDIN); | |
270 | if ($ncols != $columns or $nrows != $rows) { | |
271 | $columns = $ncols; | |
272 | $rows = $nrows; | |
273 | $frame = $create_websockt_frame->("1:$columns:$rows:"); | |
274 | $full_write->($web_socket, $frame); | |
275 | } | |
276 | $winch_received = 0; | |
277 | }; | |
278 | ||
279 | while (1) { | |
280 | while(my @ready = $select->can_read(3)) { | |
281 | $check_terminal_size->() if $winch_received; | |
282 | ||
283 | foreach my $fh (@ready) { | |
284 | ||
285 | if ($fh == $web_socket) { | |
286 | # Read from WebSocket | |
287 | ||
288 | my $nr = $wb_socket_read_available_bytes->(); | |
289 | if (!defined($nr)) { | |
290 | die "web socket read error $!\n"; | |
291 | } elsif ($nr == 0) { | |
292 | return; # EOF | |
293 | } else { | |
294 | my ($payload, $req_close) = $parse_web_socket_frame->(\$wsbuf); | |
295 | if ($payload) { | |
296 | $full_write->(\*STDOUT, $payload); | |
297 | } | |
298 | return if $req_close; | |
299 | } | |
300 | ||
301 | } elsif ($fh == $input_fh) { | |
302 | # Read from STDIN | |
303 | ||
304 | my $nr = read(\*STDIN, my $buff, 4096); | |
305 | return if !$nr; # EOF or error | |
306 | ||
307 | my $char = ord($buff); | |
308 | ||
309 | # check for CTRL-a-q | |
310 | return if $ctrl_a_pressed_before == 1 && $char == hex("0x71"); | |
311 | ||
312 | $ctrl_a_pressed_before = ($char == hex("0x01") && $ctrl_a_pressed_before == 0) ? 1 : 0; | |
313 | ||
314 | my $frame = $create_websockt_frame->("0:" . $nr . ":" . $buff); | |
315 | $full_write->($web_socket, $frame); | |
316 | } | |
317 | } | |
318 | } | |
319 | $check_terminal_size->() if $winch_received; | |
320 | ||
321 | # got timeout | |
322 | $full_write->($web_socket, $create_websockt_frame->("2")); # ping server to keep connection alive | |
323 | } | |
324 | }; | |
325 | my $err = $@; | |
326 | ||
327 | eval { # cleanup | |
328 | ||
329 | # switch back to blocking mode (else later shell commands will fail). | |
330 | STDIN->blocking(1); | |
331 | ||
332 | if ($web_socket->connected) { | |
333 | # close connection | |
334 | my $msg = "\x88" . pack('N', 0) . pack('n', 0); # Opcode, mask, statuscode | |
335 | $full_write->($web_socket, $msg); | |
336 | close($web_socket); | |
337 | } | |
338 | ||
339 | # Reset the terminal parameters. | |
340 | $full_write->(\*STDOUT, "\e[24H\r\n"); | |
341 | PVE::PTY::tcsetattr(*STDIN, $old_termios); | |
342 | }; | |
343 | warn $@ if $@; # show cleanup errors | |
344 | ||
345 | print STDERR "\nERROR: $err" if $err; | |
346 | ||
347 | return undef; | |
348 | }}); | |
349 | ||
350 | __PACKAGE__->register_method ({ | |
351 | name => 'list', | |
352 | path => 'list', | |
353 | method => 'GET', | |
354 | description => "List containers.", | |
355 | parameters => { | |
356 | additionalProperties => 0, | |
357 | properties => { | |
358 | remote => get_standard_option('pveclient-remote-name'), | |
359 | }, | |
360 | }, | |
361 | returns => { type => 'null'}, | |
362 | code => sub { | |
363 | my ($param) = @_; | |
364 | ||
365 | die "implement me"; | |
366 | ||
367 | }}); | |
368 | ||
369 | ||
370 | our $cmddef = { | |
371 | enter => [ __PACKAGE__, 'enter', ['remote', 'vmid']], | |
372 | list => [ __PACKAGE__, 'list', ['remote']], | |
373 | }; | |
374 | ||
375 | 1; |