Born to be proud
10/7
2018

利用perl+extjs开发web应用

组里的网络测试仪整个框架是用perl+extjs实现的。项目很大,看了好久了解了整体框架结构。perl+extjs的实现主要有以下几个好处:

  • 轻量级,web服务端与业务逻辑紧耦合,无需web服务器去承载应用
  • 调用外部命令行程序方便
  • 采用ajax技术,省去了频繁的页面跳转
  • extjs封装了大量的组件,界面效果风格一致,省去了前端页面开发工作

入口程序为 server.pl , 启动web服务,监听端口,轮训请求,两个主要函数如下:

sub ServerInit
{
    my $group = shift;
    my $server_sock = undef;

    if($group eq "GUI")
    {
        $server_sock = &guiServerInit($ServerParams->{CONFIG});
        GuiServer->removeShm();
    }
    elsif($group eq "PKTGEN")
    {$server_sock = &pktgenInit($ServerParams->{CONFIG});}
    elsif($group eq "CLUSTER_TEST")
    {$server_sock = &clusterMasterInit($ServerParams->{CONFIG});}
    elsif($group eq "ATTACKENGINE")
    {$server_sock = AttackEngineServer::init($ServerParams->{CONFIG});}

    if( defined($server_sock) )
    {$ServerParams->{"$group"}->{"socket"} = $server_sock;}
    else
    {
        printf("Can not setup $group socket at--[%s, %s]\n", __FILE__, __LINE__);
        return 0;
    }
    $ServerParams->{"SELECT"}->add($ServerParams->{"$group"}->{"socket"});

    return 1;
}


sub mainLoop
{
    my @ready;
    my $sock;
    my $sel = $ServerParams->{"SELECT"};
    my $GUIConnections = $ServerParams->{"GUI"}->{"connections"};
    my $PKTGENConnections = $ServerParams->{"PKTGEN"}->{"connections"};
    my $ATTACKENGINEConnections = $ServerParams->{"ATTACKENGINE"}->{"connections"};
    my $CLUSTERConnections = $ServerParams->{"CLUSTER_TEST"}->{"connections"};
    my $STATConnections =  $ServerParams->{"PKTGEN"}->{"connections"};
    my $tmpSock;
    my $tid; 
    #starts looping
    while(1)
    {
        #select: BLOCK, selection loop may be interrupted by signals.
        @ready = $sel->can_read();

        foreach my $fh (@ready)
        {
            if($fh == $ServerParams->{"GUI"}->{"socket"})
            {
                #accept the connection
                $sock = $fh->accept();
                #setup buffer for this connection
                $GUIConnections->{"$sock"} = {
                    "buffer" => ""
                };
                #add this socket into selection loop
                $sel->add($sock);
            }
            #Connection from PKTGEN
            elsif($fh == $ServerParams->{"PKTGEN"}->{"socket"})
            {
                $sock = $fh->accept();
                $PKTGENConnections->{"$sock"} = {
                    "tid" => "-1",
                    "buffer" => ""
                };
                $sel->add($sock);
            }
            elsif($fh == $ServerParams->{"ATTACKENGINE"}->{"socket"})
            {
                $sock = $fh->accept();
                $sel->add($sock);
                $ATTACKENGINEConnections->{"$sock"} = {};
            }
            elsif($fh == $ServerParams->{"CLUSTER_TEST"}->{"socket"})
            {
                # connection request from cluster master
                $sock = $fh->accept();
                $sel->add($sock);
                $CLUSTERConnections->{"$sock"} = {
                    status => "CONNECTED",
                    buffer => "",
                    'length' => 0,
                    DFA => "AUTH"
                };
            }
            elsif(&WhichConnection($fh, "GUI")) #for GUI requests
            {
                if(! &processGUIRequest_keepalive($fh, $GUIConnections))
                {
                    $sel->remove($fh);
                    close($fh); 
                    delete $GUIConnections->{"$fh"};
                }
            }
            elsif(&WhichConnection($fh, "CLUSTER_TEST"))
            {
                if(! &processClusterMasterRequest($fh, $CLUSTERConnections))
                {
                    $sel->remove($fh);
                    close($fh);
                }
            }
            #stat. from pkt-gens
            elsif(&WhichConnection($fh, "PKTGEN")) #for PKTGEN running statistics
            {
                if(! &processPKTGENMsg_keepalive($fh, $PKTGENConnections))
                {
                    $sel->remove($fh);
                    close($fh); 
                    delete $PKTGENConnections->{"$fh"};
                }
            }
            elsif(&WhichConnection($fh, "ATTACKENGINE"))
            {
                if(! AttackEngineServer::processAttackEngineResponse($fh, $ATTACKENGINEConnections->{"$fh"}))
                {
                    $sel->remove($fh);
                    close($fh); 
                    delete $ATTACKENGINEConnections->{"$fh"};
                }
            }
            else
            {
                #TODO: error
                print("Isolated connection!!!!!!!Never should be here. Need debugging.");
                #throw this connection away
                $sel->remove($fh);
                close($fh);
            }
        }
        # can write
    }
}

