This commit is contained in:
Дмитрий Кустов 2024-12-09 18:54:19 +03:00
commit de116796d4
21 changed files with 4485 additions and 0 deletions

1
.gear/rules Normal file
View File

@ -0,0 +1 @@
tar: .

3
.gear/upstream/remotes Normal file
View File

@ -0,0 +1,3 @@
[remote "upstream"]
url = git://git.proxmox.com/git/pve-http-server.git
fetch = +refs/heads/*:refs/remotes/upstream/*

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
build/
*.deb
*.buildinfo
*.changes

50
Makefile Normal file
View File

@ -0,0 +1,50 @@
include /usr/share/dpkg/pkg-info.mk
PACKAGE=libpve-http-server-perl
GITVERSION:=$(shell git rev-parse HEAD)
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION)
DSC=$(PACKAGE)_$(DEB_VERSION).dsc
DEB=$(PACKAGE)_$(DEB_VERSION)_all.deb
all:
$(BUILDDIR): src debian
rm -rf $@ $@.tmp
cp -a src $@.tmp
cp -a debian $@.tmp/
echo "git clone git://git.proxmox.com/git/pve-http-server\\ngit checkout $(GITVERSION)" > $@.tmp/debian/SOURCE
mv $@.tmp $@
.PHONY: deb
deb: $(DEB)
$(DEB): $(BUILDDIR)
cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
lintian $(DEB)
.PHONY: dsc
dsc: $(DSC)
$(DSC): $(BUILDDIR)
cd $(BUILDDIR); dpkg-buildpackage -S -us -uc
lintian $(DSC)
sbuild: $(DSC)
sbuild $(DSC)
.PHONY: upload
upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
upload: $(DEB)
tar cf - $(DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg --dist $(UPLOAD_DIST)
.PHONY: clean distclean
distclean: clean
$(MAKE) -C src $@
clean:
$(MAKE) -C src $@
rm -rf $(PACKAGE)-*/ *.deb *.dsc *.tar.* *.changes *.build *.buildinfo examples/simple-demo.lck
.PHONY: dinstall
dinstall: $(DEB)
dpkg -i $(DEB)

426
debian/changelog vendored Normal file
View File

