程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> PHP 進行統一郵箱登陸的代理實現(swoole),

PHP 進行統一郵箱登陸的代理實現(swoole),

編輯:關於PHP編程

PHP 進行統一郵箱登陸的代理實現(swoole),


  在工作的過程中,經常會有很多應用有發郵件的需求,這個時候需要在每個應用中配置smtp服務器。一旦公司調整了smtp服務器的配置,比如修改了密碼等,這個時候對於維護的人員來說要逐一修改應用中smtp的配置。這樣的情況雖然不多見,但遇上了還是很頭痛的一件事情。

 

  知道了問題,解決起來就有了方向。於是就有了自己開發一個簡單的smtp代理的想法,這個代理主要的功能(參照問題)主要是:

    1.接受指定IP應用的smtp請求;

    2.應用不需要知道smtp的用戶和密碼;

    3.轉發應用的smtp請求。

 

  開發的環境:Linux,php(swoole);

  代碼如下:

<?php
/**
 *
 * SMTP Proxy Server
 * @author Terry Zhang, 2015-11-13
 *
 * @version 1.0
 *
 * 注意:本程序只能運行在cli模式,且需要擴展Swoole 1.7.20+的支持。
 *
 * Swoole的源代碼及安裝請參考 https://github.com/swoole/swoole-src/
 *
 * 本程序的使用場景:
 * 
 * 在多個分散的系統中使用同一的郵件地址進行系統郵件發送時,一旦郵箱密碼修改,則要修改每個系統的郵件配置參數。
 * 同時,在每個系統中配置郵箱參數,使得郵箱的密碼容易外洩。
 * 
 * 通過本代理進行郵件發送的客戶端,可以隨便指定用戶名和密碼。
 * 
 * 
 */

//error_reporting(0);

defined('DEBUG_ON') or define('DEBUG_ON', false);

//主目錄
defined('BASE_PATH') or define('BASE_PATH', __DIR__);

class CSmtpProxy{
    
    //軟件版本
    const VERSION = '1.0';    
    
    const EOF = "\r\n";    
    
    public static $software = "SMTP-Proxy-Server";
    
    private static $server_mode = SWOOLE_PROCESS;    
    
    private static $pid_file;
    
    private static $log_file;
    
    private $smtp_host = 'localhost';
    
    private $smtp_port = 25;
    
    private $smtp_user = '';
    
    private $smtp_pass = '';
    
    private $smtp_from = '';
    
    //待寫入文件的日志隊列(緩沖區)
    private $queue = array();
    
    public $host = '0.0.0.0';
    
    public $port = 25;
    
    public $setting = array();
    
    //最大連接數
    public $max_connection = 50;
    

    /**
     * @var swoole_server
     */
    protected $server;
    
    protected $connection = array();
    
    public static function setPidFile($pid_file){
        self::$pid_file = $pid_file;
    }
    
    public static function start($startFunc){
        if(!extension_loaded('swoole')){
            exit("Require extension `swoole`.\n");
        }
        $pid_file = self::$pid_file;
        $server_pid = 0;
        if(is_file($pid_file)){
            $server_pid = file_get_contents($pid_file);
        }
        global $argv;
        if(empty($argv[1])){
            goto usage;
        }elseif($argv[1] == 'reload'){
            if (empty($server_pid)){
                exit("SMTP Proxy Server is not running\n");
            }
            posix_kill($server_pid, SIGUSR1);
            exit;
        }elseif ($argv[1] == 'stop'){
            if (empty($server_pid)){
                exit("SMTP Proxy is not running\n");
            }
            posix_kill($server_pid, SIGTERM);
            exit;
        }elseif ($argv[1] == 'start'){
            //已存在ServerPID,並且進程存在
            if (!empty($server_pid) and posix_kill($server_pid,(int) 0)){
                exit("SMTP Proxy is already running.\n");
            }
            //啟動服務器
            $startFunc();
        }else{
            usage:
            exit("Usage: php {$argv[0]} start|stop|reload\n");
        }
    }
    
    public function __construct($host,$port){    
        $flag = SWOOLE_SOCK_TCP;
        $this->server = new swoole_server($host,$port,self::$server_mode,$flag);
        $this->host = $host;
        $this->port = $port;
        $this->setting = array(
                'backlog' => 128,
                'dispatch_mode' => 2,
        );
    }
    