业务逻辑代码靠 GuiServer.pm 承载,并返回内容,主要函数如下:

sub guiServerInit
{
    my $config = shift;

    $config_defs->{config} = $config;
    $config_defs->{path} = $config->{database}->{xmlDatabasePath};
    $config_defs->{maxHistory} = $config->{web}->{maxHistory};
    $config_defs->{webRoot} = $config->{web}->{webRoot};
    $config_defs->{cacheEnabled} = $config->{web}->{cacheEnabled};
    $config_defs->{sessionExpires} = $config->{web}->{sessionExpires};
    $config_defs->{port} = $config->{web}->{port};

    $config_defs->{protocolTemplates} = $config->{msgCompiler}->{protocolTemplates};

    $config_defs->{webVersion} = $config->{web}->{version} ? $config->{web}->{version} : "0.1";
    $config_defs->{msgCompilerVersion} = $config->{msgCompiler}->{version} ? $config->{msgCompiler}->{version} : "0.1";
    $config_defs->{pktgenVersion} = $config->{pktgenVersion}->{version} ? $config->{pktgenVersion}->{version} : "0.1";


    $config_defs->{protocolScripts} = $config->{msgCompiler}->{protocolScripts};
    $config_defs->{attackScripts} = $config->{msgCompiler}->{attackScripts};
    $config_defs->{database} = Database->getDatabaseConnection() or return undef;
    $config_defs->{totParser} = ToTParser->new($config_defs->{protocolTemplates}, $config_defs->{protocolScripts});
    #$config_defs->{"physicalPortManager"} = PhysicalPortManager->new("$config_defs->{path}/System/physicalPortManagement.config","$config_defs->{path}/System/ui_physicalPortMapping.config");
    $config_defs->{"physicalPortManager"} = PhysicalPortManager->new("$config_defs->{path}");
    #$config_defs->{"physicalPortManager"} = shared_clone(PhysicalPortManager->new("$config_defs->{path}"));

    #$config_defs->{"ManagementIPConfig"} = ManagementIPConfig->new($MANAGEMENTPORT);
    $config_defs->{"ManagementIPConfig"} = ManagementIPConfig->new($config_defs->{path});

    # set session time
    $config_defs->{sessions} = Session->new($config_defs->{sessionExpires});
    $config_defs->{preferences} = Preference->new($config_defs->{path});
    $config_defs->{packetGenerator}={};
    $config_defs->{packetGenerator}->{port} =  $config->{packetGenerator}->{port};

    $config_defs->{DocGenerator} =  $config->{DocGenerator};

    $config_defs->{PcapBuffer} =  $config->{PcapBuffer};

    # printf("INIT GUI LOG FILE: $config->{web}->{logFile}\n");
    if($config->{web}->{logFile} ne "")
    {
        open($config_defs->{GUILOGFILE}, ">$config->{web}->{logFile}") || die "failed to open gui log file!\n";
        guiLog("GUI LOG FILE\n");
    }
    #print(Dumper($config_defs));
    return IO::Socket::INET->new(LocalPort=> $config_defs->{port}, Reuse => 1, Listen => SOMAXCONN);
}