@ -0,0 +1,426 @@
libpve-http-server-perl (5.1.2) bookworm; urgency=medium
* fix #5391: proxy request: avoid "HTTP 599 Too many redirections" error
that could occur due to long-running requests and bad timing during
connection reuse. Disable connection reuse for all but GET requests that
are proxied between different nodes, and allow one retry in this case.
This can add a tiny bit of overhead if many PUT requests that are proxied
to other nodes are issued with only a small delay between each other.
However, such a high-frequency PUT request pattern is considered an edge
case, and benchmarks show that the slowdown is about 2ms on average, which
is often negligible compared to the actual time required to process the
request.
-- Proxmox Support Team <support@proxmox.com> Fri, 04 Oct 2024 14:02:39 +0200
libpve-http-server-perl (5.1.1) bookworm; urgency=medium
* handler: only allow downloads for annotated endpoints and remove support
for directly returned download info
-- Proxmox Support Team <support@proxmox.com> Mon, 23 Sep 2024 11:07:22 +0200
libpve-http-server-perl (5.1.0) bookworm; urgency=medium
* http: support the deflate compression content encoding
-- Proxmox Support Team <support@proxmox.com> Mon, 22 Apr 2024 13:14:26 +0200
libpve-http-server-perl (5.0.6) bookworm; urgency=medium
* access control: avoid "uninitialized value" warning if using IP
ranges
-- Proxmox Support Team <support@proxmox.com> Tue, 26 Mar 2024 09:16:48 +0100
libpve-http-server-perl (5.0.5) bookworm; urgency=medium
* fix #4859: properly configure TLSv1.3 only mode
-- Proxmox Support Team <support@proxmox.com> Fri, 03 Nov 2023 12:06:31 +0100
libpve-http-server-perl (5.0.4) bookworm; urgency=medium
* fix #4802: reduce CA lookups while proxying with OpenSSL as packaged in
Debian 12 Bookworm.
* avoid AnyEvent::AIO to fix CPU spinning if the pure-perl implementation
libanyevent-aio-perl is installed, for example on development machines
when trying to use the perl language server.
-- Proxmox Support Team <support@proxmox.com> Mon, 03 Jul 2023 09:38:56 +0200
libpve-http-server-perl (5.0.3) bookworm; urgency=medium
* proxy request: handle missing content-type header
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jun 2023 18:58:05 +0200
libpve-http-server-perl (5.0.2) bookworm; urgency=medium
* formatter/bootstrap: set SameSite attr of auth cookie to 'strict'
* when proxying requests, preserve json formatting instead of converting to
x-www-form-urlencoded
* support actual arrays for array parameters, as a replacement for '-list' and
'-alist' formats
-- Proxmox Support Team <support@proxmox.com> Wed, 07 Jun 2023 13:21:19 +0200
libpve-http-server-perl (5.0.1) bookworm; urgency=medium
* fix regression in the html (bootstrap) based API debug explorer, which
came in through a more strict pattern checking in a newer version of the
used URL encoding library
-- Proxmox Support Team <support@proxmox.com> Sat, 03 Jun 2023 15:15:47 +0200
libpve-http-server-perl (5.0.0) bookworm; urgency=medium
* switch over to native versioning
* various small code and packaging clean ups
* re-build for Debian 12 Bookworm based releases
-- Proxmox Support Team <support@proxmox.com> Wed, 17 May 2023 07:26:11 +0200
libpve-http-server-perl (4.2-3) bullseye; urgency=medium
* file upload: don't always calculate MD5 for syslog message, rather log the
file name instead,
* explicitly disallow tmpfilename parameter in query URL
-- Proxmox Support Team <support@proxmox.com> Fri, 14 Apr 2023 16:27:07 +0200
libpve-http-server-perl (4.2-2) bullseye; urgency=medium
* multipart upload: properly parse file parts without Content-Type
-- Proxmox Support Team <support@proxmox.com> Tue, 11 Apr 2023 14:44:03 +0200
libpve-http-server-perl (4.2-1) bullseye; urgency=medium
* fix #4494: redirect incoming HTTP requests to HTTPS to avoid common
pitfall when opening the Proxmox VE or Proxmox Mail Gateway web-interface
for the first time
-- Proxmox Support Team <support@proxmox.com> Thu, 16 Mar 2023 16:57:59 +0100
libpve-http-server-perl (4.1-6) bullseye; urgency=medium
* multipart upload: fix upload of files starting with newlines
* multipart upload: don't fail on presebce of additional headers
* multipart upload: loosen trailing-newline requirement from spec, as some
more popular clients (e.g., postman) violate that rule.
* fix #4344: http-server: fix regression that required the 'Content-Type' to
be always present for multipart headers, while it wasn't used at all.
-- Proxmox Support Team <support@proxmox.com> Mon, 06 Mar 2023 13:39:57 +0100
libpve-http-server-perl (4.1-5) bullseye; urgency=medium
* upload: re-allow having white-space in filenames
-- Proxmox Support Team <support@proxmox.com> Mon, 07 Nov 2022 16:43:31 +0100
libpve-http-server-perl (4.1-4) bullseye; urgency=medium
* acknowledge content-disposition header
* request: add missing early return to future proof error check
-- Proxmox Support Team <support@proxmox.com> Thu, 29 Sep 2022 14:37:05 +0200
libpve-http-server-perl (4.1-3) bullseye; urgency=medium
* response: forbid linefeeds in response status message
* proxy request: assert that API url starts with a slash
* pass through streaming: only allow from privileged local pvedaemon as
safety net
* requests: assert that there is no @ in the URLs authority
-- Proxmox Support Team <support@proxmox.com> Sat, 02 Jul 2022 09:16:21 +0200
libpve-http-server-perl (4.1-2) bullseye; urgency=medium
* tls: log failure to apply TLS 1.3 ciphers
* html formatter: encode href attributes for API debug viewer
-- Proxmox Support Team <support@proxmox.com> Tue, 17 May 2022 16:40:12 +0200
libpve-http-server-perl (4.1-1) bullseye; urgency=medium
* web socket: guard disconnect block check properly
* avoid warning if request params does not exist
* fix #3807: don't attempt response on closed handle
* fix #3790: allow setting TLS 1.3 cipher suites
* fix #3745: allow overriding TLS key location
* fix #3789: allow disabling TLS v1.2/v1.3
-- Proxmox Support Team <support@proxmox.com> Thu, 13 Jan 2022 13:32:43 +0100
libpve-http-server-perl (4.0-4) bullseye; urgency=medium
* webproxy: handle unflushed write buffer
* fix #3724: disable TLS renegotiation
* download-stream: allow the api call to set the content-encoding
-- Proxmox Support Team <support@proxmox.com> Wed, 24 Nov 2021 18:14:53 +0100
libpve-http-server-perl (4.0-3) bullseye; urgency=medium
* anyevent: move unlink from http-server to endpoint
-- Proxmox Support Team <support@proxmox.com> Mon, 04 Oct 2021 10:18:12 +0200
libpve-http-server-perl (4.0-2) pve pmg; urgency=medium
* AnyEvent/websocket_proxy: remove 'base64' handling
* AnyEvent/websocket_proxy: drop handling of websocket subprotocols
-- Proxmox Support Team <support@proxmox.com> Tue, 18 May 2021 10:19:00 +0200
libpve-http-server-perl (4.0-1) bullseye; urgency=medium
* rebuild for Debian 11 Bullseye based releases
-- Proxmox Support Team <support@proxmox.com> Fri, 14 May 2021 16:37:34 +0200
libpve-http-server-perl (3.2-2) pve pmg; urgency=medium
* access control: correctly match v4-mapped-v6 addresses
* access control: also match any IPv6 in 'ALL'
-- Proxmox Support Team <support@proxmox.com> Fri, 07 May 2021 17:49:34 +0200
libpve-http-server-perl (3.2-1) pve pmg; urgency=medium
* allow 'download' to be passed from API handler
* utils: add LISTEN_IP option in proxy configuration
* support streaming data form a file handle to a client
* allow stream download from path and over short-cutted pvedaemon-proxy
-- Proxmox Support Team <support@proxmox.com> Fri, 23 Apr 2021 13:54:04 +0200
libpve-http-server-perl (3.1-1) pve pmg; urgency=medium
* accept connection phase: fix connection count leak
* accept connection phase: immediately close socket on early error
-- Proxmox Support Team <support@proxmox.com> Fri, 11 Dec 2020 08:39:36 +0100
libpve-http-server-perl (3.0-6) pve pmg; urgency=medium
* fix #2766: allow application/json as content-type for post/put requests
* increase maximal accepted header count to 64. Modern browsers and proxy
combinations can exceed the old limit of 30. The maximal accumulated total
header size of 8 KiB stays untouched.
-- Proxmox Support Team <support@proxmox.com> Thu, 02 Jul 2020 09:42:39 +0200
libpve-http-server-perl (3.0-5) pve pmg; urgency=medium
* partially fix #2618: use new unified spice port range helper from
pve-common, increases maximum proxy port for spice to 61999
* Websocket: implement ping/pong from RFC
* Websocket: performance improvements
-- Proxmox Support Team <support@proxmox.com> Mon, 09 Mar 2020 16:12:45 +0100
libpve-http-server-perl (3.0-4) pve pmg; urgency=medium
* allow ticket in 'Authorization' header as fallback
* api-server: extract, set and handle API token header
-- Proxmox Support Team <support@proxmox.com> Wed, 29 Jan 2020 09:32:04 +0100
libpve-http-server-perl (3.0-3) pve pmg; urgency=medium
* send_file_start: allow to pass a open fh and content-type
-- Proxmox Support Team <support@proxmox.com> Fri, 11 Oct 2019 11:25:12 +0200
libpve-http-server-perl (3.0-2) pve pmg; urgency=medium
* decode_urlencoded: cope with undefined values
* anyevent: rpcenv is optional and from our child instance
-- Proxmox Support Team <support@proxmox.com> Thu, 11 Jul 2019 19:30:23 +0200
libpve-http-server-perl (3.0-1) pve pmg; urgency=medium
* rebuild for Debian Buster / PVE 6.0
* update jQuery to 3.4.1
* update Bootstrap to 3.4.1
-- Proxmox Support Team <support@proxmox.com> Tue, 21 May 2019 21:35:00 +0200
libpve-http-server-perl (2.0-13) unstable; urgency=medium
* tls: make dh to openssl 1.1 compatible
* store Host header in rpc environment
* forward Host header in proxy_request
-- Proxmox Support Team <support@proxmox.com> Wed, 03 Apr 2019 13:55:44 +0200
libpve-http-server-perl (2.0-12) unstable; urgency=medium
* Allow one to specify 'honor_cipher_order' and 'compression' parameters
* move read_proxy_conf from PVE::API2Tools to new PVE::ApiServer::Utils module
-- Proxmox Support Team <support@proxmox.com> Tue, 26 Feb 2019 07:07:31 +0100
libpve-http-server-perl (2.0-11) unstable; urgency=medium
* fix #1935: spice proxy: read empty line after 200 OK
-- Proxmox Support Team <support@proxmox.com> Fri, 28 Sep 2018 10:41:22 +0200
libpve-http-server-perl (2.0-10) unstable; urgency=medium
* fix #1869: send correct http response in spice proxy
* websocket: set $max_payload_size = 128*1024; (131072)
-- Proxmox Support Team <support@proxmox.com> Fri, 17 Aug 2018 08:29:53 +0200
libpve-http-server-perl (2.0-9) unstable; urgency=medium
* Fix #1684 WebSocket proxy behind a buffered proxy
-- Proxmox Support Team <support@proxmox.com> Mon, 28 May 2018 10:33:41 +0200
libpve-http-server-perl (2.0-8) unstable; urgency=medium
* auth_handler: handle exceptions correctly instead of always returning 401
* add 'map' filetype to http-server
* do not send websocket status code to port
-- Proxmox Support Team <support@proxmox.com> Mon, 11 Dec 2017 15:35:34 +0100
libpve-http-server-perl (2.0-7) unstable; urgency=medium
* add content type application/x-compressed-tar
* allow API calls to download file contents
* build: reformat debian/control
-- Proxmox Support Team <support@proxmox.com> Tue, 14 Nov 2017 08:05:17 +0100
libpve-http-server-perl (2.0-6) unstable; urgency=medium
* pass $format to rest_handler()
-- Proxmox Support Team <support@proxmox.com> Thu, 10 Aug 2017 12:05:42 +0200
libpve-http-server-perl (2.0-5) unstable; urgency=medium
* add json/mp3/oga/svg MIME types for the new novnc
-- Proxmox Support Team <support@proxmox.com> Fri, 02 Jun 2017 12:49:02 +0200
libpve-http-server-perl (2.0-4) unstable; urgency=medium
* assume all parameters are utf8 encoded
-- Proxmox Support Team <support@proxmox.com> Tue, 02 May 2017 11:55:21 +0200
libpve-http-server-perl (2.0-3) unstable; urgency=medium
* avoid locale specific time stamps
-- Proxmox Support Team <support@proxmox.com> Mon, 24 Apr 2017 07:43:29 +0200
libpve-http-server-perl (2.0-2) unstable; urgency=medium
* fix #1332: allow ECDHE with all supported curves
-- Proxmox Support Team <support@proxmox.com> Mon, 03 Apr 2017 15:11:38 +0200
libpve-http-server-perl (2.0-1) unstable; urgency=medium
* bump version for debian stretch
-- Proxmox Support Team <support@proxmox.com> Fri, 10 Mar 2017 08:50:55 +0100
libpve-http-server-perl (1.0-4) unstable; urgency=medium
* add debian triggers file
-- Proxmox Support Team <support@proxmox.com> Sat, 21 Jan 2017 16:36:47 +0100
libpve-http-server-perl (1.0-3) unstable; urgency=medium
* console-demo.pl: add a more complex demo
* call Net::SSLeay::ERR_clear_error after all handlers
* avoid warnings when clients disconnects early
-- Proxmox Support Team <support@proxmox.com> Sat, 21 Jan 2017 16:19:20 +0100
libpve-http-server-perl (1.0-2) unstable; urgency=medium
* simple-demo.pl: simple demo server for testing
* extract_auth_cookie: always call uri_unescape($ticket)
* use canonical flag for json format
* remove base_handler_class from required arguments
* remove all references to rpcenv
* include jquery and bootstrap
* new helper add_dirs
* add new hook function to generate CSRF token
* add generic formatter framework
-- Proxmox Support Team <support@proxmox.com> Mon, 16 Jan 2017 18:39:21 +0100
libpve-http-server-perl (1.0-1) unstable; urgency=medium
* first try
-- Proxmox Support Team <support@proxmox.com> Fri, 13 Jan 2017 12:47:07 +0100

