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