sub parseGUIRequest_keepalive
{
    my ($sock, $request, $cleanSockFun) = @_;
    my ($ret, $params, $file);

    while(length($request->{"buffer"}) > 0)
    {
        if(! defined($request->{"state"}))
        {$request->{"state"} = "WAIT_FOR_HEADER";}

        if($request->{"state"} eq "WAIT_FOR_HEADER")
        {
            # whether http header is ready
            $ret = index($request->{"buffer"}, "\r\n\r\n");
            if($ret <= 0) # negative
            {return "MORE_DATA";}

            # cut request header
            $request->{"header"} = substr($request->{"buffer"}, 0, $ret + 4);

            guiLog(Dumper($request->{"header"}));

            substr($request->{"buffer"}, 0, $ret + 4) = "";

            if($request->{"header"} =~ m/^.*Accept-Language: ([^\r\n]+)\r\n/s)
            {
                #Accept-Language: zh-CN,zh;q=0.8
                @{$request->{languages}} = split(/ *, */, $1);
            }

            # parse header
            $request->{"header"} =~ m/^.*(GET|get|POST|post)\s+(\S+).*/s;
            $request->{"method"} = uc($1);
            $request->{"uri"} = $2;
            $request->{"header"} =~ m/^.*Connection: (\S+)\r\n.*/s;
            $request->{"connection"} = $1;

            # parse cookie
            if($request->{"header"} =~ m/^.*Cookie: ([^\r\n]+)\r\n/s)
            {$request->{"cookies"} = $1;}

            # check Modifed-Since
            if($request->{"header"} =~ m/^.*If-None-Match/s)
            {
                print("TODO: return 304 not modifed\n");
                print(Dumper($request->{header}));
            }

            $request->{"uri"} =~ /^([^?]+)(\?(.+))?/;
            $request->{"path"} = $1;
            if($3){
                %{$params} = split(/&|=/, $3);
            }
            foreach my $pp (keys(%{$params}))
            {$request->{"params"}->{$pp} = &urlDecode($params->{$pp});}

            $request->{"state"} = "HEADER_READY";
        }

        if($request->{"state"} eq "HEADER_READY")
        {
            if($request->{"method"} eq "GET")
            {$request->{"state"} = "REQUEST_READY";}
            else
            {
                if($request->{"header"} =~ m/^.*Content-Length:\s+(\d+).*/s)
                {$request->{"length"} = $1;}
                if($request->{"length"} < 0) # wrong http post request, ignore it.
                {return "END";}
                else
                {
                    if($request->{"path"} =~ /^\/upload\//i || $request->{"path"} =~ /^\/import\//i)
                    {$request->{"state"} = "FILE_UPLOADING";}
                    elsif(length($request->{"buffer"}) >= $request->{"length"})
                    {
                        $request->{"data"} = substr($request->{"buffer"}, 0, $request->{"length"});
                        substr($request->{"buffer"}, 0, $request->{"length"}) = "";
                        $param = {};
                        %{$params} = split(/&|=/, $request->{"data"});
                        foreach my $pp (keys(%{$params}))
                        {
                            my $tmpParam=&urlDecode($params->{$pp});
                            $tmpParam =~ s/^\s+|\s+$//g;#remove head and tail backspeace,equal to string trim
                            $request->{"params"}->{$pp} = $tmpParam;
                        }
                        $request->{"state"} = "REQUEST_READY";
                    }
                    else
                    {return "MORE_DATA";}
                }
            }
        }
        # http request is ready
        if($request->{"state"} eq "REQUEST_READY")
        {
            #print("\n++++++++++\n".$request->{"path"}."\n+++++++++++\n");

            # forward to ajax,

            if($request->{"path"} =~ /^\/ajax\//)
            {$ret = &ajaxRequest($sock, $request);}
            elsif($request->{"path"} =~ /^\/attackQuery\//)
            {$ret = &attackQuery($sock, $request);}
            elsif($request->{"path"} =~ /^\/testsQuery\//)
            {$ret = &testsQueryProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/login\//)
            {$ret = &loginProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/logout\//)
            {$ret = &logoutProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/session.js$/)
            {$ret = &sessionJSProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/command\//)
            {$ret = &commandProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/export\//)
            {$ret = &exportProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/testReportsDoc\//)
            {$ret = &testReportsDocProc($sock, $request, $cleanSockFun);}
            elsif($request->{"path"} =~ /^\/monitor\//)
            {$ret = &monitorProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/util\//)
            {$ret = &utilProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/exportPcap\//)
            {$ret = &exportPcapProc($sock, $request, $cleanSockFun);}
            elsif($request->{"path"} =~ /^\/onlineHelp\//)
            {$ret = &onlineHelpProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/loadresourcefile\//)
            {$ret = &loadResourceFileProc($sock, $request);}
            elsif($request->{"path"} =~ /^\/license\//)
            {$ret = &licenseProc($sock, $request);}
            else
            {$ret = &staticFileProc($sock, $request);}

            if(! defined($ret))
            {printf("connection closed!\n"); return undef;}

            foreach my $e (keys(%{$request}))
            {
                if($e eq "state" || $e eq "buffer")
                {next;}
                delete $request->{$e};
            }
            # waiting for next round
            $request->{"state"} = "WAIT_FOR_HEADER";

        }

        if($request->{"state"} eq "FILE_UPLOADING")
        {
            if(&fileUploadProc($sock, $request))
            {
                foreach my $e (keys(%{$request}))
                {
                    if($e eq "state" || $e eq "buffer")
                    {next;}
                    delete $request->{$e};
                }
                $request->{"state"} = "WAIT_FOR_HEADER";
            }
        }
        if(defined($request->{"connection"})){
            if($request->{"connection"} =~ /close/i)
                {printf("Connection to be closed!\n"); return "END";}
        }

    }
    return "MORE_DATA";
}

extjs 在前端通过ajax发起请求,与后端进行交互,示例代码如下:

// 左侧导航历史结果列表数据
var historyresultStore = new Ext.data.JsonStore({
            // store configs
            autoDestroy : true,
            storeId : 'historyresultStore',
            proxy : {
                type : 'ajax',
                url : '/loadresultlist/',
                reader : {
                    type : 'json',
                    root : 'history',
                    idProperty : 'id'
                }
            },
            fields : ['name', 'time', 'url']
        });
// 左侧导航历史结果列表数据的容器
var historychart = Ext.create('Ext.grid.Panel', {
    title : '历史结果',
    id : 'historychart',
    store : Ext.data.StoreManager.lookup('historyresultStore'),
    columns : [{
                header:'',
                xtype:'rownumberer',
                align:'center'
            },{
                text : '名称',
                dataIndex : 'name',
                align:'center'
            },{
                text : '时间',
                dataIndex : 'time',
                align:'center'
            }],
    width : "100%",
    forceFit : true,
    listeners : {
        'itemcontextmenu' : function(view, record, item, index, e, eOpts) {
            // 禁用浏览器的右键相应事件
            e.preventDefault();
            e.stopEvent();
            // var nodename = record.raw.name;
            delete_menu.showAt(e.getXY());
        }
    },
    viewConfig : {
        listeners : {
            scope : this,
            itemdblclick : function(view, record, item, index, e) {
                var filename = record.data.url;
                Ext.Ajax.request({
                    url : "/getrunlog/",
                    method : 'get',
                    params : {
                        name : filename
                    },
                    success : function(response, opts) {
                        // 清空tabs内容
                        Ext.getCmp('tabs').removeAll();
                        if(Ext.getCmp('Onlynavigationpanel')!=undefined){Ext.getCmp('Onlynavigationpanel').destroy();}            
                        var testreport = Ext.create('Mytestreport');
                        Ext.getCmp('tabs').add(testreport);
                        Ext.getCmp('tabs').doLayout(true);
                        Ext.getCmp('testreport').body
                                .update('<iframe scrolling="auto" frameborder="0" width="100%" height="100%" src="../../testResult/'
                                        + filename
                                        + '/index.html"></iframe>');
                        // runlog.collapse();
                        testreport.expand();
                        // Ext.getCmp('runlog').body.update(response["responseText"]);
                    },
                    failure : function(response, opts) {
                        Ext.MessageBox.alert({
                                    title : '提示信息',
                                    msg : "运行日志获取失败!",
                                    buttons : Ext.Msg.OK
                                });
                    }
                });

            }
        }
    }

});
// console.log(historyresultStore);
historyresultStore.load();