当前位置:编程学习 > 网站相关 >>

异步编程经典 读skipfish源码

 

skipfish是google开源出来的一款安全检测工具(google真是好啊,开源了好多东东)。作者是个geek,写起程序来相当精干老道,项目主页上列举其特点之一:

High speed: pure C code, highly optimized HTTP handling, minimal CPU footprint – easily achieving 2000 requests per second with responsive targets.

一个单线程的程序如何能达到这个性能我们就来分析一下源码吧。

先从高处俯瞰一下整个程序:

整个程序分为一下几个模块:

http模块(http_client.c),数据管理模块(database.c),爬虫兼攻击模块(crawler.c,易做图ysis.c)和报表模块(report.c)。

其中http模块主要负责http回话的的处理,包括url解析,请求的发送,响应的解析。这个模块没用三方库来处理,所有功能都自己实现,相当牛,也相当高效。

数据管理模块主要是管理爬虫需要的站点树和检查出的错误,不多说。

爬虫兼攻击模块负责在url里插入攻击向量,以及html解析和攻击结果的检查。

报表模块是最后生成网页报表的模块,就是把数据模块里的数据输出,不解释。

好,我们来仔细分析http模块和攻击模块。

我们仔细想下整个程序的性能问题就可以发现,攻击往往是顺序进行的。而网页的下载却有快有慢(更内容多少,即时网速都有关)。所以,如果将http的处理逻辑串到攻击逻辑里面必然会造成一会儿网卡忙cpu闲,一会儿cpu闲而网卡忙。这个问题在我前面一篇文章讨论过。解决方法毫无疑问,异步化,下面我们就来看看这两个逻辑是如何异步交互的。

从攻击的角度来看:一个url需要往往都要经过至少以下一些检查步骤:

1.url的类型检查

2.xss测试

3.sql注入测试

从http的角度来看,一个url意味着建立连接,发送请求,接收响应。

从http入手,如何将网卡的性能发挥到100%,并发!skipfish的并发采用了最简单的poll,你可能疑问为什么不用epoll,答案很简单,并发量不多(skipfish默认并发是40个连接),因为http的请求主要是下载,所以一个连接需要下载很多东西,几十个连接流量已经不小了。这不是关键,不多说了。整个skipfish的主循环也在http模块中:u32 next_from_queue(void)。在main函数中有如下代码:

   while ((next_from_queue() && !stop_soon) || (!show_once++)) {

 

 

}

很显然,这个函数就是这个程序的引擎。函数的意思也很明了:http请求队列里是否还有请求。这里引出了很重要的东西:请求队列。代码如下:

/* Request queue descriptor: */

 

struct queue_entry {

  struct http_request*  req;    /* Request descriptor           */

  struct http_response* res;    /* Response descriptor          */

  struct conn_entry*    c;      /* Connection currently used    */

  struct queue_entry*   prev;   /* Previous queue entry         */

  struct queue_entry*   next;   /* Next queue entry             */

  u8 retrying;                  /* Request being retried?       */

};

 

static struct queue_entry* queue;

代码很简单,一个双向链表。里面存放着请求响应对,和关联的网络连接。整个http模块所干的事情,基本也就是next_from_queue函数干的事情,说得简单一点,就是从网卡上取数据往请求队列里面的响应体里面填。填完之后呢?看代码,在next_from_queue函数里:

if (!p)

  p = __DFL_ck_alloc(sizeof(struct pollfd) * max_connections);

 

while (c) {

  p[i].fd = c->fd;

  p[i].events = POLLIN | POLLERR | POLLHUP;

  if (c->write_len - c->write_off || c->SSL_rd_w_wr)

    p[i].events |= POLLOUT;

  p[i].revents = 0;

  c = c->next;

  i++;

}

 

poll(p, conn_cur, 100);

这里填充poll相关的结构体并调用poll,这是整个程序除了gethostbyname以外唯一会阻塞的地方(没流量啥都干不了当然阻塞啦)。下面开始处理所有连接上的数据,代码被我删掉很多,仅留下了关键点。

c = conn;

 

  for (i=0;i<conn_cur;i++) {

 

    if (p[i].revents & (POLLERR|POLLHUP)) {

 

     if (c->q) {

 

        if (c->write_len - c->write_off || !c->read_len) {

          c->q->res->state = STATE_CONNERR;

          keep = c->q->req->callback(c->q->req, c->q->res);

          req_errors_net++;

          req_errors_cur++;

        } else {

          if (parse_response(c->q->req, c->q->res, c->read_buf,

                             c->read_len, 0) != 2) {

            c->q->res->state = STATE_OK;

            keep = c->q->req->callback(c->q->req, c->q->res);

            if (req_errors_cur <= max_fail)

              req_errors_cur = 0;

          } else {

            c->q->res->state = STATE_CONNERR;

            keep = c->q->req->callback(c->q->req, c->q->res);

            req_errors_net++;

            req_errors_cur++;

          }

        }

 

      }

 

      destroy_unlink_conn(c, keep);

 

    } else

以上就是当连接错误或者连接关闭的时候的处理,我们看到,这里主要做的事情就是调用parse_response来解析已获得的数据,如果解析没问题,说明网页已经传好了,就调用回调函数req->callback(c->q->req, c->q->res);,我们会经常看到这样的调用,而callback是http_request结构体里面的一个函数指针,它的值在请求发出之前就已经设置好了。我们后面再看。

if (((p[i].revents & POLLIN) && !c->SSL_wr_w_rd) ||

    ((p[i].revents & POLLOUT) && c->SSL_rd_w_wr)) {

 

  if (c->q) {

 

    if (c->proto == PROTO_HTTPS) {

 

      read_res = SSL_read(c->srv_ssl, c->read_buf + c->read_len,

                          READ_CHUNK);

 

    } else {

      read_res = read(c->fd, c->read_buf + c->read_len, READ_CHUNK);

      if (read_res <= 0) goto network_error;

    }

 

    c->read_buf[c

补充:综合编程 , 安全编程 ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,