dmz社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 1165|回复: 0

[功能实现] PHP 进行统一邮箱登陆的代理实现(swoole)

[复制链接]
  • TA的每日心情

    2024-11-19 20:46
  • 签到天数: 244 天

    [LV.8]以坛为家I

    4434

    主题

    1459

    帖子

    1万

    积分

    会|员

    Rank: 9Rank: 9Rank: 9

    积分
    10734
    发表于 2020-2-29 02:05:45 | 显示全部楼层 |阅读模式

    本站资源全部免费,回复即可查看下载地址!

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    在工作的过程中,经常会有很多应用有发邮件的需求,这个时候需要在每个应用中配置smtp服务器。一旦公司调整了smtp服务器的配置,比如修改了密码等,这个时候对于维护的人员来说要逐一修改应用中smtp的配置。这样的情况虽然不多见,但遇上了还是很头痛的一件事情。

      知道了问题,解决起来就有了方向。于是就有了自己开发一个简单的smtp代理的想法,这个代理主要的功能(参照问题)主要是:
        1.接受指定IP应用的smtp请求;
        2.应用不需要知道smtp的用户和密码;
        3.转发应用的smtp请求。

      开发的环境:Linux,php(swoole);
      代码如下:
    [PHP] 纯文本查看 复制代码
    <?php
    /**
     *
     * SMTP Proxy Server
     * @author Terry Zhang, 2015-11-13
     *
     * @version 1.0
     *
     * 注意:本程序只能运行在cli模式,且需要扩展Swoole 1.7.20+的支持。
     *
     * Swoole的源代码及安装请参考 [url]https://github.com/swoole/swoole-src/[/url]
     *
     * 本程序的使用场景:
     * 
     * 在多个分散的系统中使用同一的邮件地址进行系统邮件发送时,一旦邮箱密码修改,则要修改每个系统的邮件配置参数。
     * 同时,在每个系统中配置邮箱参数,使得邮箱的密码容易外泄。
     * 
     * 通过本代理进行邮件发送的客户端,可以随便指定用户名和密码。
     * 
     * 
     */
    
    //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 = '';
    }
    配置文件例子:
    [PHP] 纯文本查看 复制代码
    /**
     *  运行配置
    */
    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.*'),
            )
    );

    运行例子:
    [AppleScript] 纯文本查看 复制代码
    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);
    });

    应用配置:
    [AppleScript] 纯文本查看 复制代码
    
      smtp host: 192.168.0.*  //指定smtpproxy 运行的服务器IP。
    
         port: 25
    
         user: xxxx  //随意填写
    
         pass:  xxxx  //随意填写
    
         from: [email]xxxx@xxxx.com[/email] // 根据情况填写
    
     

    存在的问题:
        1、不支持ssl模式;
        2、应用的from还是要填写正确,否则发出的邮件发件人会显示错误。
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|Archiver|小黑屋|本站代理|dmz社区

    GMT+8, 2024-12-23 19:26 , Processed in 0.108034 second(s), 33 queries .

    Powered by Discuz! X3.4 Licensed

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表