diff options
-rw-r--r-- | INSTALL | 5 | ||||
-rw-r--r-- | config.yml | 17 | ||||
-rw-r--r-- | css/style.css | 99 | ||||
-rw-r--r-- | img/fripost_logo.png | bin | 0 -> 6567 bytes | |||
-rwxr-xr-x | index.cgi | 18 | ||||
-rw-r--r-- | lib/FPanel/Interface.pm | 118 | ||||
-rw-r--r-- | lib/FPanel/Login.pm | 194 | ||||
-rwxr-xr-x | server.pl | 23 | ||||
-rw-r--r-- | template/domain-list.html | 38 | ||||
-rw-r--r-- | template/error.html | 20 | ||||
-rw-r--r-- | template/login.html | 38 |
11 files changed, 570 insertions, 0 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..7af586a --- /dev/null +++ b/img/fripost_logo.png 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>✔</span><TMPL_ELSE><span class=nonactive>✘</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> + |