mv $@.tmp $@
install: ${BTDATA} PMG/pmgcfg.pm
+ install -d -m 0700 -o www-data -g www-data ${DESTDIR}/var/log/pmgproxy
install -d -m 0755 ${PERL5DIR}/PMG
install -d -m 0755 ${PERL5DIR}/PMG/API2
install -d -m 0755 ${PERL5DIR}/PMG/Service
install -m 0644 PMG/pmgcfg.pm ${PERL5DIR}/PMG
+ install -m 0644 PMG/Cluster.pm ${PERL5DIR}/PMG
install -m 0644 PMG/API2.pm ${PERL5DIR}/PMG
install -m 0644 PMG/HTTPServer.pm ${PERL5DIR}/PMG
install -m 0644 PMG/Ticket.pm ${PERL5DIR}/PMG
install -m 0644 PMG/API2/Nodes.pm ${PERL5DIR}/PMG/API2
install -m 0644 PMG/API2/AccessControl.pm ${PERL5DIR}/PMG/API2
install -m 0644 PMG/Service/pmgdaemon.pm ${PERL5DIR}/PMG/Service
+ install -m 0644 PMG/Service/pmgproxy.pm ${PERL5DIR}/PMG/Service
install -d -m 0755 ${DESTDIR}/usr/bin
install -m 0755 bin/pmgdaemon ${DESTDIR}/usr/bin
+ install -m 0755 bin/pmgproxy ${DESTDIR}/usr/bin
.PHONY: upload
upload: ${DEB}
--- /dev/null
+package PMG::Cluster;
+
+use strict;
+use warnings;
+
+use Socket;
+use PVE::Tools;
+use PVE::INotify;
+
+# this is also used to get the IP of the local node
+sub remote_node_ip {
+ my ($nodename, $noerr) = @_;
+
+ # todo: implement cluster node list
+
+ # fallback: try to get IP by other means
+ my ($family, $packed_ip);
+
+ eval {
+ my @res = PVE::Tools::getaddrinfo_all($nodename);
+ $family = $res[0]->{family};
+ $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
+ };
+
+ if ($@) {
+ die "hostname lookup failed:\n$@" if !$noerr;
+ return undef;
+ }
+
+ my $ip = Socket::inet_ntop($family, $packed_ip);
+ if ($ip =~ m/^127\.|^::1$/) {
+ die "hostname lookup failed - got local IP address ($nodename = $ip)\n" if !$noerr;
+ return undef;
+ }
+
+ return wantarray ? ($ip, $family) : $ip;
+}
+
+# X509 Certificate cache helper
+
+my $cert_cache_nodes = {};
+my $cert_cache_timestamp = time();
+my $cert_cache_fingerprints = {};
+
+sub update_cert_cache {
+ my ($update_node, $clear) = @_;
+
+ syslog('info', "Clearing outdated entries from certificate cache")
+ if $clear;
+
+ $cert_cache_timestamp = time() if !defined($update_node);
+
+ my $node_list = defined($update_node) ?
+ [ $update_node ] : [ keys %$cert_cache_nodes ];
+
+ my $clear_node = sub {
+ my ($node) = @_;
+ if (my $old_fp = $cert_cache_nodes->{$node}) {
+ # distrust old fingerprint
+ delete $cert_cache_fingerprints->{$old_fp};
+ # ensure reload on next proxied request
+ delete $cert_cache_nodes->{$node};
+ }
+ };
+
+ my $nodename = PVE::INotify::nodename();
+
+ foreach my $node (@$node_list) {
+
+ if ($node ne $nodename) {
+ &$clear_node($node) if $clear;
+ next;
+ }
+
+ my $cert_path = "/etc/proxmox/pmg-api.pem";
+
+ my $cert;
+ eval {
+ my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r');
+ $cert = Net::SSLeay::PEM_read_bio_X509($bio);
+ Net::SSLeay::BIO_free($bio);
+ };
+ my $err = $@;
+ if ($err || !defined($cert)) {
+ &$clear_node($node) if $clear;
+ next;
+ }
+
+ my $fp;
+ eval {
+ $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
+ };
+ $err = $@;
+ if ($err || !defined($fp) || $fp eq '') {
+ &$clear_node($node) if $clear;
+ next;
+ }
+
+ my $old_fp = $cert_cache_nodes->{$node};
+ $cert_cache_fingerprints->{$fp} = 1;
+ $cert_cache_nodes->{$node} = $fp;
+
+ if (defined($old_fp) && $fp ne $old_fp) {
+ delete $cert_cache_fingerprints->{$old_fp};
+ }
+ }
+}
+
+# load and cache cert fingerprint once
+sub initialize_cert_cache {
+ my ($node) = @_;
+
+ update_cert_cache($node)
+ if defined($node) && !defined($cert_cache_nodes->{$node});
+}
+
+sub check_cert_fingerprint {
+ my ($cert) = @_;
+
+ # clear cache every 30 minutes at least
+ update_cert_cache(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
+
+ # get fingerprint of server certificate
+ my $fp;
+ eval {
+ $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
+ };
+ return 0 if $@ || !defined($fp) || $fp eq ''; # error
+
+ my $check = sub {
+ for my $expected (keys %$cert_cache_fingerprints) {
+ return 1 if $fp eq $expected;
+ }
+ return 0;
+ };
+
+ return 1 if &$check();
+
+ # clear cache and retry at most once every minute
+ if (time() - $cert_cache_timestamp >= 60) {
+ syslog ('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
+ update_cert_cache();
+ return &$check();
+ }
+
+ return 0;
+}
+
+1;
use PVE::RESTEnvironment;
use PMG::Ticket;
+use PMG::Cluster;
use PMG::API2;
use Data::Dumper;
sub check_cert_fingerprint {
my ($self, $cert) = @_;
- return PVE::Cluster::check_cert_fingerprint($cert);
+ return PMG::Cluster::check_cert_fingerprint($cert);
}
sub initialize_cert_cache {
my ($self, $node) = @_;
- PVE::Cluster::initialize_cert_cache($node);
+ PMG::Cluster::initialize_cert_cache($node);
}
sub remote_node_ip {
my ($self, $node) = @_;
- my $remip = PVE::Cluster::remote_node_ip($node);
+ my $remip = PMG::Cluster::remote_node_ip($node);
die "unable to get remote IP address for node '$node'\n" if !$remip;
--- /dev/null
+package PMG::Service::pmgproxy;
+
+use strict;
+use warnings;
+
+use PVE::SafeSyslog;
+use PVE::Daemon;
+use HTTP::Response;
+use URI;
+use URI::QueryParam;
+use Data::Dumper;
+
+use PVE::Tools;
+use PVE::APIServer::Formatter;
+use PVE::APIServer::Formatter::Standard;
+use PVE::APIServer::Formatter::HTML;
+use PVE::APIServer::AnyEvent;
+
+use PMG::HTTPServer;
+use PMG::API2;
+
+#use PMG::ExtJSIndex;
+
+use base qw(PVE::Daemon);
+
+my $cmdline = [$0, @ARGV];
+
+my %daemon_options = (
+ max_workers => 3,
+ restart_on_error => 5,
+ stop_wait_time => 15,
+ leave_children_open_on_reload => 1,
+ setuid => 'www-data',
+ setgid => 'www-data',
+ pidfile => '/var/run/pmgproxy/pmgproxy.pid',
+);
+
+my $daemon = __PACKAGE__->new('pmgproxy', $cmdline, %daemon_options);
+
+sub add_dirs {
+ my ($result_hash, $alias, $subdir) = @_;
+
+ PVE::APIServer::AnyEvent::add_dirs($result_hash, $alias, $subdir);
+}
+
+sub init {
+ my ($self) = @_;
+
+ my $accept_lock_fn = "/var/lock/pmgproxy.lck";
+
+ my $lockfh = IO::File->new(">>${accept_lock_fn}") ||
+ die "unable to open lock file '${accept_lock_fn}' - $!\n";
+
+ my $family = PVE::Tools::get_host_address_family($self->{nodename});
+ my $socket = $self->create_reusable_socket(8006, undef, $family);
+
+ my $dirs = {};
+
+ add_dirs($dirs, '/pve2/ext6/', '/usr/share/javascript/extjs/');
+ #add_dirs($dirs, '/pve2/images/' => '/usr/share/pve-manager/images/');
+ #add_dirs($dirs, '/pve2/css/' => '/usr/share/pve-manager/css/');
+ #add_dirs($dirs, '/pve2/js/' => '/usr/share/pve-manager/js/');
+ #add_dirs($dirs, '/pve-docs/' => '/usr/share/pve-docs/');
+ #add_dirs($dirs, '/vncterm/' => '/usr/share/vncterm/');
+ #add_dirs($dirs, '/novnc/' => '/usr/share/novnc-pve/');
+
+ $self->{server_config} = {
+ title => 'Proxmox Mail Gateway API',
+ cookie_name => 'PMG',
+ keep_alive => 100,
+ max_conn => 500,
+ max_requests => 1000,
+ lockfile => $accept_lock_fn,
+ socket => $socket,
+ lockfh => $lockfh,
+ debug => $self->{debug},
+ trusted_env => 0, # not trusted, anyone can connect
+ logfile => '/var/log/pmgproxy/pmgproxy.log',
+ ssl => {
+ # Note: older versions are considered insecure, for example
+ # search for "Poodle"-Attac
+ method => 'any',
+ sslv2 => 0,
+ sslv3 => 0,
+ cipher_list => 'HIGH:MEDIUM:!aNULL:!MD5',
+ cert_file => '/etc/proxmox/pmg-api.pem',
+ dh => 'skip2048',
+ },
+ # Note: there is no authentication for those pages and dirs!
+ pages => {
+ '/' => sub { get_index($self->{nodename}, @_) },
+ # avoid authentication when accessing favicon
+# '/favicon.ico' => {
+# file => '/usr/share/pve-manager/images/favicon.ico',
+# },
+ },
+ dirs => $dirs,
+ };
+}
+
+sub run {
+ my ($self) = @_;
+
+ my $server = PMG::HTTPServer->new(%{$self->{server_config}});
+ $server->run();
+}
+
+$daemon->register_start_command();
+$daemon->register_restart_command(1);
+$daemon->register_stop_command();
+$daemon->register_status_command();
+
+our $cmddef = {
+ start => [ __PACKAGE__, 'start', []],
+ restart => [ __PACKAGE__, 'restart', []],
+ stop => [ __PACKAGE__, 'stop', []],
+ status => [ __PACKAGE__, 'status', [], undef, sub { print shift . "\n";} ],
+};
+
+# NOTE: Requests to those pages are not authenticated
+# so we 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>Proxmox Mail Gateway API</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 $lang = 'en';
+ my $username;
+ my $token = 'null';
+
+ if (my $cookie = $r->header('Cookie')) {
+ if (my $newlang = ($cookie =~ /(?:^|\s)PMGLangCookie=([^;]*)/)[0]) {
+ if ($newlang =~ m/^[a-z]{2,3}(_[A-Z]{2,3})?$/) {
+ $lang = $newlang;
+ }
+ }
+ my $ticket = PVE::APIServer::Formatter::extract_auth_cookie($cookie, $server->{cookie_name});
+ if (($username = PMG::Ticket::verify_ticket($ticket, 1))) {
+ $token = PMG::Ticket::assemble_csrf_prevention_token($username);
+ }
+ }
+
+ $username = '' if !$username;
+
+ #my $page = PMG::ExtJSIndex::get_index($lang, $username, $token, $args->{console}, $nodename, $server->{debug});
+ my $page = $root_page;
+
+ my $headers = HTTP::Headers->new(Content_Type => "text/html; charset=utf-8");
+ my $resp = HTTP::Response->new(200, "OK", $headers, $page);
+
+ return $resp;
+}
+
+1;
--- /dev/null
+#!/usr/bin/perl -T
+
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+
+delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
+
+use strict;
+use warnings;
+
+use PVE::SafeSyslog;
+use PMG::Service::pmgproxy;
+
+$SIG{'__WARN__'} = sub {
+ my $err = $@;
+ my $t = $_[0];
+ chomp $t;
+ print STDERR "$t\n";
+ syslog('warning', "%s", $t);
+ $@ = $err;
+};
+
+my $prepare = sub {
+ my $rundir="/var/run/pmgproxy";
+ if (mkdir($rundir, 0700)) { # only works at first start if we are root)
+ my $gid = getgrnam('www-data') || die "getgrnam failed - $!\n";
+ my $uid = getpwnam('www-data') || die "getpwnam failed - $!\n";
+ chown($uid, $gid, $rundir);
+ }
+};
+
+PMG::Service::pmgproxy->run_cli_handler(prepare => $prepare);
--- /dev/null
+[Unit]
+Description=Proxmox Mail Gateway API
+ConditionPathExists=/usr/bin/pmgproxy
+Wants=pmgdaemon.service
+Wants=ssh.service
+Wants=remote-fs.target
+After=pmgdaemon.service
+After=ssh.service
+After=remote-fs.target
+
+[Service]
+ExecStart=/usr/bin/pmgproxy start
+ExecStop=/usr/bin/pmgproxy stop
+ExecReload=/usr/bin/pmgproxy restart
+PIDFile=/var/run/pmgproxy/pmgproxy.pid
+Type=forking
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+#!/bin/sh
+
+set -e
+
+
+case "$1" in
+ configure)
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
--- /dev/null
+#!/bin/sh
+
+set -e
+
+case "$1" in
+ remove|upgrade|deconfigure)
+ ;;
+
+ failed-upgrade)
+ ;;
+
+ *)
+ echo "prerm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
override_dh_installinit:
dh_systemd_enable --name=pmgdaemon pmgdaemon.service
+ dh_systemd_enable --name=pmgproxy pmgproxy.service
+
+
+override_dh_fixperms:
+ dh_fixperms --exclude /var/log/pmgproxy