    public function daemonize(){
        $this->setting['daemonize'] = 1;
    }
    
    public function getConnectionInfo($fd){
        return $this->server->connection_info($fd);
    }
    
    /**
     * 啟動服務進程
     * @param array $setting
     * @throws Exception
     */
    public function run($setting = array()){
        $this->setting = array_merge($this->setting,$setting);
        //不使用swoole的默認日志
        if(isset($this->setting['log_file'])){
            self::$log_file = $this->setting['log_file'];
            unset($this->setting['log_file']);
        }
        if(isset($this->setting['max_connection'])){
            $this->max_connection = $this->setting['max_connection'];
            unset($this->setting['max_connection']);
        }
        if(isset($this->setting['smtp_host'])){
            $this->smtp_host = $this->setting['smtp_host'];
            unset($this->setting['smtp_host']);
        }
        if(isset($this->setting['smtp_port'])){
            $this->smtp_port = $this->setting['smtp_port'];
            unset($this->setting['smtp_port']);
        }
        if(isset($this->setting['smtp_user'])){
            $this->smtp_user = $this->setting['smtp_user'];
            unset($this->setting['smtp_user']);
        }
        if(isset($this->setting['smtp_pass'])){
            $this->smtp_pass = $this->setting['smtp_pass'];
            unset($this->setting['smtp_pass']);
        }
        if(isset($this->setting['smtp_from'])){
            $this->smtp_from = $this->setting['smtp_from'];
            unset($this->setting['smtp_from']);
        }
    
        $this->server->set($this->setting);
        $version = explode('.', SWOOLE_VERSION);
        if($version[0] == 1 && $version[1] < 7 && $version[2] <20){
            throw new Exception('Swoole version require 1.7.20 +.');
        }
        //事件綁定
        $this->server->on('start',array($this,'onMasterStart'));
        $this->server->on('shutdown',array($this,'onMasterStop'));
        $this->server->on('ManagerStart',array($this,'onManagerStart'));
        $this->server->on('ManagerStop',array($this,'onManagerStop'));
        $this->server->on('WorkerStart',array($this,'onWorkerStart'));
        $this->server->on('WorkerStop',array($this,'onWorkerStop'));
        $this->server->on('WorkerError',array($this,'onWorkerError'));
        $this->server->on('Connect',array($this,'onConnect'));
        $this->server->on('Receive',array($this,'onReceive'));
        $this->server->on('Close',array($this,'onClose'));
            
        $this->server->start();
    }
    
    public function log($msg,$level = 'debug',$flush = false){
        if(DEBUG_ON){
            $log = date('Y-m-d H:i:s').' ['.$level."]\t" .$msg."\n";
            if(!empty(self::$log_file)){
                $debug_file = dirname(self::$log_file).'/debug.log';
                file_put_contents($debug_file, $log,FILE_APPEND);
                if(filesize($debug_file) > 10485760){//10M
                    unlink($debug_file);
                }
            }
            echo $log;
        }
        if($level != 'debug'){
            //日志記錄
            $this->queue[] = date('Y-m-d H:i:s')."\t[".$level."]\t".$msg;
        }
        if(count($this->queue)>10 && !empty(self::$log_file) || $flush){
            if (filesize(self::$log_file) > 209715200){ //200M
                rename(self::$log_file,self::$log_file.'.'.date('His'));
            }
            $logs = '';
            foreach ($this->queue as $q){
                $logs .= $q."\n";
            }
            file_put_contents(self::$log_file, $logs,FILE_APPEND);
            $this->queue = array();
        }
    }
    
    public function shutdown(){
        return $this->server->shutdown();
    }
    
    public function close($fd){
        return $this->server->close($fd);
    }
    
    public function send($fd,$data){
        $data = strtr($data,array("\n" => "", "\0" => "", "\r" => ""));
        $this->log("[P --> C]\t" . $data);
        return $this->server->send($fd,$data.self::EOF);
    }
    
    
    /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
     + 事件回調
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
    
    public function onMasterStart($serv){
        global $argv;
        swoole_set_process_name('php '.$argv[0].': master -host='.$this->host.' -port='.$this->port);
        if(!empty($this->setting['pid_file'])){
            file_put_contents(self::$pid_file, $serv->master_pid);
        }
        $this->log('Master started.');
    }
    
