wifidog源码分析 - 用户连接过程
引言
之前的文章已经描述wifidog大概的一个工作流程,这里我们具体说说wifidog是怎么把一个新用户重定向到认证服务器中的,它又是怎么对一个已认证的用户实行放行操作的。我们已经知道wifidog在启动时会删除iptables中mangle、nat、filter表中的所有规则,并在这三个表中添加wifidog自己的规则,其规则简单来说就是将网关80端口重定向到指定端口(默认为2060),禁止非认证用户连接外网(除认证服务器外)。当有新用户连接路由器上网时,wifidog会通过监听2060端口获取新用户的http报文,通过报文即可知道报文是否携带token进行认证,如果没有token,wifidog会返回一个重定向的http报文给新用户,用户则会跳转到认证服务器进行认证,当认证成功后,认真服务器又会对用户重定向到网关,并在重定向报文中添加关键路径"/wifidog/auth"和token,wifidog重新获取到用户的http报文,检测到包含关键路径"/wifidog/auth"后,会通过认证服务器验证token是否有效,有效则修改iptables放行此客户端。
main_loop
此函数几乎相当于我们写程序的main函数,主要功能都是在这里面实现的,函数主要实现了主循环,并启动了三个线程,这三个线程的功能具体见wifidog源码分析 - wifidog原理,在此函数最后主循环中会等待用户连接,新用户只要通过浏览器打开非认证服务器网页时主循环就会监听到,监听到后会启动一个处理线程。其流程为
- 设置程序启动时间
- 获取网关信息
- 绑定http端口(80重定向到了2060)
- 设置关键路径和404错误的回调函数
- 重新建立iptables规则
- 启动客户端检测线程 (稍后文章分析)
- 启动wdctrl交互线程 (稍后文章分析)
- 认证服务器心跳检测线程 (稍后文章分析)
- 循环等待用户http请求,为每个请求启动一个处理线程。(代码片段1.2 代码片段1.3 代码片段1.4)
代码片段1.1
static void
main_loop(void)
{
int result;
pthread_t tid;
s_config *config = config_get_config();
request *r;
void **params; /* 设置启动时间 */
if (!started_time) {
debug(LOG_INFO, "Setting started_time");
started_time = time(NULL);
}
else if (started_time < MINIMUM_STARTED_TIME) {
debug(LOG_WARNING, "Detected possible clock skew - re-setting started_time");
started_time = time(NULL);
} /* 获取网关IP,失败退出程序 */
if (!config->gw_address) {
debug(LOG_DEBUG, "Finding IP address of %s", config->gw_interface);
if ((config->gw_address = get_iface_ip(config->gw_interface)) == NULL) {
debug(LOG_ERR, "Could not get IP address information of %s, exiting...", config->gw_interface);
exit();
}
debug(LOG_DEBUG, "%s = %s", config->gw_interface, config->gw_address);
} /* 获取网关ID,失败退出程序 */
if (!config->gw_id) {
debug(LOG_DEBUG, "Finding MAC address of %s", config->gw_interface);
if ((config->gw_id = get_iface_mac(config->gw_interface)) == NULL) {
debug(LOG_ERR, "Could not get MAC address information of %s, exiting...", config->gw_interface);
exit();
}
debug(LOG_DEBUG, "%s = %s", config->gw_interface, config->gw_id);
} /* 初始化监听网关2060端口的socket */
debug(LOG_NOTICE, "Creating web server on %s:%d", config->gw_address, config->gw_port); if ((webserver = httpdCreate(config->gw_address, config->gw_port)) == NULL) {
debug(LOG_ERR, "Could not create web server: %s", strerror(errno));
exit();
} debug(LOG_DEBUG, "Assigning callbacks to web server");
/* 设置关键路径及其回调函数,在代码片段1.2中会使用到 */
httpdAddCContent(webserver, "/", "wifidog", , NULL, http_callback_wifidog);
httpdAddCContent(webserver, "/wifidog", "", , NULL, http_callback_wifidog);
httpdAddCContent(webserver, "/wifidog", "about", , NULL, http_callback_about);
httpdAddCContent(webserver, "/wifidog", "status", , NULL, http_callback_status);
httpdAddCContent(webserver, "/wifidog", "auth", , NULL, http_callback_auth);
/* 设置404错误回调函数,在里面实现了重定向至认证服务器 */
httpdAddC404Content(webserver, http_callback_404); /* 清除iptables规则 */
fw_destroy();
/* 重新设置iptables规则 */
if (!fw_init()) {
debug(LOG_ERR, "FATAL: Failed to initialize firewall");
exit();
} /* 客户端检测线程 */
result = pthread_create(&tid_fw_counter, NULL, (void *)thread_client_timeout_check, NULL);
if (result != ) {
debug(LOG_ERR, "FATAL: Failed to create a new thread (fw_counter) - exiting");
termination_handler();
}
pthread_detach(tid_fw_counter); /* wdctrl交互线程 */
result = pthread_create(&tid, NULL, (void *)thread_wdctl, (void *)safe_strdup(config->wdctl_sock));
if (result != ) {
debug(LOG_ERR, "FATAL: Failed to create a new thread (wdctl) - exiting");
termination_handler();
}
pthread_detach(tid); /* 认证服务器心跳检测线程 */
result = pthread_create(&tid_ping, NULL, (void *)thread_ping, NULL);
if (result != ) {
debug(LOG_ERR, "FATAL: Failed to create a new thread (ping) - exiting");
termination_handler();
}
pthread_detach(tid_ping); debug(LOG_NOTICE, "Waiting for connections");
while() {
/* 监听2060端口等待用户http请求 */
r = httpdGetConnection(webserver, NULL); /* 错误处理 */
if (webserver->lastError == -) {
/* Interrupted system call */
continue; /* restart loop */
}
else if (webserver->lastError < -) {
debug(LOG_ERR, "FATAL: httpdGetConnection returned unexpected value %d, exiting.", webserver->lastError);
termination_handler();
}
else if (r != NULL) {
/* 用户http请求接收成功 */
debug(LOG_INFO, "Received connection from %s, spawning worker thread", r->clientAddr);
params = safe_malloc( * sizeof(void *));
*params = webserver;
*(params + ) = r; /* 开启http请求处理线程 */
result = pthread_create(&tid, NULL, (void *)thread_httpd, (void *)params);
if (result != ) {
debug(LOG_ERR, "FATAL: Failed to create a new thread (httpd) - exiting");
termination_handler();
}
pthread_detach(tid);
}
else {
;
}
} /* never reached */
}
用户连接启动线程(void thread_httpd(void * args))
代码片段1.2
此段代码是当有新用户(未认证的用户 代码片段1.3,已在认证服务器上认证但没有在wifidog认证的用户 代码片段1.4)连接时创建的线程,其主要功能为
- 获取用户浏览器发送过来的http报头
- 分析http报头,分析是否包含关键路径
- 不包含关键路径则调用404回调函数
- 包含关键路径则执行关键路径回调函数(这里主要讲解"/wifidog/auth"路径)
void
thread_httpd(void *args)
{
void **params;
httpd *webserver;
request *r; params = (void **)args;
webserver = *params;
r = *(params + );
free(params); /* 获取http报文 */
if (httpdReadRequest(webserver, r) == ) {
debug(LOG_DEBUG, "Processing request from %s", r->clientAddr);
debug(LOG_DEBUG, "Calling httpdProcessRequest() for %s", r->clientAddr);
/* 分析http报文 */
httpdProcessRequest(webserver, r);
debug(LOG_DEBUG, "Returned from httpdProcessRequest() for %s", r->clientAddr);
}
else {
debug(LOG_DEBUG, "No valid request received from %s", r->clientAddr);
}
debug(LOG_DEBUG, "Closing connection with %s", r->clientAddr);
httpdEndRequest(r);
} /* 被thread_httpd调用 */
void httpdProcessRequest(httpd *server, request *r)
{
char dirName[HTTP_MAX_URL],
entryName[HTTP_MAX_URL],
*cp;
httpDir *dir;
httpContent *entry; r->response.responseLength = ;
strncpy(dirName, httpdRequestPath(r), HTTP_MAX_URL);
dirName[HTTP_MAX_URL-]=;
cp = rindex(dirName, '/');
if (cp == NULL)
{
printf("Invalid request path '%s'\n",dirName);
return;
}
strncpy(entryName, cp + , HTTP_MAX_URL);
entryName[HTTP_MAX_URL-]=;
if (cp != dirName)
*cp = ;
else
*(cp+) = ; /* 获取http报文中的关键路径,在main_loop中已经设置 */
dir = _httpd_findContentDir(server, dirName, HTTP_FALSE);
if (dir == NULL)
{
/* http报文中未包含关键路径,执行404回调函数(在404回调函数中新用户被重定向到认证服务器),见代码片段1.3 */
_httpd_send404(server, r);
_httpd_writeAccessLog(server, r);
return;
}
/* 获取关键路径内容描述符 */
entry = _httpd_findContentEntry(r, dir, entryName);
if (entry == NULL)
{
_httpd_send404(server, r);
_httpd_writeAccessLog(server, r);
return;
}
if (entry->preload)
{
if ((entry->preload)(server) < )
{
_httpd_writeAccessLog(server, r);
return;
}
}
switch(entry->type)
{
case HTTP_C_FUNCT:
case HTTP_C_WILDCARD:
/* 如果是被认证服务器重定向到网关的用户,此处的关键路径为"/wifidog/auth",并执行回调函数 */
(entry->function)(server, r);
break; case HTTP_STATIC:
_httpd_sendStatic(server, r, entry->data);
break; case HTTP_FILE:
_httpd_sendFile(server, r, entry->path);
break; case HTTP_WILDCARD:
if (_httpd_sendDirectoryEntry(server, r, entry,
entryName)<)
{
_httpd_send404(server, r);
}
break;
}
_httpd_writeAccessLog(server, r);
}
代码片段1.3
此段代码表示wifidog是如何通过http 404回调函数实现客户端重定向了,实际上就是在404回调函数中封装了一个307状态的http报头,http的307状态在http协议中就是用于重定向的,封装完成后通过已经与客户端连接的socket返回给客户端。步骤流程为
- 判断本机是否处于离线状态
- 判断认证服务器是否在线
- 封装http 307报文
- 发送于目标客户端
void
http_callback_404(httpd *webserver, request *r)
{
char tmp_url[MAX_BUF],
*url;
s_config *config = config_get_config();
t_auth_serv *auth_server = get_auth_server(); memset(tmp_url, , sizeof(tmp_url)); snprintf(tmp_url, (sizeof(tmp_url) - ), "http://%s%s%s%s",
r->request.host,
r->request.path,
r->request.query[] ? "?" : "",
r->request.query);
url = httpdUrlEncode(tmp_url); if (!is_online()) {
/* 本机处于离线状态,此函数调用结果由认证服务器检测线程设置 */
char * buf;
safe_asprintf(&buf,
"<p>We apologize, but it seems that the internet connection that powers this hotspot is temporarily unavailable.</p>"
"<p>If at all possible, please notify the owners of this hotspot that the internet connection is out of service.</p>"
"<p>The maintainers of this network are aware of this disruption. We hope that this situation will be resolved soon.</p>"
"<p>In a while please <a href='%s'>click here</a> to try your request again.</p>", tmp_url); send_http_page(r, "Uh oh! Internet access unavailable!", buf);
free(buf);
debug(LOG_INFO, "Sent %s an apology since I am not online - no point sending them to auth server", r->clientAddr);
}
else if (!is_auth_online()) {
/* 认证服务器处于离线状态 */
char * buf;
safe_asprintf(&buf,
"<p>We apologize, but it seems that we are currently unable to re-direct you to the login screen.</p>"
"<p>The maintainers of this network are aware of this disruption. We hope that this situation will be resolved soon.</p>"
"<p>In a couple of minutes please <a href='%s'>click here</a> to try your request again.</p>", tmp_url); send_http_page(r, "Uh oh! Login screen unavailable!", buf);
free(buf);
debug(LOG_INFO, "Sent %s an apology since auth server not online - no point sending them to auth server", r->clientAddr);
}
else {
/* 本机与认证服务器都在线,返回重定向包于客户端 */
char *urlFragment;
safe_asprintf(&urlFragment, "%sgw_address=%s&gw_port=%d&gw_id=%s&url=%s",
auth_server->authserv_login_script_path_fragment,
config->gw_address,
config->gw_port,
config->gw_id,
url);
debug(LOG_INFO, "Captured %s requesting [%s] and re-directing them to login page", r->clientAddr, url);
http_send_redirect_to_auth(r, urlFragment, "Redirect to login page"); /* 实际上此函数中通过socket返回一个307状态的http报头给客户端,里面包含有认证服务器地址 */
free(urlFragment);
}
free(url);
}
代码片段1.4
此段表明当客户端已经在认证服务器确认登陆,认证服务器将客户端重新重定向回网关,并在重定向包中包含关键路径"/wifidog/auth"和token,认证服务器所执行的操作。
void
http_callback_auth(httpd *webserver, request *r)
{
t_client *client;
httpVar * token;
char *mac;
/* 判断http报文是否包含登出logout */
httpVar *logout = httpdGetVariableByName(r, "logout");
if ((token = httpdGetVariableByName(r, "token"))) {
/* 获取http报文中的token */
if (!(mac = arp_get(r->clientAddr))) {
/* 获取客户端mac地址失败 */
debug(LOG_ERR, "Failed to retrieve MAC address for ip %s", r->clientAddr);
send_http_page(r, "WiFiDog Error", "Failed to retrieve your MAC address");
} else {
LOCK_CLIENT_LIST();
/* 判断客户端是否存在于列表中 */
if ((client = client_list_find(r->clientAddr, mac)) == NULL) {
debug(LOG_DEBUG, "New client for %s", r->clientAddr);
/* 将此客户端添加到客户端列表 */
client_list_append(r->clientAddr, mac, token->value);
} else if (logout) {
/* http报文为登出 */
t_authresponse authresponse;
s_config *config = config_get_config();
unsigned long long incoming = client->counters.incoming;
unsigned long long outgoing = client->counters.outgoing;
char *ip = safe_strdup(client->ip);
char *urlFragment = NULL;
t_auth_serv *auth_server = get_auth_server();
/* 修改iptables禁止客户端访问外网 */
fw_deny(client->ip, client->mac, client->fw_connection_state);
/* 从客户端列表中删除此客户端 */
client_list_delete(client);
debug(LOG_DEBUG, "Got logout from %s", client->ip); if (config->auth_servers != NULL) {
UNLOCK_CLIENT_LIST();
/* 发送登出认证包给认证服务器 */
auth_server_request(&authresponse, REQUEST_TYPE_LOGOUT, ip, mac, token->value,
incoming, outgoing);
LOCK_CLIENT_LIST(); /* 将客户端重定向到认证服务器 */
debug(LOG_INFO, "Got manual logout from client ip %s, mac %s, token %s"
"- redirecting them to logout message", client->ip, client->mac, client->token);
safe_asprintf(&urlFragment, "%smessage=%s",
auth_server->authserv_msg_script_path_fragment,
GATEWAY_MESSAGE_ACCOUNT_LOGGED_OUT
);
http_send_redirect_to_auth(r, urlFragment, "Redirect to logout message");
free(urlFragment);
}
free(ip);
}
else {
debug(LOG_DEBUG, "Client for %s is already in the client list", client->ip);
}
UNLOCK_CLIENT_LIST();
if (!logout) {
/* 通过认证服务器认证此客户端token */
authenticate_client(r);
}
free(mac);
}
} else {
send_http_page(r, "WiFiDog error", "Invalid token");
}
} /* 此函数用于提交token到认证服务器进行认证 */
void
authenticate_client(request *r)
{
t_client *client;
t_authresponse auth_response;
char *mac,
*token;
char *urlFragment = NULL;
s_config *config = NULL;
t_auth_serv *auth_server = NULL; LOCK_CLIENT_LIST(); client = client_list_find_by_ip(r->clientAddr);
/* 判断此客户端是否在列表中 */
if (client == NULL) {
debug(LOG_ERR, "Could not find client for %s", r->clientAddr);
UNLOCK_CLIENT_LIST();
return;
} mac = safe_strdup(client->mac);
token = safe_strdup(client->token); UNLOCK_CLIENT_LIST(); /* 提交token、客户端ip、客户端mac至认证服务器 */
auth_server_request(&auth_response, REQUEST_TYPE_LOGIN, r->clientAddr, mac, token, , ); LOCK_CLIENT_LIST(); /*再次判断客户端是否存在于列表中,保险起见,因为有可能在于认证服务器认证过程中,客户端检测线程把此客户端下线 */
client = client_list_find(r->clientAddr, mac); if (client == NULL) {
debug(LOG_ERR, "Could not find client node for %s (%s)", r->clientAddr, mac);
UNLOCK_CLIENT_LIST();
free(token);
free(mac);
return;
} free(token);
free(mac); config = config_get_config();
auth_server = get_auth_server(); /* 判断认证服务器认证结果 */
switch(auth_response.authcode) { case AUTH_ERROR:
/* 认证错误 */
debug(LOG_ERR, "Got %d from central server authenticating token %s from %s at %s", auth_response, client->token, client->ip, client->mac);
send_http_page(r, "Error!", "Error: We did not get a valid answer from the central server");
break; case AUTH_DENIED:
/* 认证服务器拒绝此客户端 */
debug(LOG_INFO, "Got DENIED from central server authenticating token %s from %s at %s - redirecting them to denied message", client->token, client->ip, client->mac);
safe_asprintf(&urlFragment, "%smessage=%s",
auth_server->authserv_msg_script_path_fragment,
GATEWAY_MESSAGE_DENIED
);
http_send_redirect_to_auth(r, urlFragment, "Redirect to denied message");
free(urlFragment);
break; case AUTH_VALIDATION:
/* 认证服务器处于等待此客户端电子邮件确认回执状态 */
debug(LOG_INFO, "Got VALIDATION from central server authenticating token %s from %s at %s"
"- adding to firewall and redirecting them to activate message", client->token,
client->ip, client->mac);
client->fw_connection_state = FW_MARK_PROBATION;
fw_allow(client->ip, client->mac, FW_MARK_PROBATION);
safe_asprintf(&urlFragment, "%smessage=%s",
auth_server->authserv_msg_script_path_fragment,
GATEWAY_MESSAGE_ACTIVATE_ACCOUNT
);
http_send_redirect_to_auth(r, urlFragment, "Redirect to activate message");
free(urlFragment);
break; case AUTH_ALLOWED:
/* 认证通过 */
debug(LOG_INFO, "Got ALLOWED from central server authenticating token %s from %s at %s - "
"adding to firewall and redirecting them to portal", client->token, client->ip, client->mac);
client->fw_connection_state = FW_MARK_KNOWN;
fw_allow(client->ip, client->mac, FW_MARK_KNOWN);
served_this_session++;
safe_asprintf(&urlFragment, "%sgw_id=%s",
auth_server->authserv_portal_script_path_fragment,
config->gw_id
);
http_send_redirect_to_auth(r, urlFragment, "Redirect to portal");
free(urlFragment);
break; case AUTH_VALIDATION_FAILED:
/* 电子邮件确认回执超时 */
debug(LOG_INFO, "Got VALIDATION_FAILED from central server authenticating token %s from %s at %s "
"- redirecting them to failed_validation message", client->token, client->ip, client->mac);
safe_asprintf(&urlFragment, "%smessage=%s",
auth_server->authserv_msg_script_path_fragment,
GATEWAY_MESSAGE_ACCOUNT_VALIDATION_FAILED
);
http_send_redirect_to_auth(r, urlFragment, "Redirect to failed validation message");
free(urlFragment);
break; default:
debug(LOG_WARNING, "I don't know what the validation code %d means for token %s from %s at %s - sending error message", auth_response.authcode, client->token, client->ip, client->mac);
send_http_page(r, "Internal Error", "We can not validate your request at this time");
break; } UNLOCK_CLIENT_LIST();
return;
}
wifidog源码分析 - 用户连接过程的更多相关文章
- Dubbo 源码分析 - 服务调用过程
注: 本系列文章已捐赠给 Dubbo 社区,你也可以在 Dubbo 官方文档中阅读本系列文章. 1. 简介 在前面的文章中,我们分析了 Dubbo SPI.服务导出与引入.以及集群容错方面的代码.经过 ...
- wifidog源码分析 - wifidog原理 tiger
转:http://www.cnblogs.com/tolimit/p/4223644.html wifidog源码分析 - wifidog原理 wifidog是一个用于配合认证服务器实现无线网页认证功 ...
- SOFA 源码分析 —— 服务引用过程
前言 在前面的 SOFA 源码分析 -- 服务发布过程 文章中,我们分析了 SOFA 的服务发布过程,一个完整的 RPC 除了发布服务,当然还需要引用服务. So,今天就一起来看看 SOFA 是如何引 ...
- MyBatis 源码分析 - 配置文件解析过程
* 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...
- v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码
百篇博客分析|本篇为:(用户态锁篇) | 如何使用快锁Futex(上) 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁) ...
- 源码分析HotSpot GC过程(一)
«上一篇:源码分析HotSpot GC过程(一)»下一篇:源码分析HotSpot GC过程(三):TenuredGeneration的GC过程 https://blogs.msdn.microsoft ...
- 源码分析HotSpot GC过程(三):TenuredGeneration的GC过程
老年代TenuredGeneration所使用的垃圾回收算法是标记-压缩-清理算法.在回收阶段,将标记对象越过堆的空闲区移动到堆的另一端,所有被移动的对象的引用也会被更新指向新的位置.看起来像是把杂陈 ...
- 内核通信之Netlink源码分析-用户内核通信原理2
2017-07-05 上文以一个简单的案例描述了通过Netlink进行用户.内核通信的流程,本节针对流程中的各个要点进行深入分析 sock的创建 sock管理结构 sendmsg源码分析 sock的 ...
- SOFA 源码分析 —— 服务发布过程
前言 SOFA 包含了 RPC 框架,底层通信框架是 bolt ,基于 Netty 4,今天将通过 SOFA-RPC 源码中的例子,看看他是如何发布一个服务的. 示例代码 下面的代码在 com.ali ...
随机推荐
- DevExpress GridControl 使用方法技巧 总结 收录整理
一.如何解决单击记录整行选中的问题 View->OptionsBehavior->EditorShowMode 设置为:Click 二.如何新增一条记录 ().gridView.AddNe ...
- How to get Financial Dimension Value from Worker Position[AX2012]
To get financial dimension value from worker position, add a new method in hcmWorker Table with scri ...
- IOS应用程序生命周期
一.IOS应用的5种状态 Not Running(非运行状态) 应用没有运行或被系统终止. Inactive(前台非活动状态) 应用正在进入前台状态,但是还不能接受事件处理. Active(前台活动状 ...
- 多分类问题multicalss classification
多分类问题:有N个类别C1,C2,...,Cn,多分类学习的基本思路是"拆解法",即将多分类任务拆分为若干个而分类任务求解,最经典的拆分策略是:"一对一",&q ...
- 第十三章 调试及安全性(In .net4.5) 之 验证程序输入
1. 概述 本章介绍验证程序输入的重要性以及各种验证方法:Parse.TryParse.Convert.正则表达式.JavaScriptSerializer.XML Schemas. 2. 主要内容 ...
- python中的lambda
lambda表达式返回一个函数对象 例子: func = lambda x,y:x+y func相当于下面这个函数 def func(x,y): return x+y 注意def是语句而lambda是 ...
- Labview实现脉波调制( PDM )
Labview实现脉波调制( PDM ) 根据定义为脉冲宽度调制 生成一个正弦信号,得到其幅值输入给一个方波信号的占空比 由于方波信号的占空比里面含有正弦信号的信息 因此通过滤出方波信号的占空比信息则 ...
- 设置xx-net,访问youtube等国外网站
配合使用chrome+xx-net,就可以免费访问youtube等外网了.步骤如下: 1. 按照https://github.com/XX-net/XX-Net/wiki/%E4%BD%BF%E7%9 ...
- C Primer Plus学习笔记
1.汇编语言是特地的Cpu设计所采用的一组内部指令的助记符,不同的Cpu类型使用不同的Cpu C给予你更多的自由,也让你承担更多的风险 自由的代价是永远的警惕 2.目标代码文件.可执行文件和库 3.可 ...
- P3245: 最快路线
这道题其实还是不难的,只是自己搞混了=-=//晕,做了好久啊,其实就是个spfa,关键是存储路径搞昏了.输出格式要求太严了,航模不能有空格啊,所以因为格式WA了三次,哭啊/(ㄒoㄒ)/~~.贴上代码吧 ...