# vim: set cindent expandtab ts=4 sw=4:
#
# Copyright (c) 1998-2005 Chi-Keung Ho. All rights reserved.
#
# This programe is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# ExtMan - web interface to manage virtual accounts
# $Id$
package Ext::MgrApp;
# This package design for simple interface to userland programe,
# it Inherite basic modules and methods.
use strict;
use Ext;
use vars qw(@ISA $VERSION);
@Ext::MgrApp::ISA=qw(Ext Ext::Template);
use Ext::CGI;
use Ext::Session; # import parse_sess()
use Ext::Template;
use Ext::Utils; # import get_remoteip()
use Ext::DateTime qw(time2epoch epoch2time);
use Benchmark;
$VERSION = '0.2.3';
# the Management packages
use Ext::Mgr;
use vars qw(%lang_global);
use Ext::Lang;
sub add_methods {
my $self = shift;
my %meths = @_;
if(ref($self)) {
for my $meth (keys %meths) {
$self->{fcbl}{$meth} = $meths{$meth};
}
}
}
sub init {
my $self = shift;
# trace and store exception, install signal handler
local $SIG{__DIE__} = $SIG{__WARN__} = sub { $self->trace(@_) };
my $CGI = new Ext::CGI;
$self->{query} = $CGI;
$self->{requires_login} =1;
# must initialize first
$self->init_sysconfig;
# begin to initialize other things
$self->{tpl} = Ext::Template->new(
root => $self->{sysconfig}->{SYS_TEMPLDIR},
cache => 0,
blind_cache => 0,
);
my $sid = $CGI->get_cookie('webman_sid'); # only trust cookie
if($sid) {
$self->{sid} = $sid;
if ($self->valid_session) {
$self->{error} = undef;
$self->init_env($sid);
$CGI->set_cookie(
name => 'webman_sid',
value => $sid,
); # cookie expire after the browser closed
} else {
$CGI->set_cookie(
name => 'webman_sid',
value => '',
expires => $CGI->expires('-1y'),
);
$self->error('Session expired, please login again!');
kill_sid($self->{sid}); # destory session what ever it's
}
return 1; # return
}
$self->error('Invalid session, try again!') unless($self->permit);
}
sub init_env {
my $self = shift;
# feed the sid, or $self->{sid}, for sometime user not login
# while app calling init_env(), so need manual sid feed
my $sid = $_[0] || $self->{sid};
my $info=parse_sess($sid);
$ENV{LOGTIME} = $info->{loginTime};
$ENV{USERNAME} = $info->{User};
$ENV{USERTYPE} = $info->{Type};
}
sub init_sysconfig {
my $self = shift;
my $c = \%Ext::Cfg; # after call MgrApp::run(), %Ext::Cfg will be initialized
$c->{SYS_CONFIG} = $c->{SYS_CONFIG} || '/var/www/cgi-bin/extman/';
$c->{SYS_LANGDIR} = $c->{SYS_LANGDIR} || $c->{SYS_CONFIG}.'/lang/';
$c->{SYS_TEMPLDIR} = $c->{SYS_TEMPLDIR} || $c->{SYS_CONFIG}.'/html/';
$c->{SYS_TEMPLATE_NAME} = $c->{SYS_TEMPLATE_NAME} || 'standard';
$c->{SYS_BACKEND_TYPE} = $c->{SYS_BACKEND_TYPE} || 'mysql';
$c->{SYS_PSIZE} = $c->{SYS_PSIZE} || 20;
$c->{SYS_LANG} = $c->{SYS_LANG} || guess_intl(); # XXX auto detect?
$c->{SYS_TEMPLATE} = $c->{SYS_TEMPLATE} || 'standard';
$c->{SYS_CHARSET} = 'UTF-8'; # XXX only UTF-8
$c->{SYS_MIN_PASS_LEN} = $c->{SYS_MIN_PASS_LEN} || 2;
$c->{SYS_CRYPT_TYPE} = $c->{SYS_CRYPT_TYPE} || 'crypt';
$self->{sysconfig}=$c;
}
sub run {
my $app = shift;
my $q = $app->{query};
eval {
REQUEST:
{
$app->pre_backend; # prepare backend
if($app->{requires_login}) {
LOGIN:
{
my $user = lc $q->cgi("username");
last LOGIN if $app->already_login;
if($user and $q->cgi('action')) {
if ($app->{sysconfig}->{SYS_CAPTCHA_ON}) {
require Ext::CaptCha;
my $data = $q->get_cookie('scode');
my $raw = $q->cgi('vcode'); # verify code
my $key = $app->{sysconfig}->{SYS_CAPTCHA_KEY} || 'extmail';
my $cap = Ext::CaptCha->new(key => $key);
if (!$cap->verify(lc $raw, $data)) {
$app->{redirect} = "?__mode=show_login&error=vcode";
last LOGIN;
}
}
my($status, $ref) = $app->login;
if($status) {
# if login ok, re_calculate Quota, this is trick
# to udpate quota, only once after login. XXX
$app->init_env; # must initialize %ENV
$app->{redirect} = "?__mode=welcome&sid=$ref->{sid}";
}else {
$app->{redirect} = '?__mode=show_login&error=badlogin';
}
}
} # LOGIN block END
}
my $mode = $q->cgi("__mode") || $app->{default_mode};
my $code = $app->{fcbl}{$mode} or
$app->error("No such action: $mode"), last REQUEST;
if(($code && $app->valid) || $app->permit) {
$q->send_cookie;
unless ($app->{redirect}) {
$app->pre_run;
my $t0 = new Benchmark;
$app->global_tpl;
$code->($app);
my $t1 = new Benchmark;
my $t = timediff($t1,$t0);
my $f = "%3d wsecs (%5.2f usr + %5.2f sys)";
$app->{tpl}->assign(
TIME => sprintf($f,$t->[0], $t->[1],$t->[2])
);
$app->post_run;
}
}
if(my $url = $app->{redirect}) {
$app->redirect($url);
}
} # END of REQUEST
};
if ($@) {
$app->error($@);
}
if($app->{sysconfig}->{SYS_SHOW_WARN}) {
$app->trace($app->{sysconfig}->{SYS_SHOW_WARN});
$app->warn($app->{_trace});
}
}
sub register {
my $app = shift;
my $pkg = caller;
$pkg =~ s!Ext::App(::)*!!;
$app->{pkg} = $pkg if($pkg && !$app->{pkg});
}
sub permit {
return 1 if(shift->{pkg}=~/(Login|Signup)/);
return 0;
}
sub warn {
my $self = shift;
if($self->{tpl}->{noprint}) {
print "Content-type: text/html\n\n";
}
print $self->{_trace};
}
sub error {
my $self = shift;
my $tpl = $self->{tpl};
my $hdr = $self->{sent_headers};
$self->{query}->send_cookie;
# if(not defined $hdr or !$hdr=~m#text/html#) {
# print "Content-type: text/html\n\n";
# $self->{sent_headers} = 'text/html';
# }
$tpl->assign(ERR => "@_");
$tpl->process('error.html');
$tpl->print;
$tpl->{errmsg} = @_; # set errmsg and disable follow print
}
sub trace {
my $self = shift;
$self->{_trace} .= "@_";
$self->{tpl}->{_trace} .= "@_"; # XXX
}
# prepare backend information, eg: mysql/ldap connectoin and bind info
sub pre_backend {
my $self = shift;
my $a = "";
my $c = $self->{sysconfig};
if ($c->{SYS_BACKEND_TYPE} eq 'mysql') {
$a = Ext::Mgr->new(
type => 'mysql',
host => $c->{SYS_MYSQL_HOST},
socket => $c->{SYS_MYSQL_SOCKET},
dbname => $c->{SYS_MYSQL_DB},
dbuser => $c->{SYS_MYSQL_USER},
dbpw => $c->{SYS_MYSQL_PASS},
table => $c->{SYS_MYSQL_TABLE},
table_attr_username => $c->{SYS_MYSQL_ATTR_USERNAME},
table_attr_passwd => $c->{SYS_MYSQL_ATTR_PASSWD},
table_attr_clearpw => $c->{SYS_MYSQL_ATTR_CLEARPW},
crypt_type => $c->{SYS_CRYPT_TYPE},
psize => $c->{SYS_PSIZE} || 10,
);
} elsif ($c->{SYS_BACKEND_TYPE} eq 'ldap') {
$a = Ext::Mgr->new(
type => 'ldap',
host => $c->{SYS_LDAP_HOST},
base => $c->{SYS_LDAP_BASE},
rootdn => $c->{SYS_LDAP_RDN},
rootpw => $c->{SYS_LDAP_PASS},
ldif_attr_username => $c->{SYS_LDAP_ATTR_USERNAME},
ldif_attr_passwd => $c->{SYS_LDAP_ATTR_PASSWD},
ldif_attr_clearpw => $c->{SYS_LDAP_ATTR_CLEARPW},
crypt_type => $c->{SYS_CRYPT_TYPE},
psize => $c->{SYS_PSIZE} || 10,
bind => 1);
}else {
return 0; # auth type not support, abort
}
return 0 unless($a);
# store backend object to public use
$self->{backend} = $a;
# return handler
return 1;
}
sub login {
my $self = shift;
my $login_ok = 0;
my $q = $self->{query};
my $user = lc $q->cgi("username");
my $pass = $q->cgi("password");
my $nosameip = $q->cgi("nosameip");
my $a = $self->{backend};
my $c = $self->{sysconfig};
if($a->auth($user, $pass)) {
my $sid = gen_sid();
$self->{sid} = $sid; # save the sid and pass to other app/func*
$a->{sid} = $sid; # this is need by $ref in run();
save_sess($sid,
{
User => $user,
IPaddr => $ENV{REMOTE_ADDR},
Nosameip => ($nosameip?1:0),
loginTime => time,
Type => $a->{INFO}->{TYPE} || 'postmaster',
});
$q->set_cookie(
name => 'webman_sid',
value => $sid,
); # expire after the browser closed
$login_ok = 1;
}else {
$login_ok = 0;
}
return (1, $a) if($login_ok);
"";
}
# already_login - current it's not function, only check sid file
sub already_login {
my $self = shift;
my $q = $self->{query};
return if not $self->{sid};
if (parse_sess($self->{sid})) {
return 1;
}
0;
}
# valid_session - check the validity of current session
sub valid_session {
my $self = shift;
my $sid = $_[0] || $self->{sid};
my $sdata = parse_sess($sid);
if ($sdata && ($sdata->{Nosameip}?get_remoteip() eq $sdata->{IPaddr}:1)) {
return 1;
} else {
return 0; # invalid or expire
}
}
# valid - valid the request
sub valid {
my $self = shift;
return 0 if($self->{tpl}->{errmsg});
return 0 if($self->{error});
1;
}
sub global_tpl {
my $self = shift;
my $tpl = $self->{tpl};
# do some global template tag assignment
$tpl->assign(
USER => $ENV{USERNAME},
SID => $self->{sid},
VERSION => "ExtMan/$VERSION",
NVERSION => $VERSION,
LANG => $self->{sysconfig}->{'SYS_LANG'},
CAPTCHA_ON => ($self->{sysconfig}->{SYS_CAPTCHA_ON} ? 1 : 0),
);
if ($ENV{USERTYPE} eq 'admin') { # super user type
$tpl->assign(ADMIN => 1);
} else {
$tpl->assign(ADMIN => 0);
}
initlang($self->{sysconfig}->{'SYS_LANG'}, __PACKAGE__);
$tpl->assign(\%lang_global);
}
sub valid_perm {
my $self = shift;
my $domain = $_[0];
return 1 if ($ENV{USERTYPE} eq 'admin'); # always true
my $ref = $self->manager_owndomain($ENV{USERNAME});
if ($ref) {
if (grep(/^$domain$/, @$ref)) {
return 1;
} else {
return 0;
}
} else {
return 0;
}
}
sub manager_owndomain {
my $self = shift;
my $mgr = $self->{backend};
my $user = $_[0]; # manager username
my $res = $mgr->get_manager_info($user);
my $vd = $res->{domain};
if (ref $vd) {
if (scalar @$vd >0) {
return $vd; # return ARRAY not ref !
}
} else {
return [$vd];
}
# null
[]; # null ARRAY elemenet
}
# core function to get quota information of a specify domain
sub get_domain_usage {
my $self = shift;
my $domain = $_[0];
my $quota = 0;
my $us = $self->{backend}->get_users_list($domain) || [];
my %info = (); # cleanup
foreach my $m (@$us) {
if (my $qt = $m->{quota}) {
$qt =~ s/S$//;
$info{quota} += $qt;
}
if (my $nd = $m->{netdiskquota}) {
$nd =~ s/S$//;
$info{ndquota} += $nd;
}
}
my $as = $self->{backend}->get_aliases_list($domain);
if ($as) {
$info{alias} = scalar @$as;
}
if ($us) {
$info{user} = scalar @$us;
}
\%info;
}
sub domain_overusage {
my $self = shift;
my $mgr = $self->{backend};
my %opt = @_;
my $cur = $self->get_domain_usage($opt{domain});
my $top = $mgr->get_domain_info($opt{domain});
my $rc = 0 ; # $lang_global{'overusage_default'}; # default rc to overquota
if ($opt{alias}) {
my $qa = $top->{maxalias} || '0';
if ($qa && ($cur->{alias}+$opt{alias} > $qa)) {
return $lang_global{'overusage_alias'};
}
}
if ($opt{user}) {
my $qu = $top->{maxusers} || '0';
if ($qu && ($cur->{user}+$opt{user} > $qu)) {
return $lang_global{'overusage_user'};
}
}
if ($opt{quota}) {
my $qq = $top->{maxquota} || '0';
if ($qq) {
$qq =~ s/S//gi; # remove size flag
$cur->{quota} =~ s/S//gi;
$opt{quota} =~ s/S//gi;
if ($cur->{quota}+$opt{quota} > $qq) {
return $lang_global{'overusage_quota'};
}
}
}
if ($opt{ndquota}) {
my $qd = $top->{maxndquota} || '0';
if ($qd) {
$qd =~ s/S//gi;
$cur->{ndquota} =~ s/S//gi;
$opt{ndquota} =~ s/S//gi;
if ($cur->{ndquota}+$opt{ndquota} > $qd) {
return $lang_global{'overusage_ndquota'};
}
}
}
$rc;
}
# the important paging function merge from Mgr/* to MgrApp.pm
#
# $self, %opt => (
# filter => $filter,
# page => $page
# )
sub domain_paging {
my $self = shift;
my %opt = @_;
my $mgr = $self->{backend};
my $page = $opt{page} || 0;
my $filter = $opt{filter};
my $filter_type = $opt{filter_type};
my ($has_prev, $has_next) = (1, 0);
my $psize = $mgr->{psize}; # page size
my $begin = $page*$psize;
my $all = [];
if ($ENV{USERTYPE} eq 'postmaster') {
my $list = $self->manager_owndomain($ENV{USERNAME});
for my $e (@$list) {
push @$all, $mgr->get_domain_info($e);
}
} elsif ($ENV{USERTYPE} eq 'admin') {
$all = $mgr->get_domains_list || [];
}
my $arr = [];
delete $self->{_ext_info};
for(my $i=0; $i<scalar @$all; $i++) {
my $e = $all->[$i];
if ($filter) {
next unless $e->{$filter_type} =~ /$filter/i;
}
push @$arr, $e;
}
my $res = [];
for(my $i=$begin; $i<scalar @$arr;$i++) {
push @$res, $arr->[$i];
last if (scalar @$res>= $psize);
}
if (scalar @$res == $psize && $begin + $psize < scalar @$arr) {
$has_next =1;
}
if ($page <= 0) { $has_prev = 0 };
# XXX FIXME
$self->{_ext_info} = { total => scalar @$all };
return ($res, $has_prev, $has_next);
}
sub ext_info {
return shift->{_ext_info};
}
# ISP / HashDir relate functions*
sub get_domain_hashdir {
my $self = shift;
my $domain = shift;
my $mgr = $self->{backend};
return undef unless $domain;
my $info = $mgr->get_domain_info($domain);
return $info->{hashdirpath};
}
sub gen_domain_hashdir {
my $self = shift;
my $sys = $self->{sysconfig};
eval { require Ext::HashDir };
die 'Need Ext::HashDir' if ($@);
Ext::HashDir->import(qw(hashdir));
return undef if ($sys->{SYS_ISP_MODE} ne 'yes');
my $domain_deep = $sys->{SYS_DOMAIN_HASHDIR_DEPTH} || '2x1';
my ($len, $size) = ($domain_deep =~ /^(\d+)x(\d+)$/);
return hashdir($len, $size);
'';
}
sub gen_user_hashdir {
my $self = shift;
my $sys = $self->{sysconfig};
eval { require Ext::HashDir };
die 'Need Ext::HashDir' if ($@);
Ext::HashDir->import(qw(hashdir));
return undef if ($sys->{SYS_ISP_MODE} ne 'yes');
my $user_deep = $sys->{SYS_USER_HASHDIR_DEPTH} || '2x1';
my ($len, $size) = ($user_deep =~ /^(\d+)x(\d+)$/);
return hashdir($len, $size);
'';
}
sub num2quota {
my $self = shift;
my $type = $self->{sysconfig}->{SYS_QUOTA_TYPE} || 'vda';
my $quota = $_[0]; # must be number
if ($type eq 'vda') {
return $quota ? $quota : '0';
} else {
return $quota.'S';
}
}
sub quota2num {
my $self = shift;
my $quota = $_[0];
$quota =~ s/S$//i;
return $quota;
}
# api defination
#
# $time, $expire
#
# $expire => 0 - unlimit, [digital]+[ymd], undef -> default
sub cvt2expire {
my $self = shift;
my $default = $self->{sysconfig}->{SYS_DEFAULT_EXPIRE} || '1y';
my ($time, $expire) = @_;
if (!defined $expire || ($expire && $expire !~ /^\d+[ymd]$/)) {
$expire = $default;
}
if ($expire>0 && $expire) { # have expire setting
$time = time2epoch($time);
if (my $y = _digi_y($expire)) {
$time += _digi_y($expire);
}
if (my $m = _digi_m($expire)) {
$time += _digi_m($expire);
}
if (my $d = _digi_d($expire)) {
$time += _digi_d($expire);
}
$time = epoch2time($time);
} else {
$time = '';
}
$time; # return it, if null, means forever
}
sub valid_time {
my $self = shift;
my $dt = shift;
eval { time2epoch($dt) };
return if ($@);
1;
}
sub _digi_y {
my $digi = shift;
if ($digi =~ /(\d+)y/i) {
return $1*365*24*3600;
} else {
return undef;
}
}
sub _digi_m {
my $digi = shift;
if ($digi =~ /(\d+)m/i) {
return $1*30*24*3600;
} else {
return undef;
}
}
sub _digi_d {
my $digi = shift;
if ($digi =~ /(\d+)d/i) {
return $1*24*3600;
} else {
return undef;
}
}
# only effect for user and alias local part
sub sanity_username {
my $self = shift;
return 0 unless ($_[0]);
# contain invalid characters
if ($_[0] =~ /[^a-zA-Z0-9_\.-]/) {
return 0;
}
1;
}
# only effect for manager account, eg: foo@bar.com
sub sanity_manager {
my $self = shift;
return 0 unless ($_[0]);
if ($_[0] =~ /[^\@a-zA-Z0-9_\.-]/) {
return 0;
}
1;
}
sub pre_run { 1 };
sub post_run { 1 };
sub redirect {
my $self = shift;
my ($url, $mode) = @_;
print "Location: $url\n\n";
}
sub save_sess {
my ($sid, $hash) = @_;
my $str;
$str .= "$_ = $hash->{$_}\n" for(keys %$hash);
write_sess($sid, $str);
}
1;
syntax highlighted by Code2HTML, v. 0.9.1