31
debian/control vendored Normal file
View File

@ -0,0 +1,31 @@
Source: libpve-http-server-perl
Section: perl
Priority: optional
Maintainer: Proxmox Support Team <support@proxmox.com>
Build-Depends: debhelper-compat (= 13), perl,
Standards-Version: 4.6.2
Homepage: https://www.proxmox.com
Package: libpve-http-server-perl
Architecture: all
Depends: libanyevent-http-perl,
libanyevent-perl (>= 7.140-3),
libcrypt-ssleay-perl,
libhtml-parser-perl,
libhttp-date-perl,
libhttp-message-perl,
libio-socket-ssl-perl,
libjs-bootstrap,
libjs-jquery,
libjson-perl,
libnet-ip-perl,
libpve-common-perl (>= 8.0.2),
liburi-perl,
${misc:Depends},
${perl:Depends},
Breaks: libpve-storage-perl (<< 8.2.5),
pmg-api (<< 8.1.4),
pve-manager (<< 8.2.7),
Description: Proxmox Asynchrounous HTTP Server Implementation
This package is used as base to implement the REST API in all perl based
Proxmox projects.

16
debian/copyright vendored Normal file
View File

@ -0,0 +1,16 @@
Copyright (C) 2010-2021 Proxmox Server Solutions GmbH
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

1
debian/docs vendored Normal file
View File

@ -0,0 +1 @@
debian/SOURCE

9
debian/rules vendored Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/make -f
# See debhelper(7) (uncomment to enable)
# output every command that modifies files on the build system.
#DH_VERBOSE = 1
%:
dh $@

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (native)

1
debian/triggers vendored Normal file
View File

@ -0,0 +1 @@
activate-noawait pve-api-updates

95
pve-http-server.spec Normal file
View File

