aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--INSTALL5
-rw-r--r--config.yml17
-rw-r--r--css/style.css99
-rw-r--r--img/fripost_logo.pngbin0 -> 6567 bytes
-rwxr-xr-xindex.cgi18
-rw-r--r--lib/FPanel/Interface.pm118
-rw-r--r--lib/FPanel/Login.pm194
-rwxr-xr-xserver.pl23
-rw-r--r--template/domain-list.html38
-rw-r--r--template/error.html20
-rw-r--r--template/login.html38
11 files changed, 570 insertions, 0 deletions
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..ef51895
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,5 @@
+apt-get install libcgi-application-perl
+libcgi-application-plugin-authentication-perl
+libcgi-application-plugin-config-simple-perl
+libcgi-application-server-perl libhtml-template-pro-perl
+libyaml-syck-perl
diff --git a/config.yml b/config.yml
new file mode 100644
index 0000000..17df6d4
--- /dev/null
+++ b/config.yml
@@ -0,0 +1,17 @@
+---
+# LDAP configuration
+ldap: { server_host: ldap://127.0.0.1:3890/
+ , base_dn: ou=virtual,o=mailHosting,dc=fripost,dc=dev
+ , authcid: FPanel
+ , bind_pw: panel #TODO: this is to be replaced with a Kerberos ticket (SASL/GSSAPI authentication)
+ }
+
+# The domain that is to be appended to non fully qualified usernames
+default_realm: fripost.org
+
+# Session configuration
+session: { cookie: { path: '/index.cgi'
+ , secure: 0 # TODO: turn that on on HTTS connections
+ }
+ , expire: '+24h'
+ }
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..fe42282
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,99 @@
+/* Global */
+body {
+ font-family: "DejaVu Sans", Helvetica, Arial, sans-serif;
+ font-size: 11pt;
+ line-height: 140%;
+ color: #1a1a1a;
+}
+.error {
+ color: #FF0040;
+}
+
+
+/* Login form */
+form.loginform {
+ margin: 0px auto;
+ border: 1px solid #cccccc;
+ padding: 10pt;
+ float: center;
+ position: relative;
+ width: 300px;
+ background: #F5F5F5;
+}
+table.loginform {
+ margin:0 auto 5pt auto;
+ border-collapse:collapse;
+}
+table.loginform td {
+ padding:0 5pt 0 0;
+}
+.loginform {
+ text-align: center;
+ font-size: 12pt;
+}
+
+
+/* Header */
+#header {
+ width: 100%;
+}
+#header .column {
+ position: relative;
+ padding: 0pt;
+ border: 0pt;
+ font-size: 9pt;
+}
+#header .left {
+ width: 50%;
+ float: left;
+ text-align: left;
+}
+#header .right {
+ width: 45%;
+ float: right;
+ text-align: right;
+}
+
+
+/* Listing table */
+table.list {
+ width:90%;
+ border-top:1px solid #e5eff8;
+ border-right:1px solid #e5eff8;
+ margin:1em auto;
+ border-collapse:collapse;
+}
+table.list td {
+ color:#678197;
+ border-bottom:1px solid #e5eff8;
+ border-left:1px solid #e5eff8;
+ padding:.3em 1em;
+ text-align:center;
+}
+table.list tr.odd td {
+ background:#f7fbff
+}
+table.list th {
+ font-weight:normal;
+ color: #678197;
+ text-align:left;
+ border-bottom: 1px solid #e5eff8;
+ border-left:1px solid #e5eff8;
+ padding:.3em 1em;
+}
+table.list thead th {
+ background:#f4f9fe;
+ text-align:center;
+ font-weight:bold;
+ color:#66a3d3
+}
+.nonactive {
+ color: #FF0040;
+}
+.active {
+ color: #32CD32;
+}
+.none {
+ font-size: 6pt;
+ color: lightgray;
+}
diff --git a/img/fripost_logo.png b/img/fripost_logo.png
new file mode 100644
index 0000000..7af586a
--- /dev/null
+++ b/img/fripost_logo.png
Binary files differ
diff --git a/index.cgi b/index.cgi
new file mode 100755
index 0000000..9ac0e6e
--- /dev/null
+++ b/index.cgi
@@ -0,0 +1,18 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use utf8;
+
+use lib 'lib';
+use FPanel::Interface;
+# TODO: Try out Fast CGI
+#use CGI::Fast();
+#
+#while (my $q = new CGI::Fast){
+# my $app = new WebApp(QUERY => $q);
+# $app->run();
+#}
+
+my $cgi = FPanel::Interface->new();
+$cgi->run();
diff --git a/lib/FPanel/Interface.pm b/lib/FPanel/Interface.pm
new file mode 100644
index 0000000..adac0f0
--- /dev/null
+++ b/lib/FPanel/Interface.pm
@@ -0,0 +1,118 @@
+package FPanel::Interface;
+
+use strict;
+use warnings;
+use utf8;
+
+use lib 'lib';
+use base 'FPanel::Login';
+
+sub cgiapp_init {
+ my $self = shift;
+
+ $self->SUPER::cgiapp_init;
+
+ # define runmodes (pages) that require successful login:
+ $self->authen->protected_runmodes( ':all' );
+}
+
+sub index : Runmode {
+ my $self = shift;
+ my $template = $self->load_tmpl("index.html");
+ my $domain = (split /\//, $ENV{PATH_INFO},3)[1];
+ $template->param({
+ NAME => 'INDEX',
+ URL => $self->query->url(),
+ MYDOMAIN => $domain,
+ USER => $self->authen->username,
+ });
+ return $template->output;
+}
+
+sub DomainList : StartRunmode {
+ my $self = shift;
+
+ my ($u,$d) = split /@/, $self->authen->username, 2;
+ my $dn = "fvu=$u,fvd=$d,ou=virtual,o=mailHosting,dc=fripost,dc=dev";
+
+ my $ldap = Net::LDAP->new( 'ldap://127.0.0.1:389',
+ , async => 1,
+ , onerror => 'die'
+ );
+ my $sasl = Authen::SASL->new( mechanism => 'DIGEST-MD5'
+ , callback => { user => 'FPanel'
+ , pass => 'panel'
+ , authname => "dn:$dn" }
+ );
+ my $mesg = $ldap->bind( sasl => $sasl ) ;
+ die $mesg->error if $mesg->code;
+
+ my $domains = $ldap->search( base => "ou=virtual,o=mailHosting,dc=fripost,dc=dev"
+ , scope => 'one'
+ , filter => 'objectClass=FripostVirtualDomain'
+ , deref => 'never'
+ );
+ die $domains->error if $domains->code;
+
+
+ my $template = $self->load_tmpl("domain-list.html");
+ my $url = $self->query->url();
+ $template->param( URL => $url );
+ $template->param( USER_LOCALPART => $u, USER_DOMAINPART => $d);
+ my $i = 1;
+ $template->param( DOMAIN => [
+ map { $i = 1-$i;
+ { DOMAIN => $_->get_value('fvd')
+ , PERMS => &list_perms($_, $dn)
+ , DESCRIPTION => join ("\n", $_->get_value('description'))
+ , ISACTIVE => $_->get_value('fripostIsStatusActive') eq 'TRUE' ? 1 : 0
+ , URL => $url
+ , ODD => $i
+ };
+ }
+ $domains->sorted('fvd')
+ ]);
+ return $template->output;
+}
+
+sub list_perms {
+ my ($entry, $dn) = @_;
+ my $perms = '';
+
+ my $canCreateAlias = $entry->get_value ('fripostCanCreateAlias', asref => 1);
+ $perms .= 'a'
+ if defined $canCreateAlias and
+ grep { $dn eq $_ or (split /,/,$dn,2)[1] eq $_ }
+ @{$canCreateAlias};
+
+ my $canCreateList = $entry->get_value ('fripostCanCreateList', asref => 1);
+ $perms .= 'l'
+ if defined $canCreateList and
+ grep { $dn eq $_ or (split /,/,$dn,2)[1] eq $_ }
+ @{$canCreateList};
+
+ my $owner = $entry->get_value ('fripostOwner', asref => 1);
+ $perms = 'o'
+ if defined $owner and grep { $dn eq $_ } @{$owner};
+
+ my $postmaster = $entry->get_value ('fripostPostmaster', asref => 1);
+ $perms = 'p'
+ if defined $postmaster and grep { $dn eq $_ } @{$postmaster};
+
+ if ( $perms =~ /a/) {
+ return 'can create aliases & lists' if ( $perms =~ /l/);
+ return 'can create aliases';
+ }
+ elsif ( $perms eq 'l' ) {
+ return 'can create lists';
+ }
+ elsif ( $perms eq 'o' ) {
+ return 'owner';
+ }
+ elsif ( $perms eq 'p' ) {
+ return 'postmaster';
+ }
+}
+
+1;
+
diff --git a/lib/FPanel/Login.pm b/lib/FPanel/Login.pm
new file mode 100644
index 0000000..8f0af21
--- /dev/null
+++ b/lib/FPanel/Login.pm
@@ -0,0 +1,194 @@
+package FPanel::Login;
+
+use strict;
+use warnings;
+use utf8;
+
+use base 'CGI::Application';
+
+use CGI::Application::Plugin::AutoRunmode;
+use CGI::Application::Plugin::Session;
+use CGI::Application::Plugin::Authentication;
+use CGI::Application::Plugin::Redirect;
+use CGI::Application::Plugin::ConfigAuto qw/cfg/;
+
+use Net::LDAP;
+use Authen::SASL;
+
+
+# This method is called right before the 'setup' method below. It
+# initializes the session and authentication configurations.
+sub cgiapp_init {
+ my $self = shift;
+
+ $self->session_config(
+ # TODO: Use a Berkeley DB instead
+ CGI_SESSION_OPTIONS => [ 'driver:File'
+ , $self->query
+ , { Directory => '/tmp/fpanel-cgisess' }
+ ],
+ DEFAULT_EXPIRY => '+24h',
+ COOKIE_PARAMS => { -path => '/index.cgi/'
+ , -httponly => 1
+# # TODO: Turn the secure flag for HTTPS connections
+ , -secure => 0
+ },
+ SEND_COOKIE => 1,
+ );
+
+ # Configure authentication parameters
+ $self->authen->config(
+ DRIVER => [ 'Generic'
+ , \&authenticate ],
+ STORE => 'Session',
+ LOGOUT_RUNMODE => 'logout',
+ LOGIN_RUNMODE => 'login',
+ RENDER_LOGIN => \&login_box,
+ LOGIN_SESSION_TIMEOUT => { IDLE_FOR => '30m' },
+ );
+
+ # The run modes that require authentication
+ $self->authen->protected_runmodes( qw /okay error_rm/ );
+}
+
+
+# This method is called by the inherited new() constructor method.
+sub setup {
+ my $self = shift;
+
+ $self->tmpl_path( 'template/' );
+ $self->mode_param( \&mymode_param );
+}
+
+
+# This method choses the Run Mode depending on the URL and query string.
+sub mymode_param {
+ my $self = shift;
+ my $q = $self->query;
+ my @path = split /\//, $ENV{PATH_INFO};
+ pop @path if $#path > 0 and $path[$#path] eq '';
+
+ my $mode = 'DomainList';
+
+ if (defined $q->param('authen_username') and
+ defined $q->param('authen_password')) {
+ $mode = 'okay'
+ }
+ elsif (defined $q->param('a')) {
+ my $a = $q->param('a');
+ if ($a eq 'login') {
+ $mode = 'login';
+ }
+ elsif ($a eq 'logout') {
+ $mode = 'logout';
+ }
+ elsif ($a eq 'AddDomain') {
+ $mode = 'AddDomain';
+ }
+ }
+ elsif ($#path < 0) {
+ $mode = 'DomainList';
+ }
+ elsif ($path[1] ne '') {
+ # $domain = $path[1];
+ $mode = 'index';
+ }
+ print STDERR $ENV{PATH_INFO} . '?' . $q->query_string
+ . " -> "
+ . $mode
+ . "\n";
+ return $mode;
+}
+
+
+sub okay : Runmode {
+ my $self = shift;
+ my $destination = $self->query->param('destination') //
+ $self->query->url;
+ return $self->redirect($destination);
+}
+
+sub login : Runmode {
+ my $self = shift;
+ my $url = $self->query->url;
+
+ # Do not come back here afterwards
+ $self->query->delete( 'a' )
+ if (defined $self->query->param('a')) and
+ $self->query->param('a') eq 'login';
+
+ # A logged user has no reason to ask for a relogin
+ $self->authen->logout() if defined $self->authen->username;
+
+ $self->query->param( destination => $self->query->self_url)
+ unless (defined $self->query->param('destination'));
+
+ return $self->login_box;
+}
+
+sub login_box {
+ my $self = shift;
+
+ my $template = $self->load_tmpl('login.html');
+
+ my $destination = $self->query->param('destination') //
+ $self->mymode_param();
+
+ $template->param(ERROR => $self->authen->login_attempts);
+ $template->param(DESTINATION => $destination);
+
+ return $template->output;
+}
+
+sub logout : Runmode {
+ my $self = shift;
+
+ if ($self->authen->username) {
+ $self->authen->logout;
+ $self->session->delete;
+ }
+ return $self->redirect($self->query->url . '/');
+}
+
+sub error_rm : ErrorRunmode {
+ my $self = shift;
+ my $error = shift;
+ my $template = $self->load_tmpl("template/error.html");
+ $template->param(NAME => 'ERROR');
+ $template->param(MESSAGE => $error);
+ $template->param(URL => $self->query->url);
+ return $template->output;
+}
+
+#sub AUTOLOAD : Runmode {
+# my $self = shift;
+# my $rm = shift;
+# my $template = $self->load_tmpl("template/error.html");
+# $template->param(NAME => 'AUTOLOAD');
+# $template->param(MESSAGE =>
+# "Error: could not find run mode \'$rm\'\n");
+# $template->param(URL => $self->query->url);
+# return $template->output;
+#}
+
+sub authenticate {
+# my $self = shift;
+
+ my ($u, $p) = @_;
+ my ($l,$d) = split /@/, $u, 2;
+
+
+# my %CFG = $self->cfg;
+
+ unless (defined $d) {
+ $d = 'fripost.org';
+ $u .= '@'.$d;
+ }
+ my $ldap = Net::LDAP->new( 'ldap://127.0.0.1:389' );
+ my $mesg = $ldap->bind ( "fvu=$l,fvd=$d,ou=virtual,o=mailHosting,dc=fripost,dc=dev"
+ , password => $p );
+ $mesg->code ? 0 : $u;
+}
+
+1;
+
diff --git a/server.pl b/server.pl
new file mode 100755
index 0000000..bc76168
--- /dev/null
+++ b/server.pl
@@ -0,0 +1,23 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use utf8;
+
+use CGI::Application::Server;
+use lib 'lib';
+use FPanel::Interface;
+#use MyCGI::App::Account::Dispatch;
+
+my $server = CGI::Application::Server->new();
+#my $panel = FPanel::Interface->new(
+# PARAMS => { cfg_file => ['config.yml']
+# , format => 'YAML'
+# }
+#);
+
+$server->entry_points({
+ '/index.cgi' => #$panel
+ 'FPanel::Interface'
+});
+$server->run();
diff --git a/template/domain-list.html b/template/domain-list.html
new file mode 100644
index 0000000..771434d
--- /dev/null
+++ b/template/domain-list.html
@@ -0,0 +1,38 @@
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ <title>Domain names for <TMPL_VAR NAME=USER></title>
+ <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />
+ </head>
+ <body>
+ <div id=header>
+ <div class="left column">
+ Root /
+ </div>
+ <div class="right column">
+ Logged as <a href="<TMPL_VAR NAME=URL>/<TMPL_VAR NAME=USER_DOMAINPART>/<TMPL_VAR NAME=USER_LOCALPART>/?a=edit"
+ ><TMPL_VAR NAME=USER_LOCALPART>@<TMPL_VAR NAME=USER_DOMAINPART></a>
+ | <a href="<TMPL_VAR NAME=URL>?a=logout">Log out</a>
+ </div>
+ <br>
+ <h1>Manage domains</h1>
+ <table class=list>
+ <thead>
+ <tr class="odd">
+ <th>Domain (<a href=<TMPL_VAR NAME=URL>/?a=AddDomain>add</a>)</th>
+ <th>Permissions</th>
+ <th>Description</th>
+ <th>Active?</th>
+ </tr>
+ </thead>
+ <TMPL_LOOP NAME=DOMAIN>
+ <TMPL_IF NAME=ODD><tr class="odd"><TMPL_ELSE><tr></TMPL_IF>
+ <td><a href="<TMPL_VAR NAME=URL>/<TMPL_VAR NAME=DOMAIN>/"><TMPL_VAR NAME=DOMAIN></a></td>
+ <td><TMPL_IF NAME=PERMS><TMPL_VAR NAME=PERMS><TMPL_ELSE><span class=none>(none)</span></TMPL_IF></td>
+ <td><TMPL_IF NAME=DESCRIPTION><TMPL_VAR NAME=DESCRIPTION><TMPL_ELSE><span class=none>(none)</span></TMPL_IF></td>
+ <td><TMPL_IF NAME=ISACTIVE><span class=active>&#x2714;</span><TMPL_ELSE><span class=nonactive>&#x2718;</span></TMPL_IF></td>
+ </tr>
+ </TMPL_LOOP>
+ </table>
+ </body>
+</html>
diff --git a/template/error.html b/template/error.html
new file mode 100644
index 0000000..34a4762
--- /dev/null
+++ b/template/error.html
@@ -0,0 +1,20 @@
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ <title><TMPL_IF NAME=NAME> <TMPL_VAR NAME=NAME> </TMPL_IF></title>
+ <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />
+ </head>
+ <body>
+ <TMPL_IF NAME=NAME>
+ <p>This is the <span class=error><TMPL_VAR NAME=NAME></span> page.
+ You are not suppose to see this. If you think it is a bug, please
+ report it to <a href="mailto:admin@fripost.org">admin@fripost.org</a>.
+ </p>
+ </TMPL_IF>
+
+ <TMPL_IF NAME=MESSAGE><p class=error><b><TMPL_VAR NAME=MESSAGE></b></p></TMPL_IF>
+
+ <hr/>
+ <TMPL_IF NAME=URL><p><a href="<TMPL_VAR NAME=URL>">Back</a></p></TMPL_IF>
+ </body>
+</html>
diff --git a/template/login.html b/template/login.html
new file mode 100644
index 0000000..676cc09
--- /dev/null
+++ b/template/login.html
@@ -0,0 +1,38 @@
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ <title>Log In | Fripost's Administrator panel</title>
+ <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />
+ </head>
+ <body class=loginform onLoad="document.loginform.authen_username.focus();">
+ </br>
+ <a href=https://fripost.org
+ ><img alt=Fripost
+ src="/img/fripost_logo.png"
+ title="fripost.org|demokratisk e-post"
+ /></a>
+ <h2>Administrator Panel</h2>
+ </br>
+ </br>
+ <form class=loginform name=loginform
+ method=POST action="./" >
+ <table class=loginform>
+ <tr>
+ <td align="right">Username</td>
+ <td><input type="text" name="authen_username" size=20 /></td>
+ </tr>
+ <tr>
+ <td align="right">Password</td>
+ <td><input type="password" name="authen_password" size=20 /></td>
+ </tr>
+ <tr>
+ </table>
+ <div>
+ <input type="hidden" name="destination" value="<TMPL_VAR DESTINATION>" />
+ <input type="submit" name="login" value="Log in" />
+ </div>
+ </form>
+ <TMPL_IF NAME=ERROR><p class=error>Invalid username or password.</p></TMPL_IF>
+ </body>
+</html>
+