From f98770409a70615be3b521b0b0ef3009ebc44f5d Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 26 Jun 2013 18:50:59 +0000 Subject: [PATCH] * EMailAdmin: Postfix tcp-map and Dovecot checkpassword scripts supporting ActiveDirectory using multivalued proxyAddresses attribute as implemented by emailadmin_smtp_ads --- emailadmin/doc/dovecot_checkpassword_ads.php | 314 ++++++++++++++ emailadmin/doc/postfix_tcp_map_ads.php | 405 +++++++++++++++++++ 2 files changed, 719 insertions(+) create mode 100644 emailadmin/doc/dovecot_checkpassword_ads.php create mode 100755 emailadmin/doc/postfix_tcp_map_ads.php diff --git a/emailadmin/doc/dovecot_checkpassword_ads.php b/emailadmin/doc/dovecot_checkpassword_ads.php new file mode 100644 index 0000000000..dae620718b --- /dev/null +++ b/emailadmin/doc/dovecot_checkpassword_ads.php @@ -0,0 +1,314 @@ +#!/usr/bin/php -Cq + 'samaccountname', // do NOT remove! +// '%n' => 'uidnumber', +// '%h' => 'mailmessagestore', + '%q' => '{quota:}proxyaddresses', + '%x' => 'dn', +); +$user_name = '%u'; // '%u@%d'; +$user_home = '/var/dovecot/imap/gruene/%u'; //'/var/dovecot/imap/%d/%u'; // mailbox location +$extra = array( + 'userdb_quota_rule' => '*:bytes=%q', +/* only for director + 'proxy' => 'Y', + 'nologin' => 'Y', + 'nopassword' => 'Y', +*/ +); +// get host by not set l attribute +/* only for director +$host_filter = 'o=%d'; +$host_base = 'dc=egroupware'; +$host_attr = 'l'; +$host_default = '10.40.8.200'; +*/ + +// to return Dovecot extra system_groups_user +$group_base = $ldap_base; +$group_filter = '(&(objectCategory=group)(member=%x))'; +$group_attr = 'cn'; +$group_append = ''; //'@%d'; + +$master_dn = $bind_dn; //"cn=admin,dc=egroupware"; +//$domain_master_dn = "cn=admin,o=%d,dc=egroupware"; + +ini_set('display_errors',false); +error_reporting(E_ALL & ~E_NOTICE); +if ($log) ini_set('error_log',$log); + +if ($_SERVER['argc'] < 2) +{ + fwrite(STDERR,"\nUsage: {$_SERVER['argv'][0]} prog-to-exec\n\n"); + fwrite(STDERR,"To test run:\n"); + fwrite(STDERR,"echo -en 'username\\0000''password\\0000' | {$_SERVER['argv'][0]} env 3<&0 ; echo $?\n"); + fwrite(STDERR,"echo -en 'username\\0000' | AUTHORIZED=1 {$_SERVER['argv'][0]} env 3<&0 ; echo $?\n"); + fwrite(STDERR,"echo -en '(dovecode-admin@domain|dovecot|cyrus)\\0000''master-password\\0000' | AUTH_LOGIN_USER=username {$_SERVER['argv'][0]} env 3<&0 ; echo $?\n\n"); + exit(2); +} + +list($username,$password) = explode("\0",file_get_contents('php://fd/3')); +if (isset($_SERVER['AUTH_LOGIN_USER'])) +{ + $master = $username; + $username = $_SERVER['AUTH_LOGIN_USER']; +} +//error_log("dovecot_checkpassword '{$_SERVER['argv'][1]}': username='$username', password='$password', master='$master'"); + +$ds = ldap_connect($ldap_uri); +if ($version) ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, $version); +if ($use_tls) ldap_start_tls($ds); + +if (!@ldap_bind($ds, $bind_dn, $bind_pw)) +{ + error_log("Can't connect to LDAP server $ldap_uri!"); + exit(111); // 111 = temporary problem +} +list(,$domain) = explode('@',$username); +if (preg_match('/^(.*)\.imapc$/',$domain,$matches)) +{ + $domain = $matches[1]; + + $username = explode('.', $username); + array_pop($username); + $username = implode('.',$username); + + $user_home = '/var/tmp/imapc-%d/%s'; + $extra = array( + 'userdb_mail' => 'imapc:/var/tmp/imapc-'.$domain.'/'.$username, + //'userdb_imapc_password' => $password, + //'userdb_imapc_host' => 'hugo.de', + ); +} + +$replace = array( + '%d' => $domain, + '%s' => $username, +); +$base = strtr($search_base, $replace); + +if (($passdb_query = !isset($_SERVER['AUTHORIZED']) || $_SERVER['AUTHORIZED'] != 1)) +{ + $filter = $passdb_filter; + + // authenticate with master user/password + // master user name is hardcoded "dovecot", "cyrus" or "dovecot-admin@domain" and mapped currently to cn=admin,[o=domain,]dc=egroupware + if (isset($master)) + { + list($n,$d) = explode('@', $master); + if (!($n === 'dovecot-admin' && $d === $domain || in_array($master,array('dovecot','cyrus')))) + { + // no valid master-user for given domain + exit(1); + } + $dn = $d ? strtr($domain_master_dn,array('%d'=>$domain)) : $master_dn; + if (!@ldap_bind($ds, $dn, $password)) + { + if ($log_verbose) error_log("Can't bind as '$dn' with password '$password'! Authentication as master '$master' for user '$username' failed!"); + exit(111); // 111 = temporary problem + } + if ($log_verbose) error_log("Authentication as master '$master' for user '$username' succeeded!"); + $passdb_query = false; + $filter = $userdb_filter; + } +} +else +{ + $filter = $userdb_filter; + putenv('AUTHORIZED=2'); +} +$filter = strtr($filter, $replace); + +// remove prefixes eg. "{quota:}proxyaddresses" +$attrs = $user_attrs; +foreach($attrs as &$a) if ($a[0] == '{') list(,$a) = explode('}', $a); + +if (!($sr = ldap_search($ds, $base, $filter, array_values($attrs)))) +{ + error_log("Error ldap_search(\$ds, '$base', '$filter')!"); + exit(111); // 111 = temporary problem +} +$entries = ldap_get_entries($ds, $sr); + +if (!$entries['count']) +{ + if ($log_verbose) error_log("User '$username' NOT found!"); + exit(1); +} + +if ($entries['count'] > 1) +{ + // should not happen for passdb, but could happen for aliases ... + error_log("Error ldap_search(\$ds, '$base', '$filter') returned more then one user!"); + exit(111); // 111 = temporary problem +} +//print_r($entries); + +if ($passdb_query) +{ + // now authenticate user by trying to bind to found dn with given password + if (!@ldap_bind($ds, $entries[0]['dn'], $password)) + { + if ($log_verbose) error_log("Can't bind as '{$entries[0]['dn']}' with password '$password'! Authentication for user '$username' failed!"); + exit(1); + } + if ($log_verbose) error_log("Successfull authentication user '$username' dn='{$entries[0]['dn']}'."); +} +else // user-db query, no authentication +{ + if ($log_verbose) error_log("User-db query for user '$username' dn='{$entries[0]['dn']}'."); +} + +// add additional placeholders from $user_attrs +foreach($user_attrs as $placeholder => $attr) +{ + if ($attr[0] == '{') // prefix given --> ignore all values without and remove it + { + list($prefix, $attr) = explode('}', substr($attr, 1)); + foreach($entries[0][$attr] as $key => $value) + { + if ($key === 'count') continue; + if (strpos($value, $prefix) !== 0) continue; + $replace[$placeholder] = substr($value, strlen($prefix)); + break; + } + } + else + { + $replace[$placeholder] = is_array($entries[0][$attr]) ? $entries[0][$attr][0] : $entries[0][$attr]; + } +} + +// search memberships +if (isset($group_base) && $group_filter && $group_attr) +{ + $base = strtr($group_base, $replace); + $filter = strtr($group_filter, $replace); + $append = strtr($group_append, $replace); + if (($sr = ldap_search($ds, $base, $filter, array($group_attr))) && + ($groups = ldap_get_entries($ds, $sr)) && $groups['count']) + { + //print_r($groups); + $system_groups_user = array(); + foreach($groups as $key => $group) + { + if ($key === 'count') continue; + $system_groups_user[] = $group[$group_attr][0].$append; + } + $extra['system_groups_user'] = implode(',', $system_groups_user); // todo: check separator + } + else + { + error_log("Error searching for memberships ldap_search(\$ds, '$base', '$filter')!"); + } +} + +// set host attribute for director to old imap +if (isset($host_base) && isset($host_filter)) +{ + if (!($sr = ldap_search($ds, $host_base, $filter=strtr($host_filter,$replace), array($host_attr)))) + { + error_log("Error ldap_search(\$ds, '$host_base', '$filter')!"); + exit(111); // 111 = temporary problem + } + $entries = ldap_get_entries($ds, $sr); + if ($entries['count'] && !isset($entries[0][$host_attr])) + { + $extra['host'] = $host_default; + } +} +// close ldap connection +ldap_unbind($ds); + +// build command to run +array_shift($_SERVER['argv']); +$cmd = array_shift($_SERVER['argv']); +foreach($_SERVER['argv'] as $arg) +{ + $cmd .= ' '.escapeshellarg($arg); +} + +// setting USER, HOME, EXTRA +putenv('USER='.strtr($user_name, $replace)); +if ($user_home) putenv('HOME='.strtr($user_home, $replace)); +if ($extra) +{ + foreach($extra as $name => $value) + { + if (($pos = strpos($value,'%')) !== false) + { + // check if replacement is set, otherwise skip whole extra-value + if (!isset($replace[substr($value,$pos,2)])) + { + unset($extra[$name]); + continue; + } + $value = strtr($value,$replace); + } + putenv($name.'='.$value); + } + putenv('EXTRA='.implode(' ', array_keys($extra))); +} + +// call given command and exit with it's exit-status +passthru($cmd, $ret); + +exit($ret); diff --git a/emailadmin/doc/postfix_tcp_map_ads.php b/emailadmin/doc/postfix_tcp_map_ads.php new file mode 100755 index 0000000000..e88faae029 --- /dev/null +++ b/emailadmin/doc/postfix_tcp_map_ads.php @@ -0,0 +1,405 @@ +#!/usr/bin/php -Cq +" allows to receive mail for given + * (includes aliases AND primary email) + * - "forward:" forwards received mail to given + * (requires account to have at an "smtp:" value!) + * - ("forwardOnly" is used for no local mailbox, only forwards, not implemented!) + * - ("quota:" is used to store quota) + * + * Groups can be used as distribution lists by assigning them an + * email address via there mail attribute (no proxyAddress) + * + * PROTOCOL DESCRIPTION + * The TCP map class implements a very simple protocol: the + * client sends a request, and the server sends one reply. + * Requests and replies are sent as one line of ASCII text, + * terminated by the ASCII newline character. Request and + * reply parameters (see below) are separated by whitespace. + * + * REQUEST FORMAT + * Each request specifies a command, a lookup key, and possi- + * bly a lookup result. + * + * get SPACE key NEWLINE + * Look up data under the specified key. + * + * put SPACE key SPACE value NEWLINE + * This request is currently not implemented. + * + * REPLY FORMAT + * Each reply specifies a status code and text. Replies must + * be no longer than 4096 characters including the newline + * terminator. + * + * 500 SPACE text NEWLINE + * In case of a lookup request, the requested data + * does not exist. In case of an update request, the + * request was rejected. The text describes the + * nature of the problem. + * + * 400 SPACE text NEWLINE + * This indicates an error condition. The text + * describes the nature of the problem. The client + * should retry the request later. + * + * 200 SPACE text NEWLINE + * The request was successful. In the case of a lookup + * request, the text contains an encoded version of + * the requested data. + * + * ENCODING + * In request and reply parameters, the character %, each + * non-printing character, and each whitespace character must + * be replaced by %XX, where XX is the corresponding ASCII + * hexadecimal character value. The hexadecimal codes can be + * specified in any case (upper, lower, mixed). + * + * The Postfix client always encodes a request. The server + * may omit the encoding as long as the reply is guaranteed + * to not contain the % or NEWLINE character. + * + * @author rb(at)stylite.de + * @copyright (c) 2012-13 by rb(at)stylite.de + * @package emailadmin + * @link http://www.postfix.org/tcp_table.5.html + * @version $Id$ + */ + +// protect from being called via HTTP +if (isset($_SERVER['HTTP_HOST'])) die('This is a command line only script!'); + +// our defaults +$default_host = 'localhost'; +$verbose = false; + +// allow only clients matching that preg to access, should be only mserver IP +//$only_client = '/^10\.40\.8\.210:/'; + +// uncomment to write to log-file, otherwise errors go to stderr +//$log = 'syslog'; // or not set (stderr) or filename '/var/log/postfix_tcp_map.log'; +//$log_verbose = true; // error's are always logged, set to true to log failures and success too + +// ldap server settings +$ldap_uri = 'ldaps://10.7.102.13/'; +$base = 'CN=Users,DC=gruene,DC=intern'; +//$bind_dn = "CN=Administrator,$base"; +//$bind_dn = "Administrator@gruene.intern"; +//$bind_pw = 'secret'; +$version = 3; +$use_tls = false; +// supported maps +$maps = array( + // virtual mailbox map + 'mailboxes' => array( + 'base' => $base, + 'filter' => '(&(objectCategory=person)(proxyAddresses=smtp:%s))', + 'attrs' => 'samaccountname', // result-attrs must be lowercase! + 'port' => 2001, + ), + // virtual alias maps + 'aliases' => array( + 'base' => $base, + 'filter' => '(&(objectCategory=person)(proxyAddresses=smtp:%s))', + 'attrs' => array('samaccountname','{forward:}proxyaddresses'), + 'port' => 2002, + ), + // groups as distribution list + 'groups' => array( + 'base' => $base, + 'filter' => '(&(objectCategory=group)(mail=%s))', + 'attrs' => 'dn', + // continue with resulting dn + 'filter1' => '(&(objectCategory=person)(proxyAddresses=smtp:*)(memberOf=%s))', + 'attrs1' => array('samaccountname','{forward:}proxyaddresses'), + 'port' => 2003, + ), +); + +ini_set('display_errors',false); +error_reporting(E_ALL & ~E_NOTICE); +if ($log) ini_set('error_log',$log); + +function usage($extra=null) +{ + global $maps; + fwrite(STDERR, "\nUsage: $cmd [-v|--verbose] [-h|--help] [-l|--log (syslog|path)] [-q|--query [user@]domain (domains|mailboxes|alias|transport|canonical)] [host]\n\n"); + fwrite(STDERR, print_r($maps,true)."\n"); + if ($extra) fwrite(STDERR, "\n\n$extra\n\n"); + exit(2); +} + +$cmd = basename(array_shift($_SERVER['argv'])); + +while (($arg = array_shift($_SERVER['argv'])) && $arg[0] == '-') +{ + switch($arg) + { + case '-v': case '--verbose': + $verbose = $log_verbose = true; + break; + + case '-h': case '--help': + usage(); + break; + + case '-l': case '--log': + $log = array_shift($_SERVER['argv']); + break; + + case '-q': case '--query': + if (count($_SERVER['argv']) == 2) // need 2 arguments + { + $request = 'get '.array_shift($_SERVER['argv'])."\n"; + $map = array_shift($_SERVER['argv']); + echo respond($request, $map)."\n"; + exit; + } + usage(); + break; + + default: + usage("Unknown option '$arg'!"); + } +} +if ($_SERVER['argv']) usage(); + +if ($arg) +{ + $host = $arg; +} +else +{ + $host = $default_host; +} + +if ($verbose) echo "using $host\n"; + +$servers = $clients = $buffers = array(); + +// Create the server socket +foreach($maps as $map => $data) +{ + $addr = 'tcp://'.$host.':'.$data['port']; + if (!($server = stream_socket_server($addr, $errno, $errstr))) + { + fwrite(STDERR, date('Y-m-d H:i:s').": Error calling stream_socket_server('$addr')!\n"); + fwrite(STDERR, $errstr." ($errno)\n"); + exit($errno); + } + $servers[$data['port']] = $server; + $clients[$data['port']] = array(); +} +while (true) // mail loop of tcp server --> never exits +{ + $read = $servers; + if ($clients) $read = array_merge($read, call_user_func_array('array_merge', array_values($clients))); + if ($verbose) print 'about to call socket_select(array('.implode(',',$read).', ...) waiting... '; + if (stream_select($read, $write=null, $except=null, null)) // null = block forever + { + foreach($read as $sock) + { + if (($port = array_search($sock, $servers)) !== false) + { + $client = stream_socket_accept($sock,$timeout,$client_addr); // @ required to get not timeout warning! + + if ($verbose) echo "accepted connection $client from $client_addr on port $port\n"; + + if ($only_client && !preg_match($only_client,$client_addr)) + { + fwrite($client,"Go away!\r\n"); + fclose($client); + error_log(date('Y-m-d H:i:s').": Connection $client from wrong client $client_addr (does NOT match '$only_client') --> terminated"); + continue; + } + $clients[$port][] = $client; + } + elseif (feof($sock)) // client connection closed + { + if ($verbose) echo "client $sock closed connection\n"; + + foreach($clients as $port => &$socks) + { + if (($key = array_search($sock, $socks, true)) !== false) + { + unset($socks[$key]); + } + } + } + else // client send something + { + $buffer =& $buffers[$sock]; + + $buffer .= fread($sock, 8096); + + if (strpos($buffer, "\n") !== false) + { + list($request, $buffer) = explode("\n", $buffer, 2); + + foreach($maps as $map => $data) + { + if (($key = array_search($sock, $clients[$data['port']], true)) !== false) + { + if ($verbose) echo date('Y-m-d H:i:s').": client send: $request for map $map\n"; + + // Respond to client + fwrite($sock, respond($request, $map)); + break; + } + } + } + } + } + if ($except) + { + echo "Exception: "; print_r($except); + } + } + else + { + // timeout expired + } +} + +/** + * escapes a string for use in searchfilters meant for ldap_search. + * + * Escaped Characters are: '*', '(', ')', ' ', '\', NUL + * It's actually a PHP-Bug, that we have to escape space. + * For all other Characters, refer to RFC2254. + * + * @param string|array $string either a string to be escaped, or an array of values to be escaped + * @return string + */ +function quote($string) +{ + return str_replace(array('\\','*','(',')','\0',' '),array('\\\\','\*','\(','\)','\\0','\20'),$string); +} + +function respond($request, $map, $extra='') +{ + static $ds; + global $ldap_uri, $version, $use_tls, $bind_dn, $bind_pw; + global $maps, $log_verbose; + + if (($map == 'aliases' || $map == 'groups') && strpos($request,'@') === false && !$extra) + { + return "500 No domain aliases yet\n"; + } + if (!isset($ds)) + { + $ds = ldap_connect($ldap_uri); + if ($version) ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, $version); + if ($use_tls) ldap_start_tls($ds); + + if (!@ldap_bind($ds, $bind_dn, $bind_pw)) + { + error_log("$map: Can't connect to LDAP server $ldap_uri!"); + $ds = null; + return "400 Can't connect to LDAP server $ldap_uri!\n"; // 400 (temp.) error + } + } + if (!preg_match('/^get ([^\n]+)\n?$/', $request, $matches)) + { + error_log("$map: Wrong format '$request'!"); + return "400 Wrong format '$request'!\n"; // 400 (temp.) error + } + $username = $matches[1]; + + list($name,$domain) = explode('@',$username); + + /* check if we are responsible for the given domain + if ($domain && $map != 'domains' && (int)($response = respond("get $domain", 'domains')) != 200) + { + return $response; + }*/ + $replace = array( + '%n' => quote($name), + '%d' => quote($domain), + '%s' => quote($username), + ); + $base = strtr($maps[$map]['base'], $replace); + $filter = strtr($maps[$map]['filter'.$extra], $replace); + $prefix = isset($maps[$map]['prefix'.$extra]) ? str_replace(array('%n','%d','%s'),array($name,$domain,$username),$maps[$map]['prefix']) : ''; + $search_attrs = $attrs = (array)$maps[$map]['attrs'.$extra]; + // remove prefix like "{smtp:}proxyaddresses" + foreach($search_attrs as &$attr) + { + if ($attr[0] == '{') list(,$attr) = explode('}', $attr); + } + unset($attr); + + if (!($sr = @ldap_search($ds, $base, $filter, $search_attrs))) + { + $errno = ldap_errno($ds); + $error = ldap_error($ds).' ('.$errno.')'; + + if ($errno == -1) // eg. -1 lost connection to ldap + { + error_log("$map: get '$username' --> 400 $error: !ldap_search(\$ds, '$base', '$filter')"); + ldap_close($ds); + $ds = null; // force new connection on next lookup + return "400 $error\n"; // 400 (temp.) error + } + else // happens if base containing domain does not exist + { + if ($log_verbose) error_log("$map: get '$username' --> 500 Not found: $error: !ldap_search(\$ds, '$base', '$filter')"); + return "500 Not found: $error\n"; // 500 not found + } + } + $entries = ldap_get_entries($ds, $sr); + + if (!$entries['count']) + { + if ($log_verbose) error_log("$map: get '$username' --> 500 not found"); + return "500 Not found\n"; // 500: Query returned no result + } + $response = array(); + foreach($entries as $key => $entry) + { + if ($key === 'count') continue; + + foreach($attrs as $attr) + { + unset($filter_prefix); + if ($attr[0] == '{') + { + list($filter_prefix, $attr) = explode('}', substr($attr, 1)); + } + foreach((array)$entry[$attr] as $k => $mail) + { + if ($k !== 'count' && ($mail = trim($mail))) + { + if ($filter_prefix) + { + if (stripos($mail, $filter_prefix) === 0) + { + $mail = substr($mail, strlen($filter_prefix)); + } + else + { + continue; + } + } + $response[] = isset($maps[$map]['return']) ? $maps[$map]['return'] : $prefix.$mail; + } + } + } + } + if (!$response) + { + if ($log_verbose) error_log("$map: get '$username' --> 500 not found"); + return "500 Not found\n"; // 500: Query returned no result + } + if (isset($maps[$map]['filter'.(1+$extra)]) && isset($maps[$map]['attrs'.(1+$extra)])) + { + return respond('get '.$response[0], $map, 1+$extra); + } + $response = '200 '.implode(',',$response)."\n"; + if ($log_verbose) error_log("$map: get '$username' --> $response"); + return $response; +}