]>
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; | |
adf285bf | 11 | |
84ba32e5 | 12 | use PVE::Tools; |
3454a319 | 13 | use PVE::JSONSchema qw(get_standard_option); |
adf285bf DM |
14 | use PVE::CLIHandler; |
15 | ||
16 | use base qw(PVE::CLIHandler); | |
3454a319 | 17 | use PVE::APIClient::Config; |
84ba32e5 | 18 | |
1824383b DM |
19 | my $CRLF = "\x0D\x0A"; |
20 | my $max_payload_size = 65536; | |
21 | ||
22 | my $build_web_socket_request = sub { | |
23 | my ($path, $ticket, $termproxy) = @_; | |
24 | ||
25 | my $key = ''; | |
26 | $key .= chr(int(rand(256))) for 1 .. 16; | |
27 | my $enckey = MIME::Base64::encode_base64($key, ''); | |
28 | ||
29 | my $encticket = uri_escape($ticket); | |
30 | my $cookie = "PVEAuthCookie=$encticket; path=/; secure;"; | |
31 | ||
32 | $path .= "?port=$termproxy->{port}" . | |
33 | "&vncticket=" . uri_escape($termproxy->{ticket}); | |
34 | ||
35 | my $request = "GET $path HTTP/1.1$CRLF" | |
36 | . "Upgrade: WebSocket$CRLF" | |
37 | . "Connection: Upgrade$CRLF" | |
38 | . "Sec-WebSocket-Key: $enckey$CRLF" | |
39 | . "Sec-WebSocket-Version: 13$CRLF" | |
40 | . "Sec-WebSocket-Protocol: binary$CRLF" | |
41 | . "Cookie: $cookie$CRLF" | |
42 | . "$CRLF"; | |
43 | ||
44 | return ($request, $enckey); | |
45 | }; | |
46 | ||
47 | # FIXME: share this code with websockt code in PVE/APIServer/AnyEvent.pm ? | |
48 | my $create_websockt_frame = sub { | |
49 | my ($payload) = @_; | |
50 | ||
51 | my $string = "\x82"; # binary frame | |
52 | my $payload_len = length($payload); | |
53 | if ($payload_len <= 125) { | |
54 | $string .= pack 'C', $payload_len | 128; | |
55 | } elsif ($payload_len <= 0xffff) { | |
56 | $string .= pack 'C', 126 | 128; | |
57 | $string .= pack 'n', $payload_len; | |
58 | } else { | |
59 | $string .= pack 'C', 127 | 128; | |
60 | $string .= pack 'Q>', $payload_len; | |
61 | } | |
62 | ||
63 | $string .= pack 'N', 0; # we simply use 0 as mask | |
64 | $string .= $payload; | |
65 | ||
66 | return $string; | |
67 | }; | |
68 | ||
69 | # FIXME: share this code with websockt code in PVE/APIServer/AnyEvent.pm ? | |
70 | my $parse_web_socket_frame = sub { | |
71 | my ($wsbuf_ref, $binary, $is_server) = @_; | |
72 | ||
73 | my $wsbuf = $$wsbuf_ref; | |
74 | ||
75 | my $payload; | |
76 | my $req_close = 0; | |
77 | ||
78 | while (my $len = length($wsbuf)) { | |
79 | last if $len < 2; | |
80 | ||
81 | my $hdr = unpack('C', substr($wsbuf, 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, 1, 1); | |
91 | ||
92 | my $masked = $payload_len & 0b10000000; | |
93 | if ($is_server) { | |
94 | die "received unmasked websocket frame from client\n" if !$masked; | |
95 | } else { | |
96 | die "received masked websocket frame from server\n" if $masked; | |
97 | } | |
98 | ||
99 | my $offset = 2; | |
100 | $payload_len = $payload_len & 0b01111111; | |
101 | if ($payload_len == 126) { | |
102 | last if $len < 4; | |
103 | $payload_len = unpack('n', substr($wsbuf, $offset, 2)); | |
104 | $offset += 2; | |
105 | } elsif ($payload_len == 127) { | |
106 | last if $len < 10; | |
107 | $payload_len = unpack('Q>', substr($wsbuf, $offset, 8)); | |
108 | $offset += 8; | |
109 | } | |
110 | ||
111 | die "received too large websocket frame (len = $payload_len)\n" | |
112 | if ($payload_len > $max_payload_size) || ($payload_len < 0); | |
113 | ||
114 | my @mask = (0, 0, 0, 0); | |
115 | if ($masked) { | |
116 | last if $len < $offset + 4; | |
117 | ||
118 | @mask = (unpack('C', substr($wsbuf, $offset+0, 1)), | |
119 | unpack('C', substr($wsbuf, $offset+1, 1)), | |
120 | unpack('C', substr($wsbuf, $offset+2, 1)), | |
121 | unpack('C', substr($wsbuf, $offset+3, 1))); | |
122 | ||
123 | $offset += 4; | |
124 | } | |
125 | ||
126 | last if $len < ($offset + $payload_len); | |
127 | ||
128 | my $data = substr($wsbuf, 0, $offset + $payload_len, ''); # now consume data | |
129 | ||
130 | my $frame_data = substr($data, $offset, $payload_len); | |
131 | ||
132 | if ($masked) { | |
133 | for (my $i = 0; $i < $payload_len; $i++) { | |
134 | my $d = unpack('C', substr($frame_data, $i, 1)); | |
135 | my $n = $d ^ $mask[$i % 4]; | |
136 | substr($frame_data, $i, 1, pack('C', $n)); | |
137 | } | |
138 | } | |
139 | ||
140 | $frame_data = decode_base64($frame_data) if !$binary; | |
141 | ||
142 | $payload = '' if !defined($payload); | |
143 | $payload .= $frame_data; | |
144 | ||
145 | if ($opcode == 1 || $opcode == 2) { | |
146 | # continue | |
147 | } elsif ($opcode == 8) { | |
148 | my $statuscode = unpack ("n", $frame_data); | |
149 | $req_close = 1; | |
150 | } else { | |
151 | die "received unhandled websocket opcode $opcode\n"; | |
152 | } | |
153 | } | |
154 | ||
155 | return ($payload, $req_close); | |
156 | }; | |
157 | ||
adf285bf DM |
158 | __PACKAGE__->register_method ({ |
159 | name => 'enter', | |
160 | path => 'enter', | |
161 | method => 'POST', | |
162 | description => "Enter container console.", | |
163 | parameters => { | |
164 | additionalProperties => 0, | |
165 | properties => { | |
3454a319 | 166 | remote => get_standard_option('pveclient-remote-name'), |
adf285bf DM |
167 | vmid => { |
168 | description => "The container ID", | |
169 | type => 'string', | |
170 | }, | |
171 | }, | |
172 | }, | |
173 | returns => { type => 'null'}, | |
174 | code => sub { | |
175 | my ($param) = @_; | |
176 | ||
aa570b38 | 177 | my $conn = PVE::APIClient::Config::get_remote_connection($param->{remote}); |
1824383b DM |
178 | |
179 | # Get the real node from the resources endpoint | |
180 | my $resource_list = $conn->get("api2/json/cluster/resources", { type => 'vm'}); | |
181 | my ($resource) = grep { $_->{type} eq "lxc" && $_->{vmid} eq $param->{vmid}} @$resource_list; | |
182 | ||
183 | die "container '$param->{vmid}' does not exist\n" | |
184 | if !(defined($resource) && defined($resource->{node})); | |
185 | ||
186 | my $node = $resource->{node}; | |
84ba32e5 DM |
187 | |
188 | my $api_path = "api2/json/nodes/$node/lxc/$param->{vmid}"; | |
189 | ||
1824383b | 190 | my $termproxy = $conn->post("${api_path}/termproxy", {}); |
84ba32e5 | 191 | |
1824383b DM |
192 | my $web_socket = IO::Socket::SSL->new( |
193 | PeerHost => $conn->{host}, | |
194 | PeerPort => $conn->{port}, | |
195 | SSL_verify_mode => SSL_VERIFY_NONE, # fixme: ??? | |
196 | timeout => 30) || | |
197 | die "failed to connect: $!\n"; | |
198 | ||
199 | # WebSocket Handshake | |
200 | ||
201 | my ($request, $wskey) = $build_web_socket_request->( | |
202 | "/$api_path/vncwebsocket", $conn->{ticket}, $termproxy); | |
203 | ||
204 | $web_socket->syswrite($request); | |
205 | ||
206 | my $wsbuf = ''; | |
207 | ||
208 | my $wb_socket_read_available_bytes = sub { | |
209 | my $nr = $web_socket->sysread($wsbuf, $max_payload_size, length($wsbuf)); | |
210 | die "web socket read error - $!\n" if $nr < 0; | |
211 | return $nr; | |
212 | }; | |
213 | ||
214 | my $raw_response = ''; | |
215 | ||
216 | while(1) { | |
217 | my $nr = $wb_socket_read_available_bytes->(); | |
218 | if ($wsbuf =~ s/^(.*?)$CRLF$CRLF//s) { | |
219 | $raw_response = $1; | |
220 | last; | |
221 | } | |
222 | last if !$nr; | |
223 | }; | |
224 | ||
225 | # Note: we keep any remaining data in $wsbuf | |
226 | ||
227 | my $response = HTTP::Response->parse($raw_response); | |
228 | ||
229 | # Note: Digest::SHA::sha1_base64 has wrong padding | |
230 | my $wsaccept = Digest::SHA::sha1_base64("${wskey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11") . "="; | |
231 | ||
232 | die "got invalid websocket reponse: $raw_response\n" | |
233 | if !(($response->code == 101) && | |
234 | ($response->header('connection') eq 'upgrade') && | |
235 | ($response->header('upgrade') eq 'websocket') && | |
236 | ($response->header('sec-websocket-protocol') eq 'binary') && | |
237 | ($response->header('sec-websocket-accept') eq $wsaccept)); | |
238 | ||
239 | # send auth again... | |
240 | my $frame = $create_websockt_frame->($termproxy->{user} . ":" . $termproxy->{ticket} . "\n"); | |
241 | $web_socket->syswrite($frame); | |
242 | ||
243 | my $select = IO::Select->new; | |
244 | ||
245 | $web_socket->blocking(0); | |
246 | $select->add($web_socket); | |
247 | ||
248 | while(my @ready = $select->can_read) { | |
249 | foreach my $fh (@ready) { | |
250 | if ($fh == $web_socket) { | |
251 | my $nr = $wb_socket_read_available_bytes->(); | |
252 | my ($payload, $req_close) = $parse_web_socket_frame->(\$wsbuf, 1); | |
253 | print "GOT: $payload\n" if defined($payload); | |
254 | last if $req_close; | |
255 | last if !$nr; # eos | |
256 | } else { | |
257 | die "internal error - unknown handle"; | |
258 | } | |
259 | } | |
260 | } | |
adf285bf DM |
261 | |
262 | }}); | |
263 | ||
264 | __PACKAGE__->register_method ({ | |
265 | name => 'list', | |
266 | path => 'list', | |
267 | method => 'GET', | |
268 | description => "List containers.", | |
269 | parameters => { | |
270 | additionalProperties => 0, | |
271 | properties => { | |
3454a319 | 272 | remote => get_standard_option('pveclient-remote-name'), |
adf285bf DM |
273 | }, |
274 | }, | |
275 | returns => { type => 'null'}, | |
276 | code => sub { | |
277 | my ($param) = @_; | |
278 | ||
279 | die "implement me"; | |
280 | ||
281 | }}); | |
282 | ||
283 | ||
284 | our $cmddef = { | |
84ba32e5 | 285 | enter => [ __PACKAGE__, 'enter', ['remote', 'vmid']], |
adf285bf DM |
286 | list => [ __PACKAGE__, 'list', ['remote']], |
287 | }; | |
288 | ||
289 | 1; |