@ -0,0 +1,95 @@
%define _unpackaged_files_terminate_build 1
Name: pve-http-server
Summary: Proxmox Asynchrounous HTTP Server Implementation
Version: 5.1.2
Release: alt2
License: AGPL-3.0+
Group: Development/Perl
Url: https://www.proxmox.com
Vcs: git://git.proxmox.com/git/pve-http-server.git
Source: %name-%version.tar
ExclusiveArch: x86_64 aarch64
Provides: perl-%name = %EVR
# from debian/control
Provides: libpve-http-server-perl = %EVR
Conflicts: pve-storage < 8.2.5
Conflicts: pmg-api < 8.1.4
Conflicts: pve-manager < 8.2.7
Requires: fonts-font-awesome javascript-jquery javascript-bootstrap
BuildRequires: perl(AnyEvent/HTTP.pm) perl(AnyEvent/Handle.pm) perl(AnyEvent/IO.pm) perl(AnyEvent/Socket.pm) perl(AnyEvent/TLS.pm) perl(AnyEvent/Util.pm)
BuildRequires: perl(Compress/Zlib.pm)
BuildRequires: perl(Digest/MD5.pm) perl(Digest/SHA.pm) perl(Encode.pm)
BuildRequires: perl(Net/SSLeay.pm)
BuildRequires: perl(Time/HiRes.pm)
BuildRequires: perl(HTTP/Date.pm) perl(HTTP/Headers.pm) perl(HTTP/Request.pm) perl(HTTP/Response.pm) perl(HTTP/Status.pm) perl(HTML/Entities.pm)
BuildRequires: perl(JSON.pm)
BuildRequires: perl(Net/IP.pm)
BuildRequires: perl(URI/Escape.pm) perl(URI.pm)
BuildRequires: perl(PVE/INotify.pm) perl(PVE/SafeSyslog.pm) perl(PVE/Tools.pm) perl(PVE/JSONSchema.pm)
%description
%summary.
This package is used as base to implement the REST API in all perl based
%prep
%setup
%install
%makeinstall_std -C src
%files
%doc debian/copyright
%perl_vendor_privlib/PVE/*
%changelog
* Thu Dec 05 2024 Alexey Shabalin <shaba@altlinux.org> 5.1.2-alt2
- Revert "fix UTF-8 presentation"
* Thu Nov 28 2024 Alexey Shabalin <shaba@altlinux.org> 5.1.2-alt1
- unbootstrap
- fix UTF-8 presentation
* Sun Oct 20 2024 Alexey Shabalin <shaba@altlinux.org> 5.1.2-alt0.1
- 5.1.2
- bootstrap, build without conflicts
* Thu Aug 29 2024 Andrew A. Vasilyev <andy@altlinux.org> 5.1.0-alt1
- 5.1.0
* Fri Mar 29 2024 Andrew A. Vasilyev <andy@altlinux.org> 5.0.6-alt1
- 5.0.6
* Wed Feb 28 2024 Andrew A. Vasilyev <andy@altlinux.org> 5.0.5-alt1
- 5.0.5
* Thu May 25 2023 Andrew A. Vasilyev <andy@altlinux.org> 4.2.3-alt1
- 4.2-3
- add copyright file
* Mon Mar 20 2023 Andrew A. Vasilyev <andy@altlinux.org> 4.2.1-alt1
- 4.2-1
* Sat Mar 11 2023 Andrew A. Vasilyev <andy@altlinux.org> 4.1.6-alt1
- 4.1-6
* Mon Nov 14 2022 Alexey Shabalin <shaba@altlinux.org> 4.1.5-alt1
- 4.1-5
* Fri Oct 07 2022 Andrew A. Vasilyev <andy@altlinux.org> 4.1.4-alt2
- fix CPU eating loop
* Mon Oct 03 2022 Alexey Shabalin <shaba@altlinux.org> 4.1.4-alt1
- 4.1-4
* Thu Jul 07 2022 Andrew A. Vasilyev <andy@altlinux.org> 4.1.3-alt1
- 4.1-3
* Thu Feb 17 2022 Alexey Shabalin <shaba@altlinux.org> 4.1.1-alt1
- 4.1-1
- build as separate package

23
src/Makefile Normal file
View File

@ -0,0 +1,23 @@
DESTDIR=
PERL5DIR=${DESTDIR}/usr/share/perl5
DOCDIR=${DESTDIR}/usr/share/doc/${PACKAGE}
all:
install: PVE
install -d -m 755 ${PERL5DIR}/PVE/APIServer
install -m 0644 PVE/APIServer/AnyEvent.pm ${PERL5DIR}/PVE/APIServer
install -m 0644 PVE/APIServer/Formatter.pm ${PERL5DIR}/PVE/APIServer
install -m 0644 PVE/APIServer/Utils.pm ${PERL5DIR}/PVE/APIServer
install -d -m 755 ${PERL5DIR}/PVE/APIServer/Formatter
install -m 0644 PVE/APIServer/Formatter/Standard.pm ${PERL5DIR}/PVE/APIServer/Formatter
install -m 0644 PVE/APIServer/Formatter/Bootstrap.pm ${PERL5DIR}/PVE/APIServer/Formatter
install -m 0644 PVE/APIServer/Formatter/HTML.pm ${PERL5DIR}/PVE/APIServer/Formatter
.PHONY: clean distclean
distclean: clean
rm -f examples/simple-demo.pem
clean:
rm -rf examples/simple-demo.lck
find . -name '*~' -exec rm {} ';'

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
package PVE::APIServer::Formatter;
use strict;
use warnings;
use URI::Escape;
# generic formatter support
# PVE::APIServer::Formatter::* classes should register themselves here
my $formatter_hash = {};
my $page_formatter_hash = {};
sub register_formatter {
my ($format, $code) = @_;
die "formatter '$format' already defined"
if defined($formatter_hash->{$format});
$formatter_hash->{$format} = $code;
}
sub register_page_formatter {
my (%config) = @_;
my $format = $config{format} ||
die "missing format";
my $path = $config{path} ||
die "missing path";
my $method = $config{method} ||
die "missing method";
my $code = $config{code} ||
die "missing formatter code";
die "duplicate page formatter for '$method: $path'"
if defined($page_formatter_hash->{$format}->{$method}->{$path});
$page_formatter_hash->{$format}->{$method}->{$path} = $code;
}
sub get_formatter {
my ($format, $method, $path) = @_;
return undef if !defined($format);
if (defined($method) && defined($path)) {
my $code = $page_formatter_hash->{$format}->{$method}->{$path};
return $code if defined($code);
}
return $formatter_hash->{$format};
}
my $login_formatter_hash = {};
sub register_login_formatter {
my ($format, $code) = @_;
die "login formatter '$format' already defined"
if defined($login_formatter_hash->{$format});
$login_formatter_hash->{$format} = $code;
}
sub get_login_formatter {
my ($format) = @_;
return undef if !defined($format);
return $login_formatter_hash->{$format};
}
# some helper functions
sub extract_auth_value {
my ($header, $key) = @_;
return undef if !$header;
my $value = ($header =~ /(?:^|\s)\Q$key\E(?:=| )([^;]*)/)[0];
$value = uri_unescape($value) if $value;
return $value;
}
sub create_auth_cookie {
my ($ticket, $cookie_name) = @_;
my $encticket = uri_escape($ticket);
return "${cookie_name}=$encticket; path=/; secure; SameSite=Lax;";
}
sub create_auth_header {
my ($value, $key) = @_;
return undef if !$key;
my $encoded = uri_escape($value);
return "${key} ${encoded}";
}
1;

View File

@ -0,0 +1,237 @@
package PVE::APIServer::Formatter::Bootstrap;
use strict;
use warnings;
use HTML::Entities;
use JSON;
use URI::Escape;
# FIXME: remove console code??
# Helpers to generate simple html pages using Bootstrap markup.
my $jssrc = <<_EOJS;
PVE.open_vm_console = function(node, vmid) {
console.log("open vm " + vmid + " on node " + node);
var downloadWithName = function(uri, name) {
var link = jQuery('#pve_console_anchor');
link.attr("href", uri);
// Note: we need to tell android the correct file name extension
// but we do not set 'download' tag for other environments, because
// It can have strange side effects (additional user prompt on firefox)
var andriod = navigator.userAgent.match(/Android/i) ? true : false;
if (andriod) {
link.attr("download", name);
}
if (document.createEvent) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
link.get(0).dispatchEvent(evt);
} else {
link.get(0).fireEvent('onclick');
}
};
jQuery.ajax("/api2/json/console", {
data: { vmid: vmid, node: node },
headers: { CSRFPreventionToken: PVE.CSRFPreventionToken },
dataType: 'json',
type: 'POST',
error: function(jqXHR, textStatus, errorThrown) {
// fixme: howto view JS errors ?
console.log("ERROR " + textStatus + ": " + errorThrown);
},
success: function(data) {
var raw = "[virt-viewer]\\n";
jQuery.each(data.data, function(k, v) {
raw += k + "=" + v + "\\n";
});
var url = 'data:application/x-virt-viewer;charset=UTF-8,' +
encodeURIComponent(raw);
downloadWithName(url, "pve-spice.vv");
}
});
};
_EOJS
sub new {
my ($class, $res, $url, $auth, $config) = @_;
my $self = bless {
url => $url,
title => $config->{title},
cookie_name => $config->{cookie_name},
apitoken_name => $config->{apitoken_name},
js => '',
}, $class;
if (my $username = $auth->{userid}) {
$self->{csrftoken} = $config->{csrfgen_func}->($username);
}
return $self;
}
sub body {
my ($self, $html) = @_;
my $jssetup = "PVE = {};\n\n"; # create namespace
if ($self->{csrftoken}) {
$jssetup .= "PVE.CSRFPreventionToken = '$self->{csrftoken}';\n";
}
$jssetup .= "PVE.delete_auth_cookie = function() {\n";
if ($self->{cookie_name}) {
$jssetup .= " document.cookie = \"$self->{cookie_name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; secure; SameSite=Lax;\";\n";
};
$jssetup .= "};\n";
return <<_EOD;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>$self->{title}</title>
<!-- Bootstrap -->
<link href="/js/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script type="text/javascript">
$jssetup
$jssrc
</script>
<style>
body {
padding-top: 70px;
}
</style>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="/js/jquery/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="/js/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<a class="hidden" id="pve_console_anchor"></a>
$html
<script type="text/javascript">
$self->{js}
</script>
</body>
</html>
_EOD
}
my $comp_id_counter = 0;
sub el {
my ($self, %param) = @_;
$param{tag} = 'div' if !$param{tag};
my $id;
my $html = "<$param{tag}";
if (wantarray) {
$comp_id_counter++;
$id = "pveid$comp_id_counter";
$html .= " id=$id";
}
my $skip = {
tag => 1,
cn => 1,
html => 1,
text => 1,
};
my $boolattr = {
required => 1,
autofocus => 1,
};
my $noescape = {
placeholder => 1,
};
foreach my $attr (keys %param) {
next if $skip->{$attr};
my $v = $noescape->{$attr} ? $param{$attr} : uri_escape_utf8($param{$attr}, "^\/\ A-Za-z0-9\-\._~");
next if !defined($v);
if ($boolattr->{$attr}) {
$html .= " $attr" if $v;
} else {
$html .= " $attr=\"$v\"";
}
}
$html .= ">";
if (my $cn = $param{cn}) {
if(ref($cn) eq 'ARRAY'){
foreach my $rec (@$cn) {
$html .= $self->el(%$rec);
}
} else {
$html .= $self->el(%$cn);
}
} elsif ($param{html}) {
$html .= $param{html};
} elsif ($param{text}) {
$html .= encode_entities($param{text});
}
$html .= "</$param{tag}>";
return wantarray ? ($html, $id) : $html;
}
sub alert {
my ($self, %param) = @_;
return $self->el(class => "alert alert-danger", %param);
}
sub add_js {
my ($self, $js) = @_;
$self->{js} .= $js . "\n";
}
my $format_event_callback = sub {
my ($info) = @_;
my $pstr = encode_json($info->{param});
return "function(e){$info->{fn}.apply(e, $pstr);}";
};
sub button {
my ($self, %param) = @_;
$param{tag} = 'button';
$param{class} = "btn btn-default btn-xs";
if (my $click = delete $param{click}) {
my ($html, $id) = $self->el(%param);
my $cb = &$format_event_callback($click);
$self->add_js("jQuery('#$id').on('click', $cb);");
return $html;
} else {
return $self->el(%param);
}
}
1;

View File

@ -0,0 +1,296 @@
package PVE::APIServer::Formatter::HTML;
use strict;
use warnings;
use PVE::APIServer::Formatter;
use HTTP::Status;
use JSON;
use HTML::Entities;
use PVE::JSONSchema;
use PVE::APIServer::Formatter::Bootstrap;
use PVE::APIServer::Formatter::Standard;
my $portal_format = 'html';
my $portal_ct = 'text/html;charset=UTF-8';
my $get_portal_base_url = sub {
my ($config) = @_;
return "$config->{base_uri}/$portal_format";
};
my $get_portal_login_url = sub {
my ($config) = @_;
return "$config->{base_uri}/$portal_format/access/ticket";
};
sub render_page {
my ($doc, $html, $config) = @_;
my $items = [];
push @$items, {
tag => 'li',
cn => {
tag => 'a',
href => $get_portal_login_url->($config),
onClick => "PVE.delete_auth_cookie();",
text => "Logout",
}};
my $base_url = $get_portal_base_url->($config);
my $nav = $doc->el(
class => "navbar navbar-inverse navbar-fixed-top",
role => "navigation", cn => {
class => "container", cn => [
{
class => "navbar-header", cn => [
{
tag => 'button',
type => 'button',
class => "navbar-toggle",
'data-toggle' => "collapse",
'data-target' => ".navbar-collapse",
cn => [
{ tag => 'span', class => 'sr-only', text => "Toggle navigation" },
{ tag => 'span', class => 'icon-bar' },
{ tag => 'span', class => 'icon-bar' },
{ tag => 'span', class => 'icon-bar' },
],
},
{
tag => 'a',
class => "navbar-brand",
href => $base_url,
text => $config->{title},
},
],
},
{
class => "collapse navbar-collapse",
cn => {
tag => 'ul',
class => "nav navbar-nav",
cn => $items,
},
},
],
});
$items = [];
my @pcomp = split('/', $doc->{url});
shift @pcomp; # empty
shift @pcomp; # api2
shift @pcomp; # $format
my $href = $base_url;
push @$items, { tag => 'li', cn => {
tag => 'a',
href => $href,
text => 'Home'}};
foreach my $comp (@pcomp) {
$href .= "/".encode_entities($comp);
push @$items, { tag => 'li', cn => {
tag => 'a',
href => $href,
text => $comp}};
}
my $breadcrumbs = $doc->el(tag => 'ol', class => 'breadcrumb container', cn => $items);
return $doc->body($nav . $breadcrumbs . $html);
}
my $login_form = sub {
my ($config, $doc, $param, $errmsg) = @_;
$param = {} if !$param;
my $username = $param->{username} || '';
my $password = $param->{password} || '';
my $items = [
{
tag => 'label',
text => "Please sign in",
},
{
tag => 'input',
type => 'text',
class => 'form-control',
name => 'username',
value => $username,
placeholder => "Enter user name",
required => 1,
autofocus => 1,
},
{
tag => 'input',
type => 'password',
class => 'form-control',
name => 'password',
value => $password,
placeholder => 'Password',
required => 1,
},
];
my $html = '';
$html .= $doc->alert(text => $errmsg) if ($errmsg);
$html .= $doc->el(
class => 'container',
cn => {
tag => 'form',
role => 'form',
method => 'POST',
action => $get_portal_login_url->($config),
cn => [
{
class => 'form-group',
cn => $items,
},
{
tag => 'button',
type => 'submit',
class => 'btn btn-lg btn-primary btn-block',
text => "Sign in",
},
],
});
return $html;
};
PVE::APIServer::Formatter::register_login_formatter($portal_format, sub {
my ($path, $auth, $config) = @_;
my $headers = HTTP::Headers->new(Location => $get_portal_login_url->($config));
return HTTP::Response->new(301, "Moved", $headers);
});
PVE::APIServer::Formatter::register_formatter($portal_format, sub {
my ($res, $data, $param, $path, $auth, $config) = @_;
# fixme: clumsy!
PVE::APIServer::Formatter::Standard::prepare_response_data($portal_format, $res);
$data = $res->{data};
my $html = '';
my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
if (!HTTP::Status::is_success($res->{status})) {
$html .= $doc->alert(text => "Error $res->{status}: $res->{message}");
}
my $lnk;
if (my $info = $res->{info}) {
$html .= $doc->el(tag => 'h3', text => 'Description');
$html .= $doc->el(tag => 'p', text => $info->{description});
$lnk = PVE::JSONSchema::method_get_child_link($info);
}
if ($lnk && $data && $data->{data} && HTTP::Status::is_success($res->{status})) {
my $href = $lnk->{href};
if ($href =~ m/^\{(\S+)\}$/) {
my $items = [];
my $prop = $1;
$path =~ s/\/+$//; # remove trailing slash
foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data}}) {
next if !ref($elem);
if (defined(my $value = $elem->{$prop})) {
my $tv = to_json($elem, {pretty => 1, allow_nonref => 1, canonical => 1});
push @$items, {
tag => 'a',
class => 'list-group-item',
href => "$path/".encode_entities($value),
cn => [
{
tag => 'h4',
class => 'list-group-item-heading',
text => $value,
},
{
tag => 'pre',
class => 'list-group-item',
text => $tv,
},
],
};
}
}
$html .= $doc->el(class => 'list-group', cn => $items);
} else {
my $json = to_json($data, {allow_nonref => 1, pretty => 1, canonical => 1});
$html .= $doc->el(tag => 'pre', text => $json);
}
} else {
my $json = to_json($data, {allow_nonref => 1, pretty => 1, canonical => 1});
$html .= $doc->el(tag => 'pre', text => $json);
}
$html = $doc->el(class => 'container', html => $html);
my $raw = render_page($doc, $html, $config);
return ($raw, $portal_ct);
});
PVE::APIServer::Formatter::register_page_formatter(
'format' => $portal_format,
method => 'GET',
path => "/access/ticket",
code => sub {
my ($res, $data, $param, $path, $auth, $config) = @_;
my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
my $html = $login_form->($config, $doc);
my $raw = render_page($doc, $html, $config);
return ($raw, $portal_ct);
});
PVE::APIServer::Formatter::register_page_formatter(
'format' => $portal_format,
method => 'POST',
path => "/access/ticket",
code => sub {
my ($res, $data, $param, $path, $auth, $config) = @_;
if (HTTP::Status::is_success($res->{status})) {
my $cookie = PVE::APIServer::Formatter::create_auth_cookie(
$data->{ticket}, $config->{cookie_name});
my $headers = HTTP::Headers->new(Location => $get_portal_base_url->($config),
'Set-Cookie' => $cookie);
return HTTP::Response->new(301, "Moved", $headers);
}
# Note: HTTP server redirects to 'GET /access/ticket', so below
# output is not really visible.
my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
my $html = $login_form->($config, $doc);
my $raw = render_page($doc, $html, $config);
return ($raw, $portal_ct);
});
1;

View File

@ -0,0 +1,141 @@
package PVE::APIServer::Formatter::Standard;
use strict;
use warnings;
use PVE::APIServer::Formatter;
use HTTP::Status;
use JSON;
use HTML::Entities;
use PVE::JSONSchema;
# register result formatters
sub prepare_response_data {
my ($format, $res) = @_;
my $success = 1;
my $new = {
data => $res->{data},
};
if (scalar(keys %{$res->{errors}})) {
$success = 0;
$new->{errors} = $res->{errors};
}
if ($format eq 'extjs' || $format eq 'htmljs') {
# HACK: extjs wants 'success' property instead of useful HTTP status codes
if (HTTP::Status::is_error($res->{status})) {
$success = 0;
$new->{message} = $res->{message} || status_message($res->{status});
$new->{status} = $res->{status} || 200;
$res->{message} = undef;
$res->{status} = 200;
}
$new->{success} = $success;
}
if ($success && $res->{total}) {
$new->{total} = $res->{total};
}
if ($success && $res->{changes}) {
$new->{changes} = $res->{changes};
}
$res->{data} = $new;
}
PVE::APIServer::Formatter::register_formatter('json', sub {
my ($res, $data, $param, $path, $auth, $config) = @_;
my $nocomp = 0;
my $ct = 'application/json;charset=UTF-8';
prepare_response_data('json', $res);
my $raw = to_json($res->{data}, {utf8 => 1, allow_nonref => 1});
return ($raw, $ct, $nocomp);
});
PVE::APIServer::Formatter::register_formatter('extjs', sub {
my ($res, $data, $param, $path, $auth, $config) = @_;
my $nocomp = 0;
my $ct = 'application/json;charset=UTF-8';
prepare_response_data('extjs', $res);
my $raw = to_json($res->{data}, {utf8 => 1, allow_nonref => 1});
return ($raw, $ct, $nocomp);
});
PVE::APIServer::Formatter::register_formatter('htmljs', sub {
my ($res, $data, $param, $path, $auth, $config) = @_;
my $nocomp = 0;
# we use this for extjs file upload forms
my $ct = 'text/html;charset=UTF-8';
prepare_response_data('htmljs', $res);
my $raw = encode_entities(to_json($res->{data}, {allow_nonref => 1}));
return ($raw, $ct, $nocomp);
});
PVE::APIServer::Formatter::register_formatter('spiceconfig', sub {
my ($res, $data, $param, $path, $auth, $config) = @_;
my $nocomp = 0;
my $ct = 'application/x-virt-viewer;charset=UTF-8';
prepare_response_data('spiceconfig', $res);
$data = $res->{data};
my $raw;
if ($data && ref($data) && ref($data->{data})) {
$raw = "[virt-viewer]\n";
while (my ($key, $value) = each %{$data->{data}}) {
$raw .= "$key=$value\n" if defined($value);
}
}
return ($raw, $ct, $nocomp);
});
PVE::APIServer::Formatter::register_formatter('png', sub {
my ($res, $data, $param, $path, $auth, $config) = @_;
my $nocomp = 1;
my $ct = 'image/png';
prepare_response_data('png', $res);
$data = $res->{data};
# fixme: better to revove that whole png thing ?
my $filename;
my $raw = '';
if ($data && ref($data) && ref($data->{data}) &&
$data->{data}->{filename} && defined($data->{data}->{image})) {
$filename = $data->{data}->{filename};
$raw = $data->{data}->{image};
}
return ($raw, $ct, $nocomp);
});

View File

@ -0,0 +1,90 @@
package PVE::APIServer::Utils;
use strict;
use warnings;
use Net::IP;
# all settings are used for pveproxy and pmgproxy
# the ALLOW/DENY/POLICY is also used by spiceproxy
sub read_proxy_config {
my ($proxy_name) = @_;
my $conffile = "/etc/default/$proxy_name";
# Note: evaluate with bash
my $shcmd = ". $conffile;\n";
$shcmd .= 'echo \"LISTEN_IP:\$LISTEN_IP\";';
$shcmd .= 'echo \"ALLOW_FROM:\$ALLOW_FROM\";';
$shcmd .= 'echo \"DENY_FROM:\$DENY_FROM\";';
$shcmd .= 'echo \"POLICY:\$POLICY\";';
$shcmd .= 'echo \"CIPHERS:\$CIPHERS\";';
$shcmd .= 'echo \"CIPHERSUITES:\$CIPHERSUITES\";';
$shcmd .= 'echo \"DHPARAMS:\$DHPARAMS\";';
$shcmd .= 'echo \"TLS_KEY_FILE:\$TLS_KEY_FILE\";';
$shcmd .= 'echo \"HONOR_CIPHER_ORDER:\$HONOR_CIPHER_ORDER\";';
$shcmd .= 'echo \"COMPRESSION:\$COMPRESSION\";';
$shcmd .= 'echo \"DISABLE_TLS_1_2:\$DISABLE_TLS_1_2\";';
$shcmd .= 'echo \"DISABLE_TLS_1_3:\$DISABLE_TLS_1_3\";';
my $data = -f $conffile ? `bash -c "$shcmd"` : '';
my $res = {};
my $boolean_options = [
'HONOR_CIPHER_ORDER',
'COMPRESSION',
'DISABLE_TLS_1_2',
'DISABLE_TLS_1_3',
];
while ($data =~ s/^(.*)\n//) {
my ($key, $value) = split(/:/, $1, 2);
next if !defined($value) || $value eq '';
if ($key eq 'ALLOW_FROM' || $key eq 'DENY_FROM') {
my $ips = [];
foreach my $ip (split(/,/, $value)) {
if ($ip eq 'all') {
push @$ips, Net::IP->new('0/0') || die Net::IP::Error() . "\n";
push @$ips, Net::IP->new('::/0') || die Net::IP::Error() . "\n";
next;
}
push @$ips, Net::IP->new(normalize_v4_in_v6($ip)) || die Net::IP::Error() . "\n";
}
$res->{$key} = $ips;
} elsif ($key eq 'LISTEN_IP') {
$res->{$key} = $value;
} elsif ($key eq 'POLICY') {
die "unknown policy '$value'\n" if $value !~ m/^(allow|deny)$/;
$res->{$key} = $value;
} elsif ($key eq 'CIPHERS') {
$res->{$key} = $value;
} elsif ($key eq 'CIPHERSUITES') {
$res->{$key} = $value;
} elsif ($key eq 'DHPARAMS') {
$res->{$key} = $value;
} elsif ($key eq 'TLS_KEY_FILE') {
$res->{$key} = $value;
} elsif (grep { $key eq $_ } @$boolean_options) {
die "unknown value '$value' - use 0 or 1\n" if $value !~ m/^(0|1)$/;
$res->{$key} = $value;
} else {
# silently skip everythin else?
}
}
return $res;
}
sub normalize_v4_in_v6 {
my ($ip_text) = @_;
my $ip = Net::IP->new($ip_text) || die Net::IP::Error() . "\n";
my $v4_mapped_v6_prefix = Net::IP->new('::ffff:0:0/96');
if ($v4_mapped_v6_prefix->overlaps($ip)) {
return Net::IP::ip_get_embedded_ipv4($ip_text);
}
return $ip_text;
}
1;

552
src/examples/console-demo.pl Executable file
View File

@ -0,0 +1,552 @@
#!/usr/bin/perl
# This demo requires some other packages: novnc-pve and
# pve-manager (for PVE::NoVncIndex)
# First, we need some helpers to create authentication Tickets
package Ticket;
use strict;
use warnings;
use Net::SSLeay;
use PVE::Ticket;
use Crypt::OpenSSL::RSA;
my $min_ticket_lifetime = -60*5; # allow 5 minutes time drift
my $max_ticket_lifetime = 60*60*2; # 2 hours
my $rsa = Crypt::OpenSSL::RSA->generate_key(2048);
sub create_ticket {
my ($username) = @_;
return PVE::Ticket::assemble_rsa_ticket($rsa, 'DEMO', $username);
}
sub verify_ticket {
my ($ticket, $noerr) = @_;
return PVE::Ticket::verify_rsa_ticket(
$rsa, 'DEMO', $ticket, undef,
$min_ticket_lifetime, $max_ticket_lifetime, $noerr);
}
# VNC tickets
# - they do not contain the username in plain text
# - they are restricted to a specific resource path (example: '/vms/100')
sub assemble_vnc_ticket {
my ($username, $path) = @_;
my $secret_data = "$username:$path";
return PVE::Ticket::assemble_rsa_ticket(
$rsa, 'DEMOVNC', undef, $secret_data);
}
sub verify_vnc_ticket {
my ($ticket, $username, $path, $noerr) = @_;
my $secret_data = "$username:$path";
return PVE::Ticket::verify_rsa_ticket(
$rsa, 'DEMOVNC', $ticket, $secret_data, -20, 40, $noerr);
}
# We stack several PVE::RESTHandler classes to create
# the API for the novnc-pve console.
package NodeInfoAPI;
use strict;
use warnings;
use PVE::RESTHandler;
use PVE::JSONSchema qw(get_standard_option);
use PVE::RESTEnvironment;
use PVE::SafeSyslog;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Node index.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
},
code => sub {
my ($param) = @_;
my $result = [
{ name => 'vncshell' },
];
return $result;
}});
__PACKAGE__->register_method ({
name => 'vncshell',
path => 'vncshell',
method => 'POST',
description => "Creates a VNC Shell proxy.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
websocket => {
optional => 1,
type => 'boolean',
description => "use websocket instead of standard vnc.",
default => 1,
},
},
},
returns => {
additionalProperties => 0,
properties => {
user => { type => 'string' },
ticket => { type => 'string' },
port => { type => 'integer' },
upid => { type => 'string' },
},
},
code => sub {
my ($param) = @_;
my $node = $param->{node};
# we only implement the websocket based VNC here
my $websocket = $param->{websocket} // 1;
die "standard VNC not implemented" if !$websocket;
my $authpath = "/nodes/$node";
my $restenv = PVE::RESTEnvironment->get();
my $user = $restenv->get_user();
my $ticket = Ticket::assemble_vnc_ticket($user, $authpath);
my $family = PVE::Tools::get_host_address_family($node);
my $port = PVE::Tools::next_vnc_port($family);
my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
'-timeout', 10, '-notls', '-listen', 'localhost',
'-c', '/usr/bin/top'];
my $realcmd = sub {
my $upid = shift;
syslog ('info', "starting vnc proxy $upid\n");
my $cmdstr = join (' ', @$cmd);
syslog ('info', "launch command: $cmdstr");
eval {
foreach my $k (keys %ENV) {
next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME';
delete $ENV{$k};
}
$ENV{PWD} = '/';
$ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
PVE::Tools::run_command($cmd, errmsg => "vncterm failed");
};
if (my $err = $@) {
syslog('err', $err);
}
return;
};
my $upid = $restenv->fork_worker('vncshell', "", $user, $realcmd);
PVE::Tools::wait_for_vnc_port($port);
return {
user => $user,
ticket => $ticket,
port => $port,
upid => $upid,
};
}});
__PACKAGE__->register_method({
name => 'vncwebsocket',
path => 'vncwebsocket',
method => 'GET',
description => "Opens a weksocket for VNC traffic.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vncticket => {
description => "Ticket from previous call to vncproxy.",
type => 'string',
maxLength => 512,
},
port => {
description => "Port number returned by previous vncproxy call.",
type => 'integer',
minimum => 5900,
maximum => 5999,
},
},
},
returns => {
type => "object",
properties => {
port => { type => 'string' },
},
},
code => sub {
my ($param) = @_;
my $authpath = "/nodes/$param->{node}";
my $restenv = PVE::RESTEnvironment->get();
my $user = $restenv->get_user();
Ticket::verify_vnc_ticket($param->{vncticket}, $user, $authpath);
my $port = $param->{port};
return { port => $port };
}});
package NodeAPI;
use strict;
use warnings;
use PVE::RESTHandler;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
subclass => "NodeInfoAPI",
path => '{node}',
});
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Cluster node index.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{node}" } ],
},
code => sub {
my ($param) = @_;
my $res = [
{ node => 'elsa' },
];
return $res;
}});
package YourAPI;
use strict;
use warnings;
use PVE::RESTHandler;
use PVE::JSONSchema;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
subclass => "NodeAPI",
path => 'nodes',
});
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Directory index.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
},
code => sub {
my ($resp, $param) = @_;
my $res = [ { subdir => 'nodes' } ];
return $res;
}});
# This is the REST/HTTPS Server
package DemoServer;
use strict;
use warnings;
use HTTP::Status qw(:constants);
use URI::Escape;
use PVE::APIServer::AnyEvent;
use PVE::Exception qw(raise_param_exc);
use PVE::RESTEnvironment;
use base('PVE::APIServer::AnyEvent');
sub new {
my ($this, %args) = @_;
my $class = ref($this) || $this;
my $self = $class->SUPER::new(%args);
PVE::RESTEnvironment->init('pub');
return $self;
}
sub auth_handler {
my ($self, $method, $rel_uri, $ticket, $token, $peer_host) = @_;
my $restenv = PVE::RESTEnvironment::get();
$restenv->set_user(undef);
# explicitly allow some calls without authentication
if ($rel_uri eq '/access/ticket' &&
($method eq 'POST' || $method eq 'GET')) {
return; # allow call to create ticket
}
my $userid = Ticket::verify_ticket($ticket);
$restenv->set_user($userid);
return {
ticket => $ticket,
userid => $userid,
};
}
sub rest_handler {
my ($self, $clientip, $method, $rel_uri, $auth, $params) = @_;
my $resp = {
status => HTTP_NOT_IMPLEMENTED,
message => "Method '$method $rel_uri' not implemented",
};
if ($rel_uri eq '/access/ticket') {
if ($method eq 'POST') {
if ($params->{username} && $params->{username} eq 'demo' &&
$params->{password} && $params->{password} eq 'demo') {
return {
status => HTTP_OK,
data => {
ticket => Ticket::create_ticket($params->{username}),
},
};
}
return $resp;
} elsif ($method eq 'GET') {
# this is allowed to display the login form
return { status => HTTP_OK, data => {} };
} else {
return $resp;
}
}
my ($handler, $info);
eval {
my $uri_param = {};
($handler, $info) = YourAPI->find_handler($method, $rel_uri, $uri_param);
return if !$handler || !$info;
foreach my $p (keys %{$params}) {
if (defined($uri_param->{$p})) {
raise_param_exc({$p => "duplicate parameter (already defined in URI)"});
}
$uri_param->{$p} = $params->{$p};
}
$resp = {
data => $handler->handle($info, $uri_param),
info => $info, # useful to format output
status => HTTP_OK,
};
};
if (my $err = $@) {
$resp = { info => $info };
if (ref($err) eq "PVE::Exception") {
$resp->{status} = $err->{code} || HTTP_INTERNAL_SERVER_ERROR;
$resp->{errors} = $err->{errors} if $err->{errors};
$resp->{message} = $err->{msg};
} else {
$resp->{status} = HTTP_INTERNAL_SERVER_ERROR;
$resp->{message} = $err;
}
}
return $resp;
}
# The main package creates the socket and runs the server
package main;
use strict;
use warnings;
use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN);
use IO::Socket::IP;
use HTTP::Headers;
use HTTP::Response;
use Data::Dumper;
use PVE::Tools qw(run_command);
use PVE::INotify;
use PVE::APIServer::Formatter::Standard;
use PVE::APIServer::Formatter::HTML;
use PVE::NoVncIndex;
my $nodename = PVE::INotify::nodename();
my $port = 9999;
my $cert_file = "simple-demo.pem";
if (! -f $cert_file) {
print "generating demo server certificate\n";
my $cmd = ['openssl', 'req', '-batch', '-x509', '-newkey', 'rsa:4096',
'-nodes', '-keyout', $cert_file, '-out', $cert_file,
'-subj', "/CN=$nodename/",
'-days', '3650'];
run_command($cmd);
}
my $socket = IO::Socket::IP->new(
LocalAddr => $nodename,
LocalPort => $port,
Listen => SOMAXCONN,
Proto => 'tcp',
GetAddrInfoFlags => 0,
ReuseAddr => 1) ||
die "unable to create socket - $@\n";
# we often observe delays when using Nagle algorithm,
# so we disable that to maximize performance
setsockopt($socket, IPPROTO_TCP, TCP_NODELAY, 1);
my $accept_lock_fn = "simple-demo.lck";
my $lockfh = IO::File->new(">>${accept_lock_fn}") ||
die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $dirs = {};
PVE::APIServer::AnyEvent::add_dirs(
$dirs, '/novnc/' => '/usr/share/novnc-pve/');
my $server = DemoServer->new(
debug => 1,
socket => $socket,
lockfile => $accept_lock_fn,
lockfh => $lockfh,
title => 'Simple Demo API',
cookie_name => 'DEMO',
logfh => \*STDOUT,
tls_ctx => { verify => 0, cert_file => $cert_file },
dirs => $dirs,
pages => {
'/' => sub { get_index($nodename, @_) },
},
);
# NOTE: Requests to non-API pages are not authenticated
# so you must be very careful here
my $root_page = <<__EOD__;
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Simple Demo Server</title>
</head>
<body>
<h1>Simple Demo Server ($nodename)</h1>
<p>You can browse the API <a href='/api2/html' >here</a>. Please sign
in with usrename <b>demo</b> and passwort <b>demo</b>.</p>
<p>Server console is here: <a href="?console=shell&novnc=1&node=$nodename">Console</a>
</body>
</html>
__EOD__
sub get_index {
my ($nodename, $server, $r, $args) = @_;
my $token = '';
my ($ticket, $userid);
if (my $cookie = $r->header('Cookie')) {
#$ticket = PVE::APIServer::Formatter::extract_auth_cookie($cookie, $server->{cookie_name});
# $userid = Ticket::verify_ticket($ticket, 1);
}
my $page = $root_page;
if (defined($args->{console}) && $args->{novnc}) {
$page = PVE::NoVncIndex::get_index('en', $userid, $token,
$args->{console}, $nodename);
}
my $headers = HTTP::Headers->new(Content_Type => "text/html; charset=utf-8");
my $resp = HTTP::Response->new(200, "OK", $headers, $page);
return $resp;
}
print "demo server listens at: https://$nodename:$port/\n";
$server->run();

195
src/examples/simple-demo.pl Executable file
View File

@ -0,0 +1,195 @@
#!/usr/bin/perl
package DemoServer;
use strict;
use warnings;
use HTTP::Status qw(:constants);
use URI::Escape;
use PVE::APIServer::AnyEvent;
use PVE::Exception qw(raise_param_exc);
use base('PVE::APIServer::AnyEvent');
use Digest::MD5;
my $secret = Digest::MD5::md5_base64($$ . time());
sub create_ticket {
my ($username) = @_;
my $salt = sprintf("%08x", time());
my $data = "$username:$salt";
my $sig = Digest::MD5::md5_base64("$data:$secret");
return "$username:$salt:$sig";
}
sub verify_ticket {
my ($ticket) = @_;
die "no ticket" if !defined($ticket);
my ($userid, $salt, $rest) = split(/:/, $ticket, 3);
die "invalid ticket" if !defined($salt) || !defined($rest);
die "invalid unsername" if $userid ne 'demo';
my $sig = Digest::MD5::md5_base64("$userid:$salt:$secret");
die "invalid ticket" if $rest ne $sig;
return $userid;
}
sub auth_handler {
my ($self, $method, $rel_uri, $ticket, $token, $peer_host) = @_;
# explicitly allow some calls without authentication
if ($rel_uri eq '/access/ticket' &&
($method eq 'POST' || $method eq 'GET')) {
return; # allow call to create ticket
}
my $userid = verify_ticket($ticket);
return {
ticket => $ticket,
userid => $userid,
};
}
sub rest_handler {
my ($self, $clientip, $method, $rel_uri, $auth, $params) = @_;
my $resp = {
status => HTTP_NOT_IMPLEMENTED,
message => "Method '$method $rel_uri' not implemented",
};
if ($rel_uri eq '/access/ticket') {
if ($method eq 'POST') {
if ($params->{username} && $params->{username} eq 'demo' &&
$params->{password} && $params->{password} eq 'demo') {
return {
status => HTTP_OK,
data => {
ticket => create_ticket($params->{username}),
},
};
}
return $resp;
} elsif ($method eq 'GET') {
# this is allowed to display the login form
return { status => HTTP_OK, data => {} };
} else {
return $resp;
}
}
$resp = {
data => {
method => $method,
clientip => $clientip,
rel_uri => $rel_uri,
auth => $auth,
params => $params,
},
info => { description => "You called API method '$method $rel_uri'" },
status => HTTP_OK,
};
return $resp;
}
package main;
use strict;
use warnings;
use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN);
use IO::Socket::IP;
use HTTP::Headers;
use HTTP::Response;
use PVE::Tools qw(run_command);
use PVE::INotify;
use PVE::APIServer::Formatter::Standard;
use PVE::APIServer::Formatter::HTML;
my $nodename = PVE::INotify::nodename();
my $port = 9999;
my $cert_file = "simple-demo.pem";
if (! -f $cert_file) {
print "generating demo server certificate\n";
my $cmd = ['openssl', 'req', '-batch', '-x509', '-newkey', 'rsa:4096',
'-nodes', '-keyout', $cert_file, '-out', $cert_file,
'-subj', "/CN=$nodename/",
'-days', '3650'];
run_command($cmd);
}
my $socket = IO::Socket::IP->new(
LocalAddr => $nodename,
LocalPort => $port,
Listen => SOMAXCONN,
Proto => 'tcp',
GetAddrInfoFlags => 0,
ReuseAddr => 1) ||
die "unable to create socket - $@\n";
# we often observe delays when using Nagle algorithm,
# so we disable that to maximize performance
setsockopt($socket, IPPROTO_TCP, TCP_NODELAY, 1);
my $accept_lock_fn = "simple-demo.lck";
my $lockfh = IO::File->new(">>${accept_lock_fn}") ||
die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $server = DemoServer->new(
socket => $socket,
lockfile => $accept_lock_fn,
lockfh => $lockfh,
title => 'Simple Demo API',
logfh => \*STDOUT,
tls_ctx => { verify => 0, cert_file => $cert_file },
pages => {
'/' => sub { get_index($nodename, @_) },
},
);
# NOTE: Requests to non-API pages are not authenticated
# so you must be very careful here
my $root_page = <<__EOD__;
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Simple Demo Server</title>
</head>
<body>
<h1>Simple Demo Server ($nodename)</h1>
You can browse the API <a href='/api2/html' >here</a>. Please sign
in with usrename <b>demo</b> and passwort <b>demo</b>.
</body>
</html>
__EOD__
sub get_index {
my ($nodename, $server, $r, $args) = @_;
my $headers = HTTP::Headers->new(Content_Type => "text/html; charset=utf-8");
my $resp = HTTP::Response->new(200, "OK", $headers, $root_page);
}
print "demo server listens at: https://$nodename:$port/\n";
$server->run();