]>
Commit | Line | Data |
---|---|---|
4a17e72e DM |
1 | package PVE::Service::pveproxy; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
bc917275 | 6 | use Data::Dumper; |
4a17e72e | 7 | use Encode; |
bc917275 TL |
8 | use HTTP::Response; |
9 | use Template; | |
4a17e72e | 10 | use URI::QueryParam; |
bc917275 TL |
11 | use URI; |
12 | ||
4a17e72e | 13 | use PVE::API2; |
b996e6c0 | 14 | use PVE::APIServer::AnyEvent; |
bc917275 TL |
15 | use PVE::APIServer::Formatter::HTML; |
16 | use PVE::APIServer::Formatter::Standard; | |
17 | use PVE::APIServer::Formatter; | |
18 | use PVE::APIServer::Utils; | |
19 | use PVE::Cluster; | |
20 | use PVE::Daemon; | |
21 | use PVE::DataCenterConfig; | |
4a17e72e | 22 | use PVE::HTTPServer; |
bc917275 | 23 | use PVE::SafeSyslog; |
54165ad3 | 24 | use PVE::pvecfg; |
4a17e72e DM |
25 | use PVE::Tools; |
26 | ||
27 | use base qw(PVE::Daemon); | |
28 | ||
29 | my $cmdline = [$0, @ARGV]; | |
30 | ||
31 | my %daemon_options = ( | |
32 | max_workers => 3, | |
33 | restart_on_error => 5, | |
34 | stop_wait_time => 15, | |
35 | leave_children_open_on_reload => 1, | |
36 | setuid => 'www-data', | |
37 | setgid => 'www-data', | |
38 | pidfile => '/var/run/pveproxy/pveproxy.pid', | |
39 | ); | |
40 | ||
41 | my $daemon = __PACKAGE__->new('pveproxy', $cmdline, %daemon_options); | |
42 | ||
43 | sub add_dirs { | |
44 | my ($result_hash, $alias, $subdir) = @_; | |
45 | ||
e4697709 | 46 | PVE::APIServer::AnyEvent::add_dirs($result_hash, $alias, $subdir); |
4a17e72e DM |
47 | } |
48 | ||
245e567e | 49 | my $basedirs = { |
245e567e | 50 | docs => '/usr/share/pve-docs', |
8829bad3 | 51 | extjs => '/usr/share/javascript/extjs', |
7f3b89a0 | 52 | fontawesome => '/usr/share/fonts-font-awesome', |
4845cca7 | 53 | fontlogos => '/usr/share/fonts-font-logos', |
8829bad3 TL |
54 | i18n => '/usr/share/pve-i18n', |
55 | manager => '/usr/share/pve-manager', | |
56 | novnc => '/usr/share/novnc-pve', | |
80cb7dda | 57 | sencha_touch => '/usr/share/javascript/sencha-touch', |
f90908cb | 58 | widgettoolkit => '/usr/share/javascript/proxmox-widget-toolkit', |
8829bad3 | 59 | xtermjs => '/usr/share/pve-xtermjs', |
245e567e DC |
60 | }; |
61 | ||
4a17e72e DM |
62 | sub init { |
63 | my ($self) = @_; | |
64 | ||
65 | # we use same ALLOW/DENY/POLICY as pveproxy | |
a642f8a0 | 66 | my $proxyconf = PVE::APIServer::Utils::read_proxy_config($self->{name}); |
4a17e72e DM |
67 | |
68 | my $accept_lock_fn = "/var/lock/pveproxy.lck"; | |
69 | ||
70 | my $lockfh = IO::File->new(">>${accept_lock_fn}") || | |
71 | die "unable to open lock file '${accept_lock_fn}' - $!\n"; | |
72 | ||
36ad2b3c | 73 | my $listen_ip = $proxyconf->{LISTEN_IP}; |
e224b7d2 | 74 | my $socket = $self->create_reusable_socket(8006, $listen_ip); |
4a17e72e DM |
75 | |
76 | my $dirs = {}; | |
77 | ||
919cfa29 TL |
78 | add_dirs($dirs, '/novnc/' => "$basedirs->{novnc}/"); |
79 | add_dirs($dirs, '/pve-docs/' => "$basedirs->{docs}/"); | |
80 | add_dirs($dirs, '/pve-docs/api-viewer/extjs/' => "$basedirs->{extjs}/"); | |
81 | add_dirs($dirs, '/pve2/css/' => "$basedirs->{manager}/css/"); | |
245e567e | 82 | add_dirs($dirs, '/pve2/ext6/', "$basedirs->{extjs}/"); |
919cfa29 TL |
83 | add_dirs($dirs, '/pve2/fa/css/' => "$basedirs->{fontawesome}/css/"); |
84 | add_dirs($dirs, '/pve2/fa/fonts/' => "$basedirs->{fontawesome}/fonts/"); | |
4845cca7 | 85 | add_dirs($dirs, '/pve2/font-logos/' => "$basedirs->{fontlogos}/"); |
245e567e | 86 | add_dirs($dirs, '/pve2/images/' => "$basedirs->{manager}/images/"); |
245e567e | 87 | add_dirs($dirs, '/pve2/js/' => "$basedirs->{manager}/js/"); |
919cfa29 | 88 | add_dirs($dirs, '/pve2/locale/', "$basedirs->{i18n}/"); |
80cb7dda | 89 | add_dirs($dirs, '/pve2/sencha-touch/', "$basedirs->{sencha_touch}/"); |
919cfa29 | 90 | add_dirs($dirs, '/pve2/touch/', "$basedirs->{manager}/touch/"); |
cc8c253f | 91 | add_dirs($dirs, '/pwt/css/' => "$basedirs->{widgettoolkit}/css/"); |
919cfa29 | 92 | add_dirs($dirs, '/pwt/images/' => "$basedirs->{widgettoolkit}/images/"); |
5137b16f | 93 | add_dirs($dirs, '/pwt/themes/' => "$basedirs->{widgettoolkit}/themes/"); |
919cfa29 | 94 | add_dirs($dirs, '/xtermjs/' => "$basedirs->{xtermjs}/"); |
4a17e72e DM |
95 | |
96 | $self->{server_config} = { | |
a9de2d44 | 97 | title => 'Proxmox VE API', |
4a17e72e DM |
98 | keep_alive => 100, |
99 | max_conn => 500, | |
100 | max_requests => 1000, | |
101 | lockfile => $accept_lock_fn, | |
102 | socket => $socket, | |
103 | lockfh => $lockfh, | |
104 | debug => $self->{debug}, | |
105 | trusted_env => 0, # not trusted, anyone can connect | |
106 | logfile => '/var/log/pveproxy/access.log', | |
107 | allow_from => $proxyconf->{ALLOW_FROM}, | |
108 | deny_from => $proxyconf->{DENY_FROM}, | |
109 | policy => $proxyconf->{POLICY}, | |
110 | ssl => { | |
95035118 | 111 | cipher_list => $proxyconf->{CIPHERS}, |
ff65c929 | 112 | ciphersuites => $proxyconf->{CIPHERSUITES}, |
4a17e72e DM |
113 | key_file => '/etc/pve/local/pve-ssl.key', |
114 | cert_file => '/etc/pve/local/pve-ssl.pem', | |
95035118 | 115 | honor_cipher_order => $proxyconf->{HONOR_CIPHER_ORDER}, |
4a17e72e | 116 | }, |
a33abad1 | 117 | compression => $proxyconf->{COMPRESSION}, |
4a17e72e DM |
118 | # Note: there is no authentication for those pages and dirs! |
119 | pages => { | |
c7f32808 | 120 | '/' => sub { get_index($self->{nodename}, @_) }, |
4a17e72e DM |
121 | # avoid authentication when accessing favicon |
122 | '/favicon.ico' => { | |
245e567e | 123 | file => "$basedirs->{manager}/images/favicon.ico", |
4a17e72e | 124 | }, |
f90908cb DC |
125 | '/proxmoxlib.js' => { |
126 | file => "$basedirs->{widgettoolkit}/proxmoxlib.js", | |
127 | }, | |
6b2028cb WB |
128 | '/qrcode.min.js' => { |
129 | file => '/usr/share/javascript/qrcodejs/qrcode.min.js', | |
130 | }, | |
4a17e72e DM |
131 | }, |
132 | dirs => $dirs, | |
133 | }; | |
41196653 | 134 | |
95035118 | 135 | if (defined($proxyconf->{DHPARAMS})) { |
41196653 | 136 | $self->{server_config}->{ssl}->{dh_file} = $proxyconf->{DHPARAMS}; |
41196653 | 137 | } |
ed59fcff FG |
138 | if (defined($proxyconf->{DISABLE_TLS_1_2})) { |
139 | $self->{server_config}->{ssl}->{tlsv1_2} = !$proxyconf->{DISABLE_TLS_1_2}; | |
140 | } | |
141 | if (defined($proxyconf->{DISABLE_TLS_1_3})) { | |
142 | $self->{server_config}->{ssl}->{tlsv1_3} = !$proxyconf->{DISABLE_TLS_1_3}; | |
143 | } | |
64672c28 FG |
144 | my $custom_key_path = '/etc/pve/local/pveproxy-ssl.key'; |
145 | if (defined($proxyconf->{TLS_KEY_FILE})) { | |
146 | $custom_key_path = $proxyconf->{TLS_KEY_FILE}; | |
147 | } | |
148 | if (-f '/etc/pve/local/pveproxy-ssl.pem' && -f $custom_key_path) { | |
299d290c | 149 | $self->{server_config}->{ssl}->{cert_file} = '/etc/pve/local/pveproxy-ssl.pem'; |
64672c28 | 150 | $self->{server_config}->{ssl}->{key_file} = $custom_key_path; |
299d290c FG |
151 | syslog('info', 'Using \'/etc/pve/local/pveproxy-ssl.pem\' as certificate for the web interface.'); |
152 | } | |
4a17e72e DM |
153 | } |
154 | ||
155 | sub run { | |
156 | my ($self) = @_; | |
157 | ||
158 | my $server = PVE::HTTPServer->new(%{$self->{server_config}}); | |
159 | $server->run(); | |
160 | } | |
161 | ||
162 | $daemon->register_start_command(); | |
163 | $daemon->register_restart_command(1); | |
164 | $daemon->register_stop_command(); | |
165 | $daemon->register_status_command(); | |
166 | ||
167 | our $cmddef = { | |
168 | start => [ __PACKAGE__, 'start', []], | |
169 | restart => [ __PACKAGE__, 'restart', []], | |
170 | stop => [ __PACKAGE__, 'stop', []], | |
171 | status => [ __PACKAGE__, 'status', [], undef, sub { print shift . "\n";} ], | |
172 | }; | |
173 | ||
174 | sub is_phone { | |
175 | my ($ua) = @_; | |
176 | ||
177 | return 0 if !$ua; | |
178 | ||
179 | return 1 if $ua =~ m/(iPhone|iPod|Windows Phone)/; | |
180 | ||
181 | if ($ua =~ m/Mobile(\/|\s)/) { | |
182 | return 1 if $ua =~ m/(BlackBerry|BB)/; | |
183 | return 1 if ($ua =~ m/(Android)/) && ($ua !~ m/(Silk)/); | |
184 | } | |
185 | ||
186 | return 0; | |
187 | } | |
188 | ||
189 | # NOTE: Requests to those pages are not authenticated | |
190 | # so we must be very careful here | |
191 | ||
192 | sub get_index { | |
c7f32808 | 193 | my ($nodename, $server, $r, $args) = @_; |
4a17e72e | 194 | |
f4aa76c5 | 195 | my $lang; |
4a17e72e DM |
196 | my $username; |
197 | my $token = 'null'; | |
f16342f3 | 198 | my $theme = "auto"; |
4a17e72e DM |
199 | |
200 | if (my $cookie = $r->header('Cookie')) { | |
201 | if (my $newlang = ($cookie =~ /(?:^|\s)PVELangCookie=([^;]*)/)[0]) { | |
202 | if ($newlang =~ m/^[a-z]{2,3}(_[A-Z]{2,3})?$/) { | |
203 | $lang = $newlang; | |
204 | } | |
205 | } | |
5137b16f SS |
206 | |
207 | if (my $newtheme = ($cookie =~ /(?:^|\s)PVEThemeCookie=([^;]*)/)[0]) { | |
208 | # theme names need to be kebab case, with each segment a maximum of 10 characters long | |
209 | # and at most 6 segments | |
210 | if ($newtheme =~ m/^[a-z]{1,10}(-[a-z]{1,10}){0,5}$/) { | |
211 | $theme = $newtheme; | |
212 | } | |
213 | } | |
214 | ||
9a5a1655 | 215 | my $ticket = PVE::APIServer::Formatter::extract_auth_value($cookie, $server->{cookie_name}); |
4a17e72e DM |
216 | if (($username = PVE::AccessControl::verify_ticket($ticket, 1))) { |
217 | $token = PVE::AccessControl::assemble_csrf_prevention_token($username); | |
218 | } | |
219 | } | |
220 | ||
f4aa76c5 DC |
221 | if (!$lang) { |
222 | my $dc_conf = PVE::Cluster::cfs_read_file('datacenter.cfg'); | |
223 | $lang = $dc_conf->{language} // 'en'; | |
224 | } | |
225 | ||
6219f6c8 | 226 | my $mobile = (is_phone($r->header('User-Agent')) && (!defined($args->{mobile}) || $args->{mobile})) || $args->{mobile}; |
4a17e72e | 227 | |
03f09f9a DC |
228 | my $novnc = defined($args->{console}) && $args->{novnc}; |
229 | my $xtermjs = defined($args->{console}) && $args->{xtermjs}; | |
230 | ||
f16342f3 | 231 | my $langfile = -f "$basedirs->{i18n}/pve-lang-$lang.js" ? 1 : 0; |
184825e1 | 232 | |
180a86d3 | 233 | my $version = PVE::pvecfg::version(); |
54165ad3 | 234 | |
f90908cb | 235 | my $wtversionraw = PVE::Tools::file_read_firstline("$basedirs->{widgettoolkit}/proxmoxlib.js"); |
f16342f3 | 236 | my $wtversion = $wtversionraw =~ m|^// (.*)$| ? $1 : ''; |
f90908cb | 237 | |
c195a3c2 TL |
238 | my $debug = $server->{debug}; |
239 | if (exists $args->{debug}) { | |
240 | $debug = !defined($args->{debug}) || $args->{debug}; | |
241 | } | |
242 | ||
184825e1 DC |
243 | my $vars = { |
244 | lang => $lang, | |
245 | langfile => $langfile, | |
f16342f3 | 246 | username => $username || '', |
184825e1 DC |
247 | token => $token, |
248 | console => $args->{console}, | |
249 | nodename => $nodename, | |
c195a3c2 | 250 | debug => $debug, |
180a86d3 | 251 | version => "$version", |
f90908cb | 252 | wtversion => $wtversion, |
5137b16f | 253 | theme => $theme, |
184825e1 DC |
254 | }; |
255 | ||
256 | # by default, load the normal index | |
257 | my $dir = $basedirs->{manager}; | |
4a17e72e | 258 | |
03f09f9a | 259 | if ($novnc) { |
184825e1 | 260 | $dir = $basedirs->{novnc}; |
03f09f9a DC |
261 | } elsif ($xtermjs) { |
262 | $dir = $basedirs->{xtermjs}; | |
4a17e72e | 263 | } elsif ($mobile) { |
184825e1 | 264 | $dir = "$basedirs->{manager}/touch"; |
2ebf4aec | 265 | } |
184825e1 | 266 | |
f16342f3 TL |
267 | my $page = ''; |
268 | my $template = Template->new({ABSOLUTE => 1}); | |
269 | ||
270 | $template->process("$dir/index.html.tpl", $vars, \$page) || die $template->error(), "\n"; | |
271 | ||
4a17e72e DM |
272 | my $headers = HTTP::Headers->new(Content_Type => "text/html; charset=utf-8"); |
273 | my $resp = HTTP::Response->new(200, "OK", $headers, $page); | |
274 | ||
275 | return $resp; | |
276 | } | |
277 | ||
278 | 1; |