]> git.proxmox.com Git - pmg-api.git/commitdiff
implement pmgproxy.pm
authorDietmar Maurer <dietmar@proxmox.com>
Thu, 26 Jan 2017 11:29:27 +0000 (12:29 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Thu, 26 Jan 2017 11:29:27 +0000 (12:29 +0100)
Makefile
PMG/Cluster.pm [new file with mode: 0644]
PMG/HTTPServer.pm
PMG/Service/pmgproxy.pm [new file with mode: 0755]
bin/pmgproxy [new file with mode: 0755]
debian/pmgproxy.service [new file with mode: 0644]
debian/postinst [new file with mode: 0644]
debian/prerm [new file with mode: 0644]
debian/rules

index ba668d900244278719a57dd4a726e080ab943bc5..89b4360d8d1759e66d0d1ea694b29d5852e27c2e 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -26,10 +26,12 @@ PMG/pmgcfg.pm: PMG/pmgcfg.pm.in
        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
@@ -37,8 +39,10 @@ install: ${BTDATA} PMG/pmgcfg.pm
        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}
diff --git a/PMG/Cluster.pm b/PMG/Cluster.pm
new file mode 100644 (file)
index 0000000..c2cfcb4
--- /dev/null
@@ -0,0 +1,149 @@
+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;
index 14426b654b26f026e1f5b129c22301821fbeab02..8c792c1865df877bcf03ed80a0542b22fcf53050 100755 (executable)
@@ -11,6 +11,7 @@ use PVE::Exception qw(raise_param_exc);
 use PVE::RESTEnvironment;
 
 use PMG::Ticket;
+use PMG::Cluster;
 use PMG::API2;
 
 use Data::Dumper;
@@ -167,19 +168,19 @@ sub rest_handler {
 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;
 
diff --git a/PMG/Service/pmgproxy.pm b/PMG/Service/pmgproxy.pm
new file mode 100755 (executable)
index 0000000..b9122d5
--- /dev/null
@@ -0,0 +1,172 @@
+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;
diff --git a/bin/pmgproxy b/bin/pmgproxy
new file mode 100755 (executable)
index 0000000..f01d1c7
--- /dev/null
@@ -0,0 +1,31 @@
+#!/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);
diff --git a/debian/pmgproxy.service b/debian/pmgproxy.service
new file mode 100644 (file)
index 0000000..06867a0
--- /dev/null
@@ -0,0 +1,19 @@
+[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
diff --git a/debian/postinst b/debian/postinst
new file mode 100644 (file)
index 0000000..9afa13c
--- /dev/null
@@ -0,0 +1,24 @@
+#!/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
diff --git a/debian/prerm b/debian/prerm
new file mode 100644 (file)
index 0000000..e2a9e69
--- /dev/null
@@ -0,0 +1,23 @@
+#!/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
index 86898d2905e63d7813f9f88704bdee04351bdf7c..b9b22e03dbe2e991bee23804d4090de510d33d7f 100755 (executable)
@@ -9,3 +9,8 @@
 
 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