    public function onMasterStop($serv){
        if (!empty($this->setting['pid_file'])){
            unlink(self::$pid_file);
        }
        $this->shm->delete();
        $this->log('Master stop.');
    }
    
    public function onManagerStart($serv){
        global $argv;
        swoole_set_process_name('php '.$argv[0].': manager');
        $this->log('Manager started.');
    }
    
    public function onManagerStop($serv){
        $this->log('Manager stop.');
    }
    
    public function onWorkerStart($serv,$worker_id){
        global $argv;
        if($worker_id >= $serv->setting['worker_num']) {
            swoole_set_process_name("php {$argv[0]}: worker [task]");
        } else {
            swoole_set_process_name("php {$argv[0]}: worker [{$worker_id}]");
        }
        $this->log("Worker {$worker_id} started.");
    }
    
    public function onWorkerStop($serv,$worker_id){
        $this->log("Worker {$worker_id} stop.");
    }
    
    public function onWorkerError($serv,$worker_id,$worker_pid,$exit_code){
        $this->log("Worker {$worker_id} error:{$exit_code}.");
    }
    
    public function onConnect($serv,$fd,$from_id){
        if(count($this->server->connections) <= $this->max_connection){
            $info = $this->getConnectionInfo($fd);
            if($this->isIpAllow($info['remote_ip'])){
                //建立服務器連接
                $cli = new Client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC); //異步非阻塞
                $cli->on('connect',array($this,'onServerConnect'));
                $cli->on('receive',array($this,'onServerReceive'));
                $cli->on('error',array($this,'onServerError'));
                $cli->on('close',array($this,'onServerClose'));
                $cli->fd = $fd;
                $ip = gethostbyname($this->smtp_host);
                if($cli->connect($ip,$this->smtp_port) !== false){
                    $this->connection[$fd] = $cli;    
                }else{
                    $this->close($fd);
                    $this->log('Cannot connect to SMTP server. Connection #'.$fd.' close.');
                }                
            }else{
                $this->log('Blocked clinet connection, IP deny : '.$info['remote_ip'],'warn');
                $this->server->close($fd);
                $this->log('Connection #'.$fd.' close.');
            }
        }else{
            $this->log('Blocked clinet connection, too many connections.','warn');
            $this->server->close($fd);            
        }
    }
    
    public function onReceive($serv,$fd,$from_id,$recv_data){
        $info = $this->getConnectionInfo($fd);
        $this->log("[P <-- C]\t".trim($recv_data));
        //禁止使用STARTTLS
        if(strtoupper(trim($recv_data)) == 'STARTTLS'){
            $this->server->send($fd,"502 Not implemented".self::EOF);
            $this->log("[P --> C]\t502 Not implemented");
        }else{
            
            //重置登陸驗證                    
            if(preg_match('/^AUTH\s+LOGIN(.*)/', $recv_data,$m)){
                $m[1] = trim($m[1]);
                if(empty($m[1])){
                    //只發送AUTH LOGIN 接下來將發送用戶名
                    $this->connection[$fd]->user = $this->smtp_user;
                }else{
                    $recv_data = 'AUTH LOGIN '.base64_encode($this->smtp_user).self::EOF;
                    $this->connection[$fd]->pass = $this->smtp_pass;
                }
            }else{
                if(preg_match('/^HELO.*|^EHLO.*/', $recv_data)){
                    $recv_data = 'HELO '.$this->smtp_host.self::EOF;
                }
                //重置密碼
                if(!empty($this->connection[$fd]->pass)){
                    $recv_data = base64_encode($this->connection[$fd]->pass).self::EOF;
                    $this->connection[$fd]->pass = '';
                }
                //重置用戶名
                if(!empty($this->connection[$fd]->user)){
                    $recv_data = base64_encode($this->connection[$fd]->user).self::EOF;
                    $this->connection[$fd]->user = '';
                    $this->connection[$fd]->pass = $this->smtp_pass;
                }
                
                //重置mail from
                if(preg_match('/^MAIL\s+FROM:.*/', $recv_data)){
                    $recv_data = 'MAIL FROM:<'.$this->smtp_from.'>'.self::EOF;
                }
            }
            
            if($this->connection[$fd]->isConnected()){
                $this->connection[$fd]->send($recv_data);
                $this->log("[P --> S]\t".trim($recv_data));
            }
        }
    
    }
    
    public function onClose($serv,$fd,$from_id){
        if(isset($this->connection[$fd])){
            if($this->connection[$fd]->isConnected()){
                $this->connection[$fd]->close();
                $this->log('Connection on SMTP server close.');
            }
        }
        $this->log('Connection #'.$fd.' close. Flush the logs.','debug',true);
    }
    
    /*---------------------------------------------
     * 
     * 服務器連接事件回調
     * 
     ----------------------------------------------*/
    
    public function onServerConnect($cli){
        $this->log('Connected to SMTP server.');
    }
    
    public function onServerReceive($cli,$data){
        $this->log("[P <-- S]\t".trim($data));        
        if($this->server->send($cli->fd,$data)){
            $this->log("[P --> C]\t".trim($data));
        }        
    }
    
    public function onServerError($cli){
        $this->server->close($cli->fd);
        $this->log('Connection on SMTP server error: '.$cli->errCode.' '.socket_strerror($cli->errCode),'warn');        
    }
    
    
    public function onServerClose($cli){
        $this->log('Connection on SMTP server close.');        
        $this->server->close($cli->fd);        
    }
    
    /**
     * IP地址過濾
     * @param unknown $ip
     * @return boolean
     */
    public function isIpAllow($ip){
        $pass = false;
        if(isset($this->setting['ip']['allow'])){
            foreach ($this->setting['ip']['allow'] as $addr){
                $pattern = '/'.str_replace('*','\d+',str_replace('.', '\.', $addr)).'/';
                if(preg_match($pattern, $ip) && !empty($addr)){
                    $pass = true;
                    break;
                }
            }
        }        
        if($pass){
            if(isset($this->setting['ip']['deny'])){
                foreach ($this->setting['ip']['deny'] as $addr){
                    $pattern = '/'.str_replace('*','\d+',str_replace('.', '\.', $addr)).'/';
                    if(preg_match($pattern, $ip) && !empty($addr)){
                        $pass = false;
                        break;
                    }
                }
            }
        }
        return $pass;
    }
    
}


