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