Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
BaiXuePrincess
rt-thread
提交
bc632866
R
rt-thread
项目概览
BaiXuePrincess
/
rt-thread
与 Fork 源项目一致
Fork自
RT-Thread / rt-thread
通知
1
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
R
rt-thread
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
提交
bc632866
编写于
5月 14, 2015
作者:
B
Bernard Xiong
浏览文件
操作
浏览文件
下载
差异文件
Merge pull request #480 from weety/ftp
[FTP server]
上级
2f5c5b67
707a6577
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
229 addition
and
35 deletion
+229
-35
components/net/lwip/apps/ftpd.c
components/net/lwip/apps/ftpd.c
+229
-35
未找到文件。
components/net/lwip/apps/ftpd.c
浏览文件 @
bc632866
...
...
@@ -11,17 +11,24 @@
#define FTP_MAX_CONNECTION 2
#define FTP_USER "rtt"
#define FTP_PASSWORD "demo"
#define FTP_WELCOME_MSG "220
-= welcome on RT-Thread FTP server =-\r\n220
\r\n"
#define FTP_WELCOME_MSG "220
welcome on RT-Thread FTP server.
\r\n"
#define FTP_BUFFER_SIZE 1024
#define INET_ADDRSTRLEN 16
struct
ftp_session
{
rt_bool_t
is_anonymous
;
int
sockfd
;
struct
sockaddr_in
remote
;
struct
sockaddr_in
server
;
char
serveraddr
[
INET_ADDRSTRLEN
];
/* pasv data */
int
pasv_listen_sockfd
;
char
pasv_active
;
int
pasv_sockfd
;
...
...
@@ -43,6 +50,7 @@ struct ftp_session* ftp_new_session()
struct
ftp_session
*
session
;
session
=
(
struct
ftp_session
*
)
rt_malloc
(
sizeof
(
struct
ftp_session
));
rt_memset
((
void
*
)
session
,
0
,
sizeof
(
struct
ftp_session
));
session
->
next
=
session_list
;
session_list
=
session
;
...
...
@@ -71,6 +79,83 @@ void ftp_close_session(struct ftp_session* session)
rt_free
(
session
);
}
static
int
open_data_connection
(
struct
ftp_session
*
session
)
{
socklen_t
len
=
sizeof
(
struct
sockaddr
);
struct
sockaddr_in
sin
;
#if 0
/* Previous PORT command from client */
if (ctrl->data_address[0]) {
ctrl->data_sd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == ctrl->data_sd) {
printf("Failed creating data socket");
return -1;
}
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(ctrl->data_port);
inet_aton(ctrl->data_address, &(sin.sin_addr));
if (connect(ctrl->data_sd, (struct sockaddr *)&sin, len) == -1) {
printf("Failed connecting data socket to client");
close(ctrl->data_sd);
ctrl->data_sd = -1;
return -1;
}
DBG("Connected successfully to client's previously requested address:PORT %s:%d", ctrl->data_address, ctrl->data_port);
return 0;
}
#endif
/* Previous PASV command, accept connect from client */
if
(
session
->
pasv_listen_sockfd
>
0
)
{
char
client_ip
[
100
];
session
->
pasv_sockfd
=
accept
(
session
->
pasv_listen_sockfd
,
(
struct
sockaddr
*
)
&
sin
,
&
len
);
if
(
-
1
==
session
->
pasv_sockfd
)
{
printf
(
"Failed accepting connection from client"
);
return
-
1
;
}
len
=
sizeof
(
struct
sockaddr
);
if
(
-
1
==
getpeername
(
session
->
pasv_sockfd
,
(
struct
sockaddr
*
)
&
sin
,
&
len
))
{
printf
(
"Cannot determine client address"
);
closesocket
(
session
->
pasv_sockfd
);
session
->
pasv_sockfd
=
-
1
;
return
-
1
;
}
printf
(
"Client PASV data connection from %s
\n
"
,
inet_ntoa
(
sin
.
sin_addr
));
}
return
0
;
}
static
void
close_data_connection
(
struct
ftp_session
*
session
)
{
/* PASV server listening socket */
if
(
session
->
pasv_listen_sockfd
>
0
)
{
closesocket
(
session
->
pasv_listen_sockfd
);
session
->
pasv_listen_sockfd
=
-
1
;
}
/* PASV client socket */
if
(
session
->
pasv_sockfd
>
0
)
{
closesocket
(
session
->
pasv_sockfd
);
session
->
pasv_sockfd
=
-
1
;
}
#if 0
/* PORT */
if (ctrl->data_address[0]) {
ctrl->data_address[0] = 0;
ctrl->data_port = 0;
}
#endif
}
int
ftp_get_filesize
(
char
*
filename
)
{
int
pos
;
...
...
@@ -179,6 +264,10 @@ void ftpd_thread_entry(void* parameter)
session
=
ftp_new_session
();
if
(
session
!=
NULL
)
{
if
(
-
1
==
getsockname
(
com_socket
,
(
struct
sockaddr
*
)
&
session
->
server
,
&
addr_len
))
{
printf
(
"Cannot determine our address, need it if client should connect to us
\n
"
);
}
ipaddr_ntoa_r
(
&
(
session
->
server
.
sin_addr
),
session
->
serveraddr
,
sizeof
(
session
->
serveraddr
));
strcpy
(
session
->
currentdir
,
FTP_SRV_ROOT
);
session
->
sockfd
=
com_socket
;
session
->
remote
=
remote
;
...
...
@@ -201,6 +290,7 @@ void ftpd_thread_entry(void* parameter)
rt_kprintf
(
"Client %s disconnected
\n
"
,
inet_ntoa
(
session
->
remote
.
sin_addr
));
FD_CLR
(
session
->
sockfd
,
&
readfds
);
closesocket
(
session
->
sockfd
);
session
->
sockfd
=
-
1
;
ftp_close_session
(
session
);
}
else
...
...
@@ -210,6 +300,7 @@ void ftpd_thread_entry(void* parameter)
{
rt_kprintf
(
"Client %s disconnected
\r\n
"
,
inet_ntoa
(
session
->
remote
.
sin_addr
));
closesocket
(
session
->
sockfd
);
session
->
sockfd
=
-
1
;
ftp_close_session
(
session
);
}
}
...
...
@@ -359,7 +450,7 @@ int ftp_process_request(struct ftp_session* session, char *buf)
}
else
if
(
strcmp
(
parameter_ptr
,
FTP_USER
)
==
0
)
{
session
->
is_anonymous
=
RT_FALSE
;
session
->
is_anonymous
=
RT_FALSE
;
rt_sprintf
(
sbuf
,
"331 Password required for %s
\r\n
"
,
parameter_ptr
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
}
...
...
@@ -380,25 +471,26 @@ int ftp_process_request(struct ftp_session* session, char *buf)
session
->
is_anonymous
==
RT_TRUE
)
{
// password correct
rt_sprintf
(
sbuf
,
"230 User logged in
\r\n
"
);
rt_sprintf
(
sbuf
,
"230 User logged in
.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
rt_free
(
sbuf
);
return
0
;
}
// incorrect password
rt_sprintf
(
sbuf
,
"530 Login or Password incorrect. Bye!
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
rt_free
(
sbuf
);
return
-
1
;
}
else
if
(
str_begin_with
(
buf
,
"LIST"
)
==
0
)
{
memset
(
sbuf
,
0
,
FTP_BUFFER_SIZE
);
open_data_connection
(
session
);
rt_sprintf
(
sbuf
,
"150 Opening Binary mode connection for file list.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
do_list
(
session
->
currentdir
,
session
->
pasv_sockfd
);
close
socket
(
session
->
pasv_sockfd
);
close
_data_connection
(
session
);
session
->
pasv_active
=
0
;
rt_sprintf
(
sbuf
,
"226 Transfert Complete.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
...
...
@@ -408,8 +500,9 @@ int ftp_process_request(struct ftp_session* session, char *buf)
memset
(
sbuf
,
0
,
FTP_BUFFER_SIZE
);
rt_sprintf
(
sbuf
,
"150 Opening Binary mode connection for file list.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
open_data_connection
(
session
);
do_simple_list
(
session
->
currentdir
,
session
->
pasv_sockfd
);
close
socket
(
session
->
pasv_sockfd
);
close
_data_connection
(
session
);
session
->
pasv_active
=
0
;
rt_sprintf
(
sbuf
,
"226 Transfert Complete.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
...
...
@@ -436,8 +529,20 @@ int ftp_process_request(struct ftp_session* session, char *buf)
else
if
(
str_begin_with
(
buf
,
"PASV"
)
==
0
)
{
int
dig1
,
dig2
;
int
sockfd
;
char
optval
=
'1'
;
//int sockfd;
int
optval
=
1
;
int
port
;
struct
sockaddr_in
data
;
socklen_t
len
=
sizeof
(
struct
sockaddr
);
char
*
msg
,
*
p
;
if
(
session
->
pasv_sockfd
>
0
)
{
closesocket
(
session
->
pasv_sockfd
);
session
->
pasv_sockfd
=
-
1
;
}
if
(
session
->
pasv_listen_sockfd
>
0
)
closesocket
(
session
->
pasv_listen_sockfd
);
session
->
pasv_port
=
10000
;
session
->
pasv_active
=
1
;
...
...
@@ -448,40 +553,63 @@ int ftp_process_request(struct ftp_session* session, char *buf)
dig2
=
session
->
pasv_port
%
256
;
FD_ZERO
(
&
readfds
);
if
((
sockfd
=
socket
(
PF_INET
,
SOCK_STREAM
,
0
))
==-
1
)
if
((
s
ession
->
pasv_listen_s
ockfd
=
socket
(
PF_INET
,
SOCK_STREAM
,
0
))
==-
1
)
{
rt_sprintf
(
sbuf
,
"425 Can't open data connection.
\r\n
"
);
rt_sprintf
(
sbuf
,
"425 Can't open data connection
0
.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
goto
err1
;
}
if
(
setsockopt
(
sockfd
,
SOL_SOCKET
,
SO_REUSEADDR
,
&
optval
,
sizeof
(
optval
))
==-
1
)
if
(
setsockopt
(
s
ession
->
pasv_listen_s
ockfd
,
SOL_SOCKET
,
SO_REUSEADDR
,
&
optval
,
sizeof
(
optval
))
==-
1
)
{
rt_sprintf
(
sbuf
,
"425 Can't open data connection.
\r\n
"
);
rt_sprintf
(
sbuf
,
"425 Can't open data connection
1
.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
goto
err1
;
}
if
(
bind
(
sockfd
,
(
struct
sockaddr
*
)
&
local
,
addr_len
)
==-
1
)
if
(
bind
(
s
ession
->
pasv_listen_s
ockfd
,
(
struct
sockaddr
*
)
&
local
,
addr_len
)
==-
1
)
{
rt_sprintf
(
sbuf
,
"425 Can't open data connection.
\r\n
"
);
rt_sprintf
(
sbuf
,
"425 Can't open data connection
2
.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
goto
err1
;
}
if
(
listen
(
sockfd
,
1
)
==-
1
)
if
(
listen
(
s
ession
->
pasv_listen_s
ockfd
,
1
)
==-
1
)
{
rt_sprintf
(
sbuf
,
"425 Can't open data connection.
\r\n
"
);
rt_sprintf
(
sbuf
,
"425 Can't open data connection3.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
goto
err1
;
}
if
(
-
1
==
getsockname
(
session
->
pasv_listen_sockfd
,
(
struct
sockaddr
*
)
&
data
,
&
len
))
{
rt_kprintf
(
"Cannot determine our address, need it if client should connect to us
\n
"
);
goto
err1
;
}
port
=
ntohs
(
data
.
sin_port
);
rt_kprintf
(
"Port %d
\n
"
,
port
);
/* Convert server IP address and port to comma separated list */
msg
=
strdup
(
session
->
serveraddr
);
if
(
!
msg
)
{
rt_sprintf
(
sbuf
,
"426 Internal server error.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
goto
err1
;
}
p
=
msg
;
while
((
p
=
strchr
(
p
,
'.'
)))
*
p
++
=
','
;
rt_kprintf
(
"Listening %d seconds @ port %d
\n
"
,
tv
.
tv_sec
,
session
->
pasv_port
);
rt_sprintf
(
sbuf
,
"227 Entering passive mode (%
d,%d,%d,%d,%d,%d)
\r\n
"
,
127
,
0
,
0
,
1
,
dig1
,
dig2
);
rt_sprintf
(
sbuf
,
"227 Entering passive mode (%
s,%d,%d)
\r\n
"
,
msg
,
port
/
256
,
port
%
256
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
rt_free
(
msg
);
return
0
;
#if 0
FD_SET(sockfd, &readfds);
select(0, &readfds, 0, 0, &tv);
if(FD_ISSET(sockfd, &readfds))
{
if((session->pasv_sockfd = accept(sockfd, (struct sockaddr*)&pasvremote, &addr_len))==-1)
{
rt_sprintf
(
sbuf
,
"425 Can't open data connection.
\r\n
"
);
rt_sprintf(sbuf, "425 Can't open data connection
4
.\r\n");
send(session->sockfd, sbuf, strlen(sbuf), 0);
goto err1;
}
...
...
@@ -500,11 +628,21 @@ err1:
rt_free(sbuf);
return 0;
}
#endif
err1:
close_data_connection
(
session
);
session
->
pasv_active
=
0
;
rt_free
(
sbuf
);
rt_free
(
msg
);
return
0
;
}
else
if
(
str_begin_with
(
buf
,
"RETR"
)
==
0
)
{
int
file_size
;
open_data_connection
(
session
);
strcpy
(
filename
,
buf
+
5
);
build_full_path
(
session
,
parameter_ptr
,
filename
,
256
);
...
...
@@ -514,13 +652,15 @@ err1:
rt_sprintf
(
sbuf
,
"550
\"
%s
\"
: not a regular file
\r\n
"
,
filename
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
session
->
offset
=
0
;
rt_free
(
sbuf
);
close_data_connection
(
session
);
rt_free
(
sbuf
);
return
0
;
}
fd
=
open
(
filename
,
O_RDONLY
,
0
);
if
(
fd
<
0
)
{
close_data_connection
(
session
);
rt_free
(
sbuf
);
return
0
;
}
...
...
@@ -543,14 +683,16 @@ err1:
rt_sprintf
(
sbuf
,
"226 Finished.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
close
(
fd
);
close
socket
(
session
->
pasv_sockfd
);
close
_data_connection
(
session
);
}
else
if
(
str_begin_with
(
buf
,
"STOR"
)
==
0
)
{
open_data_connection
(
session
);
if
(
session
->
is_anonymous
==
RT_TRUE
)
{
rt_sprintf
(
sbuf
,
"550 Permission denied.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
close_data_connection
(
session
);
rt_free
(
sbuf
);
return
0
;
}
...
...
@@ -562,6 +704,7 @@ err1:
{
rt_sprintf
(
sbuf
,
"550 Cannot open
\"
%s
\"
for writing.
\r\n
"
,
filename
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
close_data_connection
(
session
);
rt_free
(
sbuf
);
return
0
;
}
...
...
@@ -579,7 +722,7 @@ err1:
else
if
(
numbytes
==
0
)
{
close
(
fd
);
close
socket
(
session
->
pasv_sockfd
);
close
_data_connection
(
session
);
rt_sprintf
(
sbuf
,
"226 Finished.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
break
;
...
...
@@ -587,12 +730,12 @@ err1:
else
if
(
numbytes
==-
1
)
{
close
(
fd
);
close
socket
(
session
->
pasv_sockfd
);
close
_data_connection
(
session
);
rt_free
(
sbuf
);
return
-
1
;
}
}
close
socket
(
session
->
pasv_sockfd
);
close
_data_connection
(
session
);
}
else
if
(
str_begin_with
(
buf
,
"SIZE"
)
==
0
)
{
...
...
@@ -659,7 +802,7 @@ err1:
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
closesocket
(
session
->
pasv_sockfd
);
session
->
pasv_active
=
0
;
rt_free
(
sbuf
);
rt_free
(
sbuf
);
return
0
;
}
pasvremote
.
sin_addr
.
s_addr
=
inet_addr
(
tmpip
);
...
...
@@ -674,7 +817,7 @@ err1:
rt_sprintf
(
sbuf
,
"425 Can't open data connection.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
closesocket
(
session
->
pasv_sockfd
);
rt_free
(
sbuf
);
rt_free
(
sbuf
);
return
0
;
}
}
...
...
@@ -697,9 +840,9 @@ err1:
{
if
(
session
->
is_anonymous
==
RT_TRUE
)
{
rt_sprintf
(
sbuf
,
"5
5
0 Permission denied.
\r\n
"
);
rt_sprintf
(
sbuf
,
"5
3
0 Permission denied.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
rt_free
(
sbuf
);
return
0
;
}
...
...
@@ -720,7 +863,7 @@ err1:
{
if
(
session
->
is_anonymous
==
RT_TRUE
)
{
rt_sprintf
(
sbuf
,
"5
5
0 Permission denied.
\r\n
"
);
rt_sprintf
(
sbuf
,
"5
3
0 Permission denied.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
return
0
;
...
...
@@ -740,9 +883,9 @@ err1:
{
if
(
session
->
is_anonymous
==
RT_TRUE
)
{
rt_sprintf
(
sbuf
,
"5
5
0 Permission denied.
\r\n
"
);
rt_sprintf
(
sbuf
,
"5
3
0 Permission denied.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
rt_free
(
sbuf
);
return
0
;
}
build_full_path
(
session
,
parameter_ptr
,
filename
,
256
);
...
...
@@ -758,12 +901,53 @@ err1:
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
}
}
else
if
(
str_begin_with
(
buf
,
"RNFR"
)
==
0
)
{
if
(
session
->
is_anonymous
==
RT_TRUE
)
{
rt_sprintf
(
sbuf
,
"530 Permission denied.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
return
0
;
}
build_full_path
(
session
,
parameter_ptr
,
filename
,
256
);
rt_sprintf
(
sbuf
,
"350 Successfully rececive old file
\"
%s
\"
.
\r\n
"
,
filename
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
}
else
if
(
str_begin_with
(
buf
,
"RNTO"
)
==
0
)
{
char
new_filename
[
256
];
if
(
session
->
is_anonymous
==
RT_TRUE
)
{
rt_sprintf
(
sbuf
,
"530 Permission denied.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
return
0
;
}
build_full_path
(
session
,
parameter_ptr
,
new_filename
,
256
);
if
(
rename
(
filename
,
new_filename
)
==
-
1
)
{
rt_sprintf
(
sbuf
,
"553 rename file
\"
%s
\"
error.
\r\n
"
,
filename
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
}
else
{
rt_sprintf
(
sbuf
,
"250 Successfully rename to new file
\"
%s
\"
.
\r\n
"
,
filename
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
}
}
else
if
((
str_begin_with
(
buf
,
"NOOP"
)
==
0
)
||
str_begin_with
(
buf
,
"noop"
)
==
0
)
{
rt_sprintf
(
sbuf
,
"200 noop!
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
}
else
if
(
str_begin_with
(
buf
,
"QUIT"
)
==
0
)
{
rt_sprintf
(
sbuf
,
"221 Bye!
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
rt_free
(
sbuf
);
rt_free
(
sbuf
);
return
-
1
;
}
else
...
...
@@ -771,7 +955,7 @@ err1:
rt_sprintf
(
sbuf
,
"502 Not Implemented.
\r\n
"
);
send
(
session
->
sockfd
,
sbuf
,
strlen
(
sbuf
),
0
);
}
rt_free
(
sbuf
);
rt_free
(
sbuf
);
return
0
;
}
...
...
@@ -787,5 +971,15 @@ void ftpd_start()
#ifdef RT_USING_FINSH
#include <finsh.h>
FINSH_FUNCTION_EXPORT
(
ftpd_start
,
start
ftp
server
)
FINSH_FUNCTION_EXPORT
(
ftpd_start
,
start
ftp
server
);
#ifdef FINSH_USING_MSH
int
cmd_ftpd_start
(
int
argc
,
char
**
argv
)
{
ftpd_start
();
return
0
;
}
FINSH_FUNCTION_EXPORT_ALIAS
(
cmd_ftpd_start
,
__cmd_ftpd_start
,
start
ftp
server
.);
#endif
#endif
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录