class Client extends swoole_client{
    /**
     * 記錄當前連接
     * @var unknown
     */
    public $fd ;
    
    public $user = '';
    
    /**
     * smtp登陸密碼
     * @var unknown
     */
    public $pass = '';
}

 

 

  配置文件例子:

 

  

/**
 *  運行配置
*/
return array(
        'worker_num' => 12,
        'log_file' => BASE_PATH.'/logs/proxyserver.log',
        'pid_file' => BASE_PATH.'/logs/proxyserver.pid',
        'heartbeat_idle_time' => 300,
        'heartbeat_check_interval' => 60,
        'max_connection' => 50,   
     //配置真實的smtp信息 'smtp_host' => '', 'smtp_port' => 25, 'smtp_user' => '', 'smtp_pass' => '', 'smtp_from' => '', 'ip' => array( 'allow' => array('192.168.0.*'), 'deny' => array('192.168.10.*','192.168.100.*'), ) );

  運行例子:

  

defined('BASE_PATH') or define('BASE_PATH', __DIR__);
defined('DEBUG_ON') or define('DEBUG_ON', true);
//服務器配置
require BASE_PATH.'/CSmtpProxy.php';

$settings = require BASE_PATH.'/conf/config.php';

CSmtpProxy::setPidFile($settings['pid_file']);

CSmtpProxy::start(function(){
	global $settings;
	$serv = new CSmtpProxy('0.0.0.0', 25);
	$serv->daemonize();
	$serv->run($settings);
});

  應用配置:

  smtp host: 192.168.0.*  //指定smtpproxy 運行的服務器IP。

     port: 25

     user: xxxx  //隨意填寫

     pass:  xxxx  //隨意填寫

     from: [email protected] // 根據情況填寫

 

——————————————————————————————————————————————————————

 

  存在的問題:

    1、不支持ssl模式;

    2、應用的from還是要填寫正確,否則發出的郵件發件人會顯示錯誤。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved