tinyhttpd-0.1.0_hacking

简介: 1 /**************************************************************************** 2 * 3 * tinyhttpd-0.
  1 /****************************************************************************
  2  *
  3  *                        tinyhttpd-0.1.0_hacking
  4  *
  5  * 1.这是tinyhttpd-0.1.0版本中httpd.c(主程序)的源码,源码不到500行(除去注释).
  6  * 2.通过分析、阅读该源码,可以一窥web服务器的大致工作机制.
  7  * 3.知识量:
  8  *     1.C语言;
  9  *     2.Unix或类Unix系统编程;
 10  *     3.微量的http协议(请求行、消息头、实体内容);
 11  *     4.如何阅读别人的代码( 从main函数开始 :) );
 12  * 4.tinyhttpd-0.1.0 文件结构如下:
 13  *     .
 14  *     |-- Makefile           -------->makefile 文件
 15  *     |-- README             -------->说明文档
 16  *     |-- htdocs             -------->程序会到该文件夹下找对应html、cgi文件
 17  *     |   |-- README         -------->说明文档
 18  *     |   |-- check.cgi      -------->cgi 程序
 19  *     |   |-- color.cgi      ----^
 20  *     |   `-- index.html     -------->默认的 web 首页文件
 21  *     |-- httpd.c            -------->你接下来要阅读的文件
 22  *     `-- simpleclient.c     -------->没发现该文件有任何用处 @_@
 23  * 5.如何阅读该文档:
 24  *     1.linux下使用vi/vim配和ctags,windows下使用Source Insight,当然你也
 25  *          可以用其他文本编辑器看.
 26  *     2.先找到main函数,然后就可以开始阅读了,遇到对应的函数,就去看对应的
 27  *          函数.
 28  *     3.对于有些函数,本人没有添加注释,或者说本人觉得没必要.
 29  *     4.祝您好运.  :)
 30  *
 31  * 6.tinyhttpd-0.1.0版本下载url: http://sourceforge.net/projects/tinyhttpd/
 32  * 
 33  * 如果您对本文有任何意见、提议,可以发邮件至zengjf42@163.com,会尽快回复.
 34  * 本文的最终解释权归本人(曾剑锋)所有,仅供学习、讨论.
 35  *
 36  *                                          2015-3-1 阴 深圳 尚观 Var
 37  *
 38  ***************************************************************************/
 39 
 40 
 41 /* J. David's webserver */
 42 /* This is a simple webserver.
 43  * Created November 1999 by J. David Blackstone.
 44  * CSE 4344 (Network concepts), Prof. Zeigler
 45  * University of Texas at Arlington
 46  */
 47 /* This program compiles for Sparc Solaris 2.6.
 48  * To compile for Linux:
 49  *  1) Comment out the #include <pthread.h> line.
 50  *  2) Comment out the line that defines the variable newthread.
 51  *  3) Comment out the two lines that run pthread_create().
 52  *  4) Uncomment the line that runs accept_request().
 53  *  5) Remove -lsocket from the Makefile.
 54  */
 55 #include <stdio.h>
 56 #include <sys/socket.h>
 57 #include <sys/types.h>
 58 #include <netinet/in.h>
 59 #include <arpa/inet.h>
 60 #include <unistd.h>
 61 #include <ctype.h>
 62 #include <strings.h>
 63 #include <string.h>
 64 #include <sys/stat.h>
 65 #include <pthread.h>
 66 #include <sys/wait.h>
 67 #include <stdlib.h>
 68 
 69 #define ISspace(x) isspace((int)(x))
 70 
 71 #define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"
 72 
 73 void accept_request(int);
 74 void bad_request(int);
 75 void cat(int, FILE *);
 76 void cannot_execute(int);
 77 void error_die(const char *);
 78 void execute_cgi(int, const char *, const char *, const char *);
 79 int  get_line(int, char *, int);
 80 void headers(int, const char *);
 81 void not_found(int);
 82 void serve_file(int, const char *);
 83 int  startup(u_short *);
 84 void unimplemented(int);
 85 
 86 /**
 87  * accept_request 函数说明:
 88  *     1.获取请求方式,目前只支持GET、POST请求;
 89  *     2.在本程序中所有的POST请求、带参数的GET请求都都被定义为访问cgi程序;
 90  *     3.从带参数的GET请求中分离出请求参数;
 91  *     4.如果没有指定需要访问的文件,使用index.html文件作为默认访问文件;
 92  *     5.检查需要访问的文件是否存在,以及其是否具有对应的权限;
 93  *     6.根据是否是cgi程序访问,来执行对应的任务.
 94  */
 95 void accept_request(int client)
 96 {
 97     /**
 98      * 局部变量说明:
 99      *     1.buf      : buffer缩写,主要用于暂存从socket中读出来的数据;
100      *     2.numchars : 用于保存每次从socket中读到的字符的个数;
101      *     3.method   : 用于保存请求方式,目前该软件只支持GET、POST这两种方式;
102      *     4.url      : 用于保存访问文件信息,有些地方叫uri;
103      *     5.path     : 用于保存文件路径;
104      *     6.i, j     : 处理数据时的下标;
105      *     7.st       : 在判断文件类型、是否存在的时候用到;
106      *     8.cgi      : 是否调用cgi程序的标志.
107      */
108     char buf[1024];
109     int  numchars;    
110     char method[255]; 
111     char url[255];    
112     char path[512];
113     size_t i, j;
114     struct stat st;
115     int cgi = 0;      /* becomes true if server decides this is a CGI
116                        * program */
117     char *query_string = NULL;
118     
119     /**
120      * 判断程序是否是GET、POST请求两种的其中一种,如果不是则报错.
121      */
122     numchars = get_line(client, buf, sizeof(buf));
123     i = 0; j = 0;
124     while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
125     {
126         method[i] = buf[j];
127         i++; j++;
128     }
129     method[i] = '\0';
130     
131     if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
132     {
133         unimplemented(client);
134         return;
135     }
136     
137     /**
138      * 该程序把POST请求定义为cgi请求.
139      */
140     if (strcasecmp(method, "POST") == 0)
141         cgi = 1;
142     
143     /**
144      * 获取当前url,这里的url不过括网址,而是除去网址之后的东西,
145      * 如浏览器中输入:http://127.0.0.1:8080/example/index.html
146      * 得到的url:/example/index.html
147      * 在有些地方不称这个为url,称之为uri
148      */
149     i = 0;
150     while (ISspace(buf[j]) && (j < sizeof(buf)))
151         j++;
152     while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
153     {
154         url[i] = buf[j];
155         i++; j++;
156     }
157     url[i] = '\0';
158 
159     /**
160      * 每次运行的时候都会出现2次这个,目前还不知道是什么原因导致的原因,
161      * 这是本人在源代码的基础上添加的调试输出.
162      * url: /favicon.ico
163      * url: /favicon.ico
164      */
165     printf("url: %s\n", url);
166     
167     /**
168      * 如果是GET请求,如果带了请求参数,那么也是cgi请求,并且从url中分离出请求参数
169      */
170     if (strcasecmp(method, "GET") == 0)
171     {
172         query_string = url;
173         while ((*query_string != '?') && (*query_string != '\0'))
174             query_string++;
175         if (*query_string == '?')
176         {
177             cgi = 1;
178             *query_string = '\0';
179             query_string++;
180         }
181     }
182     
183     /**
184      * 所有的需要的html文件、cgi程序都在htdocs文件夹中,
185      * 如果没有指定html文件,或者cgi程序,那么使用默认的index.html文件
186      * 作为目标输出文件.
187      */
188     sprintf(path, "htdocs%s", url);
189     if (path[strlen(path) - 1] == '/')
190         strcat(path, "index.html");
191     
192     /**
193      * 检查要访问的文件的状态,如:
194      *     1.是否存在;
195      *     2.是否是一个文件夹;
196      *     3.如果是cgi程序,是否用于对应的权限.
197      * 当然如果执行stat时就出错了,那么,直接将socket中的数据读完,
198      * 然后返回没有找到相关内容的信息提示.
199      */
200     if (stat(path, &st) == -1) {
201         while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
202             numchars = get_line(client, buf, sizeof(buf));
203         not_found(client);
204     }
205     else
206     {
207         if ((st.st_mode & S_IFMT) == S_IFDIR) /* 如果是一个文件夹 */
208             strcat(path, "/index.html");
209         if ((st.st_mode & S_IXUSR) ||
210             (st.st_mode & S_IXGRP) ||
211             (st.st_mode & S_IXOTH)    )  /* 权限问题 */
212             cgi = 1;
213         
214         /**
215          * 通过cgi变量来判断是执行cgi程序,还是仅仅是返回一个html页面.
216          */
217         if (!cgi)
218             serve_file(client, path); /* 向客户端返回一个html文件 */
219         else
220             execute_cgi(client, path, method, query_string); /* 执行一个cgi程序 */
221     }
222     
223     close(client);
224 }
225 
226 void bad_request(int client)
227 {
228     char buf[1024];
229 
230     sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
231     send(client, buf, sizeof(buf), 0);
232     sprintf(buf, "Content-type: text/html\r\n");
233     send(client, buf, sizeof(buf), 0);
234     sprintf(buf, "\r\n");
235     send(client, buf, sizeof(buf), 0);
236     sprintf(buf, "<P>Your browser sent a bad request, ");
237     send(client, buf, sizeof(buf), 0);
238     sprintf(buf, "such as a POST without a Content-Length.\r\n");
239     send(client, buf, sizeof(buf), 0);
240 }
241 
242 /**
243  * 主要完成将resource指向的文件内容拷贝输出到客户端浏览器中
244  */
245 void cat(int client, FILE *resource)
246 {
247     char buf[1024];
248 
249     fgets(buf, sizeof(buf), resource);
250     while (!feof(resource))
251     {
252         send(client, buf, strlen(buf), 0);
253         fgets(buf, sizeof(buf), resource);
254     }
255 }
256 
257 void cannot_execute(int client)
258 {
259     char buf[1024];
260 
261     sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
262     send(client, buf, strlen(buf), 0);
263     sprintf(buf, "Content-type: text/html\r\n");
264     send(client, buf, strlen(buf), 0);
265     sprintf(buf, "\r\n");
266     send(client, buf, strlen(buf), 0);
267     sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
268     send(client, buf, strlen(buf), 0);
269 }
270 
271 void error_die(const char *sc)
272 {
273     perror(sc);
274     exit(1);
275 }
276 
277 void execute_cgi(int client, const char *path,
278                  const char *method, const char *query_string)
279 {
280     /**
281      * 局部变量说明:
282      *     1.buf        : buffer缩写;
283      *     2.cgi_output : 用于保存输出管道的文件描述符;
284      *     3.cgi_input  : 用于保存输入管道的文件描述符;
285      *     4.pid        : 进程pid,最后父进程退出之前,等待子进程先退出,
286      *                  并回收相关的资源,这部分工作主要由waitpid()来完成;
287      *     5.status     : 在waitpid()中用于保存子进程的退出状态,本程序没有具体使用;
288      *     6.i          : 计数器;
289      *     7.c          : POST读取请求参数时,读取到的字符保存在这里;
290      *     8.numchars   : 读取的字符个数;
291      *     9.conten_length : 内容实体的字符数;
292      */
293     char buf[1024];
294     int cgi_output[2];
295     int cgi_input[2];
296     pid_t pid;
297     int status;
298     int i;
299     char c;
300     int numchars = 1;
301     int content_length = -1;
302 
303     /**
304      * 在本程序中,GET请求的消息头没有任何用处,直接处理掉就行了,
305      * 而如果是POST请求,需要的消息头中的获取实体的大小,也就是Content-Length:后面跟的数字
306      */
307     buf[0] = 'A'; buf[1] = '\0';
308     if (strcasecmp(method, "GET") == 0)
309         while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
310             numchars = get_line(client, buf, sizeof(buf));
311     else    /* POST */
312     {
313         numchars = get_line(client, buf, sizeof(buf));
314         while ((numchars > 0) && strcmp("\n", buf))
315         {
316             buf[15] = '\0';
317             if (strcasecmp(buf, "Content-Length:") == 0)
318                 content_length = atoi(&(buf[16]));
319             numchars = get_line(client, buf, sizeof(buf));
320         }
321         if (content_length == -1) {
322             bad_request(client);
323             return;
324         }
325     }
326 
327     /**
328      * 返回返回行信息.
329      */
330     sprintf(buf, "HTTP/1.0 200 OK\r\n");
331     send(client, buf, strlen(buf), 0);
332 
333     /**
334      * 父子进程通过管道通信.
335      */
336     if (pipe(cgi_output) < 0) {
337         cannot_execute(client);
338         return;
339     }
340     if (pipe(cgi_input) < 0) {
341         cannot_execute(client);
342         return;
343     }
344 
345     /**
346      * 创建子进程,用于执行cgi程序,父进程接受子进程的结果,并返回给浏览器
347      */
348     if ( (pid = fork()) < 0 ) {
349         cannot_execute(client);
350         return;
351     }
352     if (pid == 0)  /* child: CGI script */
353     {
354         char meth_env[255];   //cgi 请求方式环境变量
355         char query_env[255];  //cgi GET请求参数环境变量
356         char length_env[255]; //cgi POST请求参数内容大小环境变量
357 
358         /**
359          * 重定向标准输入输出,并设置好对应的环境变量.
360          */
361         dup2(cgi_output[1], 1);
362         dup2(cgi_input[0], 0);
363         close(cgi_output[0]);
364         close(cgi_input[1]);
365         sprintf(meth_env, "REQUEST_METHOD=%s", method);
366         putenv(meth_env);
367         if (strcasecmp(method, "GET") == 0) {
368             sprintf(query_env, "QUERY_STRING=%s", query_string);
369             putenv(query_env);
370         }
371         else {   /* POST */
372             sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
373             putenv(length_env);
374         }
375         /* 执行对应的程序 */
376         execl(path, path, NULL);
377         exit(0);
378     } else {    /* parent */
379         close(cgi_output[1]);
380         close(cgi_input[0]);
381         /**
382          * 对于POST请求,将实体中的请求参数通过管道传送到cgi程序中
383          */
384         if (strcasecmp(method, "POST") == 0)
385             for (i = 0; i < content_length; i++) {
386                 recv(client, &c, 1, 0);
387                 write(cgi_input[1], &c, 1);
388             }
389         /**
390          * 读取cgi程序的执行结果,返回给浏览器
391          */
392         while (read(cgi_output[0], &c, 1) > 0)
393             send(client, &c, 1, 0);
394 
395         close(cgi_output[0]);
396         close(cgi_input[1]);
397         /**
398          * 等待子进程运行结束,并回收子进程的资源,
399          * 防止出现孤儿进程
400          */
401         waitpid(pid, &status, 0);
402     }
403 }
404 
405 int get_line(int sock, char *buf, int size)
406 {
407     /**
408      * 局部变量说明:
409      *     1.i : 数组下标计数,不能大于size;
410      *     2.c : 每次读到的字符保存在这里面;
411      *     3.n : 每次读到的字符个数.
412      */
413     int i = 0;
414     char c = '\0';
415     int n;
416 
417     /**
418      * 一直读到buf满了,或者遇到了'\n'为止.
419      */
420     while ((i < size - 1) && (c != '\n'))
421     {
422         n = recv(sock, &c, 1, 0);
423         /* DEBUG printf("%02X\n", c); */
424         if (n > 0)
425         {
426             /**
427              * 读到'\r'也算是结束,通过判断后面有没有跟'\n'来判断是否要将下
428              * 一个字符取出来,并且无论'\r'后面跟不跟'\n',都将'\r'换成'\n'.
429              */
430             if (c == '\r')
431             {
432                 n = recv(sock, &c, 1, MSG_PEEK);
433                 /* DEBUG printf("%02X\n", c); */
434                 if ((n > 0) && (c == '\n'))
435                     recv(sock, &c, 1, 0);
436                 else
437                     c = '\n';
438             }
439             buf[i] = c;
440             i++;
441         }
442         else
443             c = '\n';
444     }
445     buf[i] = '\0'; /* 字符串结尾 */
446     
447     return(i);
448 }
449 
450 void headers(int client, const char *filename)
451 {
452     char buf[1024];
453     (void)filename;  /* could use filename to determine file type */
454 
455     strcpy(buf, "HTTP/1.0 200 OK\r\n");
456     send(client, buf, strlen(buf), 0);
457     strcpy(buf, SERVER_STRING);
458     send(client, buf, strlen(buf), 0);
459     sprintf(buf, "Content-Type: text/html\r\n");
460     send(client, buf, strlen(buf), 0);
461     strcpy(buf, "\r\n");
462     send(client, buf, strlen(buf), 0);
463 }
464 
465 void not_found(int client)
466 {
467     char buf[1024];
468 
469     sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
470     send(client, buf, strlen(buf), 0);
471     sprintf(buf, SERVER_STRING);
472     send(client, buf, strlen(buf), 0);
473     sprintf(buf, "Content-Type: text/html\r\n");
474     send(client, buf, strlen(buf), 0);
475     sprintf(buf, "\r\n");
476     send(client, buf, strlen(buf), 0);
477     sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
478     send(client, buf, strlen(buf), 0);
479     sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
480     send(client, buf, strlen(buf), 0);
481     sprintf(buf, "your request because the resource specified\r\n");
482     send(client, buf, strlen(buf), 0);
483     sprintf(buf, "is unavailable or nonexistent.\r\n");
484     send(client, buf, strlen(buf), 0);
485     sprintf(buf, "</BODY></HTML>\r\n");
486     send(client, buf, strlen(buf), 0);
487 }
488 
489 void serve_file(int client, const char *filename)
490 {
491     /**
492      * 局部变量说明:
493      *     1.resource : 打开的文件的文件指针;
494      *     2.numchars : 每次读到的字符个数;
495      *     3.buf      : buffer的缩写.
496      */
497     FILE *resource = NULL;
498     int numchars = 1;
499     char buf[1024];
500 
501     /**
502      * 在本程序中消息头对于纯GET请求没有什么用,直接读取丢掉.
503      */
504     buf[0] = 'A'; buf[1] = '\0';
505     while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
506         numchars = get_line(client, buf, sizeof(buf));
507 
508     resource = fopen(filename, "r");
509     if (resource == NULL)
510         not_found(client);
511     else
512     {
513         /* 发送消息头 */
514         headers(client, filename);
515         /* 发送内容实体 */
516         cat(client, resource);
517     }
518     fclose(resource);
519 }
520 
521 /**
522  * startup 函数完成内容:
523  *     1.获取一个作为服务器的socket;
524  *     2.绑定服务器端的socket;
525  *     3.通过判断参数port的值,确定是否需要动态分配端口号;
526  *     4.服务器开启监听;
527  *     5.返回服务器段的socket文件描述符.
528  */
529 int startup(u_short *port)
530 {
531     /**
532      * 局部变量说明:
533      *     1.httpd : 保存服务器socket描述符,并作为返回值返回;
534      *     2.name  : 用于保存服务器本身的socket信息,创建服务器.
535      */
536     int httpd = 0;
537     struct sockaddr_in name;
538 
539     httpd = socket(PF_INET, SOCK_STREAM, 0); 
540     if (httpd == -1)
541         error_die("socket");
542 
543     memset(&name, 0, sizeof(name));
544     name.sin_family = AF_INET;
545     name.sin_port = htons(*port);
546     name.sin_addr.s_addr = htonl(INADDR_ANY);
547 
548     if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
549         error_die("bind");
550 
551     if (*port == 0)  /* if dynamically allocating a port */
552     {
553         int namelen = sizeof(name);
554         if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
555             error_die("getsockname");
556         *port = ntohs(name.sin_port);
557     }
558 
559     if (listen(httpd, 5) < 0)
560         error_die("listen");
561     return(httpd);
562 }
563 
564 void unimplemented(int client)
565 {
566     char buf[1024];
567 
568     sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
569     send(client, buf, strlen(buf), 0);
570     sprintf(buf, SERVER_STRING);
571     send(client, buf, strlen(buf), 0);
572     sprintf(buf, "Content-Type: text/html\r\n");
573     send(client, buf, strlen(buf), 0);
574     sprintf(buf, "\r\n");
575     send(client, buf, strlen(buf), 0);
576     sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
577     send(client, buf, strlen(buf), 0);
578     sprintf(buf, "</TITLE></HEAD>\r\n");
579     send(client, buf, strlen(buf), 0);
580     sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
581     send(client, buf, strlen(buf), 0);
582     sprintf(buf, "</BODY></HTML>\r\n");
583     send(client, buf, strlen(buf), 0);
584 }
585 
586 /**********************************************************************/
587 
588 int main(void)
589 {
590     /**
591      * 局部变量说明:
592      *     1.server_sock : 服务器端的socket描述符;
593      *     2.port          : 服务器端的socket端口号,如果是0的,startup()将会采用
594      *                   自动生成的方式生成新的端口号供使用;
595      *     3.client_sock : 客户端连接进来产生的客户端socket描述符;
596      *     4.client_name : 用于保存客户端连接进来的socket信息;
597      *     5.client_name_len : struct sockaddr_in结构体的大小,在accpet的时候
598      *                   需要用到,这个参数必须传,否则会出错;
599      *     6.newthread   : 用于保存新创建的线程的ID.
600      */
601     int server_sock = -1; 
602     u_short port = 0;       
603     int client_sock = -1; 
604     struct sockaddr_in client_name; 
605     int client_name_len = sizeof(client_name);
606     pthread_t newthread;
607 
608     /**
609      * startup 函数完成内容:
610      *     1.获取一个作为服务器的socket;
611      *     2.帮定服务器断的sockt;
612      *     3.通过判断参数port的值,确定是否需要动态分配端口号;
613      *     4.服务器开启监听.
614      */
615     server_sock = startup(&port);
616     printf("httpd running on port %d\n", port);
617 
618     while (1)
619     {
620         /**
621          * 等待客户端的连接,使用client_name保存客户端socket信息,
622          * client_name_len是client_name对应结构体的长度.
623          */
624         client_sock = accept(server_sock,
625                              (struct sockaddr *)&client_name,
626                              &client_name_len);
627         if (client_sock == -1)
628             error_die("accept");
629         /**
630          * 创建一个新的线程来处理任务,并把客户端的socket描述符作为参数传给accept_request,
631          * accept_request 函数说明:
632          *     1.获取请求方式,目前只支持GET、POST请求;
633          *     2.在本程序中所有的POST请求、带参数的GET请求都都被定义为访问cgi程序;
634          *     3.从带参数的GET请求中分离出请求参数;
635          *     4.如果没有指定需要访问的文件,使用index.html文件作为默认访问文件;
636          *     5.检查需要访问的文件是否存在,以及其是否具有对应的权限;
637          *     6.根据是否是cgi程序访问,来执行对应的任务.
638          */
639         if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)
640             perror("pthread_create");
641     }
642 
643     /**
644      * 不知道为什么,这条语句在while外边,竟然会影响到程序的关闭 :(
645      * 这行代码注释掉才能连续访问,不注释,只能访问一次,所以直接注释了
646      * 反正程序停止都使用ctrl+c,不影响程序的运行.
647      */
648     //close(server_sock);
649 
650     return(0);
651 }

 

目录
相关文章
|
10月前
|
开发框架 安全 Java
Google Hacking的常用语法
Google Hacking的常用语法
100 0
|
存储 SQL 缓存
[工具使用]搜索引擎 Hacking(下)
[工具使用]搜索引擎 Hacking
225 0
 [工具使用]搜索引擎 Hacking(下)
|
SQL 搜索推荐 安全
[工具使用]搜索引擎 Hacking(上)
[工具使用]搜索引擎 Hacking
217 0
[工具使用]搜索引擎 Hacking(上)