]> git.proxmox.com Git - pve-xtermjs.git/blob - src/PVE/CLI/termproxy.pm
c45eb509fe7a9bb23cc2186cd921a5c540e55b99
[pve-xtermjs.git] / src / PVE / CLI / termproxy.pm
1 package PVE::CLI::termproxy;
2
3 use strict;
4 use warnings;
5
6 use PVE::RPCEnvironment;
7 use PVE::CLIHandler;
8 use PVE::JSONSchema qw(get_standard_option);
9 use PVE::AccessControl;
10 use PVE::PTY;
11 use IO::Select;
12 use IO::Socket::IP;
13
14 use base qw(PVE::CLIHandler);
15
16 use constant MAX_QUEUE_LEN => 16*1024;
17
18 sub setup_environment {
19 PVE::RPCEnvironment->setup_default_cli_env();
20 }
21
22 sub listen_and_authenticate {
23 my ($port, $timeout) = @_;
24
25 my $params = {
26 Listen => 1,
27 ReuseAddr => 1,
28 Proto => &Socket::IPPROTO_TCP,
29 GetAddrInfoFlags => 0,
30 LocalAddr => 'localhost',
31 LocalPort => $port,
32 };
33
34 my $socket = IO::Socket::IP->new(%$params) or die "failed to open socket: $!\n";
35
36 alarm 0;
37 local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
38 alarm $timeout;
39 my $client = $socket->accept; # Wait for a client
40 alarm 0;
41 close($socket);
42
43 my $queue;
44 my $n = sysread($client, $queue, 4096);
45 if ($n && $queue =~ s/^([^:]+):([^:]+):(.+)\n//) {
46 my $user = $1;
47 my $path = $2;
48 my $ticket = $3;
49
50 die "authentication failed\n"
51 if !PVE::AccessControl::verify_vnc_ticket($ticket, $user, $path);
52
53 die "aknowledge failed\n"
54 if !syswrite($client, "OK");
55
56 } else {
57 die "malformed authentication string\n";
58 }
59
60 return ($queue, $client);
61 }
62
63 sub run_pty {
64 my ($cmd, $webhandle, $queue) = @_;
65
66 foreach my $k (keys %ENV) {
67 next if $k eq 'PATH' || $k eq 'USER' || $k eq 'HOME' || $k eq 'LANG' || $k eq 'LANGUAGE';
68 next if $k =~ m/^LC_/;
69 delete $ENV{$k};
70 }
71
72 $ENV{TERM} = 'xterm-256color';
73
74 my $pty = PVE::PTY->new();
75
76 my $pid = fork();
77 die "fork: $!\n" if !defined($pid);
78 if (!$pid) {
79 $pty->make_controlling_terminal();
80 exec {$cmd->[0]} @$cmd
81 or POSIX::_exit(1);
82 }
83
84 $pty->set_size(80,20);
85
86 read_write_loop($webhandle, $pty->master, $queue, $pty);
87
88 $pty->close();
89 waitpid($pid,0);
90 exit(0);
91 }
92
93 sub read_write_loop {
94 my ($webhandle, $cmdhandle, $queue, $pty) = @_;
95
96 my $select = new IO::Select;
97
98 $select->add($webhandle);
99 $select->add($cmdhandle);
100
101 my @handles;
102
103 # we may have already messages from the first read
104 $queue = process_queue($queue, $cmdhandle, $pty);
105
106 my $timeout = 5*60;
107
108 while($select->count && scalar(@handles = $select->can_read($timeout))) {
109 foreach my $h (@handles) {
110 my $buf;
111 my $n = $h->sysread($buf, 4096);
112
113 if ($h == $webhandle) {
114 if ($n && (length($queue) + $n) < MAX_QUEUE_LEN) {
115 $queue = process_queue($queue.$buf, $cmdhandle, $pty);
116 } else {
117 return;
118 }
119 } elsif ($h == $cmdhandle) {
120 if ($n) {
121 syswrite($webhandle, $buf);
122 } else {
123 return;
124 }
125 }
126 }
127 }
128 }
129
130 sub process_queue {
131 my ($queue, $handle, $pty) = @_;
132
133 my $msg;
134 while(length($queue)) {
135 ($queue, $msg) = remove_message($queue, $pty);
136 last if !defined($msg);
137 syswrite($handle, $msg);
138 }
139 return $queue;
140 }
141
142
143 # we try to remove a whole message
144 # if we succeed, we return the remaining queue and the msg
145 # if we fail, the message is undef and the queue is not changed
146 sub remove_message {
147 my ($queue, $pty) = @_;
148
149 my $msg;
150 my $type = substr $queue, 0, 1;
151
152 if ($type eq '0') {
153 # normal message
154 my ($length) = $queue =~ m/^0:(\d+):/;
155 my $begin = 3 + length($length);
156 if (defined($length) && length($queue) >= ($length + $begin)) {
157 $msg = substr $queue, $begin, $length;
158 if (defined($msg)) {
159 # msg contains now $length chars after 0:$length:
160 $queue = substr $queue, $begin + $length;
161 }
162 }
163 } elsif ($type eq '1') {
164 # resize message
165 my ($cols, $rows) = $queue =~ m/^1:(\d+):(\d+):/;
166 if (defined($cols) && defined($rows)) {
167 $queue = substr $queue, (length($cols) + length ($rows) + 4);
168 eval { $pty->set_size($cols, $rows) if defined($pty) };
169 warn $@ if $@;
170 $msg = "";
171 }
172 } elsif ($type eq '2') {
173 # ping
174 $queue = substr $queue, 1;
175 $msg = "";
176 } else {
177 # ignore other input
178 $queue = substr $queue, 1;
179 $msg = "";
180 }
181
182 return ($queue, $msg);
183 }
184
185 __PACKAGE__->register_method ({
186 name => 'exec',
187 path => 'exec',
188 method => 'POST',
189 description => "Connects a TCP Socket with a commandline",
190 parameters => {
191 additionalProperties => 0,
192 properties => {
193 port => {
194 type => 'integer',
195 description => "The port to listen on."
196 },
197 'extra-args' => get_standard_option('extra-args'),
198 },
199 },
200 returns => { type => 'null'},
201 code => sub {
202 my ($param) = @_;
203
204 my $cmd;
205 if (defined($param->{'extra-args'})) {
206 $cmd = [@{$param->{'extra-args'}}];
207 } else {
208 die "No command given\n";
209 }
210
211 my ($queue, $handle) = listen_and_authenticate($param->{port}, 10);
212
213 run_pty($cmd, $handle, $queue);
214
215 return undef;
216 }});
217
218 our $cmddef = [ __PACKAGE__, 'exec', ['port', 'extra-args' ]];
219
220 1;