FTP协议的介绍
FTP协议又称文件传输协议,它的组成包括两个部分:FTP服务端和FTP客户端,FTP服务器用来存储文件,其中20端口是传输数据的端口,21端口是传输控制信息的端口。
项目简介
本项目主要的功能是实现一个FTP服务器,客户端使用的是leapftp,是一个交互开发的过程,项目具体功能如下
- 可以实现FTP内部标准命令:UESR,PASS,PORT,PASV,LIST,STOR等。
- 实现配置文件的解析和用户的鉴权登录功能
- 实现FTP主动(port)模式和被动(pasv)模式的数据连接的建立
- 实现文件的上传下载,续传续载以及限速功能
- 实现系统的空闲断开(数据连接断开和控制连接断开)以及系统的连接数限制(客户连接数限制和每IP数限制)
FTP工作原理
我们将FTP客户端发起的一次请求称为一次会话。 1.1 启动FTP 启动FTP客户端,在客户端通过交互的用户页面,客户输入启动FTP的交互式指令。 1.2 建立控制连接 客户端根据用户命令给出的服务端的IP地址,向服务端的21端口发出主动连接的请求,服务器收到请求连接之后,通过TCP三次握手协议,在客户端的控制通道和服务端的控制通道建立了一条连接。 控制连接通道主要用来传送FTP的内部命令以及命令的响应等控制信息,它并不用来传输数据。 1.3建立数据连接 控制连接建立之后,进行数据传输的时候需要开辟数据连接通道。 FTP有两种建立数据连接的方式:分别是PORT模式和PASV模式。将分别在下面进行赘述。 1.4 关闭连接 数据传输完成之后,数据连接被关闭,重新回到FTP会话状态,直到控制连接被关闭,FTP服务结束。
FTP主动模式和被动模式连接
注意PORT模式和PASV模式都是相对于服务端来说的。 1.主动模式: 客户端向服务端发送PORT命令,客户端会创建临时套接字,绑定一个临时端口,并且在套接字上监听。 FTP服务器将通过20端口主动的连接客户端的临时端口,通过TCP三次握手建立数据连接通道。 2.被动模式: 客户端向服务端发送PASV命令,此时服务端会创建监听套接字,绑定一个端口,服务器在套接字上监听。 FTP服务端将通过控制通道告知自己的端口号,被动的等待客户端的连接。 客户端绑定临时端口和服务端创建的端口进行连接,通过TCP三次握手协议建立数据连接通道。
项目系统的逻辑架构
如上图所示:每来一个客户端。主进程就会创建一个新的进程组,该进程组专门为该客户端进行服务。其中nobody进程主要负责的是内部协议的解析,而FTP服务进程则负责数据传输和FTP协议的解析。两个进程之间的数据是通过进程通信模块来完成的。
项目实现
一、项目框架搭建
项目文件包含的文件如下图所示,文件主要可以分为以下3个部分
-
蓝色部分为配置文件加载模块,主要是仿照vsftpd中的配置文件,直接在miniftp.conf中修改配置项即可更改FTP相关的参数配置,方便用户操作。 -
橙色部分为系统公共模块,其中common.h:包含系统头文件,sysutil.c包含项目所需要的公有函数,str.c模块主要是用来解析命令行参数。 -
紫色部分为项目的重点模块,ftpproto.c模块用来实现ftp常用命令的解析,主被动模式的建立,文件的上传下载等功能。其中ftpcodes.h模块用来存储FTP的应答方式。服务器通过控制连接发送给客户端FTP应答,用户根据收到的应答信息来对服务器做出相关响应。 privparent.c主要负责的是创建主动模式和被动模式下的数据连接套接字,它其实就是nobody进程(FTP服务进程的父进程),通过privsock.c模块中提供的套接字发送接收函数将nobody进程创建的套接字发送给其子进程ftpproto.c模块。
上文提到过,FTP的一次连接其实就是一次会话。故session.c用来建立一次会话,通过fork函数会创建子进程ftp服务进程(ftpproto.c)以及自身进程(nobody进程)。之后在下文中将依次介绍每个模块是如何实现的。
二、系统各个模块的实现
配置文件解析模块
配置解析模块主要用来将miniftp.conf中对应的配置项的值加载到tunable.c文件中对应的配置变量中去。 miniftp.conf的内容如下所示:
#服务器IP
listen_address=192.168.138.8
#是否开启被动模式
pasv_enable=YES
#是否开启主动模式
port_enable=NO
#FTP服务器端口
listen_port=9100
#最大连接数
max_clients=2000
#每ip最大连接数
max_per_ip=50
#Accept超时间
accept_timeout=60
#Connect超时间
connect_timeout=60
#控制连接超时时间
idle_session_timeout=10
#数据连接超时时间
data_connection_timeout=0
#掩码
local_umask = 077
#最大上传速度
upload_max_rate=102400
#最大下载速度
download_max_rate=102400
tunable.c的内容如下,每一个变量对应的值是默认值。
#include"tunable.h"
int tunable_pasv_enable = 1;
int tunable_port_enable = 1;
unsigned int tunable_listen_port = 21;
unsigned int tunable_max_clients = 2000;
unsigned int tunable_max_per_ip = 50;
unsigned int tunable_accept_timeout = 60;
unsigned int tunable_connect_timeout = 60;
unsigned int tunable_idle_session_timeout=300;
unsigned int tunable_data_connection_timeout=300;
unsigned int tunable_local_umask = 077;
unsigned int tunable_upload_max_rate = 0;
unsigned int tunable_download_max_rate=0;
const char *tunable_listen_address;
我们需要实现的功能是使得配置文件miniftp.conf中配置的参数最终可以保存在tunable.c中对应的变量中。因此,需要建立两者的映射关系。parseconf.c文件就是用来将对应的配置项参数加载到配置变量中的。 首先需要定义对应的查询数组,如下图所示: parseconf_str_setting用来解析配置项的参数值为字符串的情况,该结构体中定义两个字符指针,分别指向了配置项参数名和配置变量。同理parseconf_int_setting结构体是用来解析配置项的参数值为整数的情况的。 需要注意的是结构体数组中包含两个NULL指针,用来代表配置项结束。 其次它的内部逻辑如下所示: 将miniftp.conf中的数据解析为参数名和对应的参数值,分别用key和value作为表示。 循环遍历各自的结构体数组,将key和结构体成员p_setting_name作比较,两者相等的时候,将value值赋值给相对应的配置变量即可。 parseconf.c模块对应的文件如下所示:
#include"parseconf.h"
#include"tunable.h"
#include"str.h"
static struct parseconf_bool_setting
{
const char *p_setting_name;
int *p_variable;
}
parseconf_bool_array[] =
{
{"pasv_enable", &tunable_pasv_enable},
{"port_enable", &tunable_port_enable},
{NULL, NULL}
};
static struct parseconf_uint_setting
{
const char *p_setting_name;
unsigned int *p_variable;
}
parseconf_uint_array[] =
{
{"listen_port", &tunable_listen_port},
{"max_clients", &tunable_max_clients},
{"max_per_ip" , &tunable_max_per_ip},
{"accept_timeout", &tunable_accept_timeout},
{"connect_timeout", &tunable_connect_timeout},
{"idle_session_timeout", &tunable_idle_session_timeout},
{"data_connection_timeout", &tunable_data_connection_timeout},
{"local_umask", &tunable_local_umask},
{"upload_max_rate", &tunable_upload_max_rate},
{"download_max_rate", &tunable_download_max_rate},
{NULL, NULL}
};
static struct parseconf_str_setting
{
const char *p_setting_name;
const char **p_variable;
}
parseconf_str_array[] =
{
{"listen_address", &tunable_listen_address},
{NULL, NULL}
};
void parseconf_load_file(const char *path)
{
FILE *fp = fopen(path, "r");
if(NULL == fp)
ERR_EXIT("parseconf_load_file");
char setting_line[MAX_SETTING_LINE_SIZE] = {0};
while(fgets(setting_line, MAX_SETTING_LINE_SIZE, fp) != NULL)
{
if(setting_line[0]=='\0' || setting_line[0]=='#')
continue;
str_trim_crlf(setting_line);
parseconf_load_setting(setting_line);
memset(setting_line, 0, MAX_SETTING_LINE_SIZE);
}
fclose(fp);
}
void parseconf_load_setting(const char *setting)
{
char key[MAX_KEY_SIZE] = {0};
char value[MAX_VALUE_SIZE] = {0};
str_split(setting, key, value, '=');
const struct parseconf_str_setting *p_str_setting = parseconf_str_array;
while(p_str_setting->p_setting_name != NULL)
{
if(strcmp(key, p_str_setting->p_setting_name) == 0)
{
const char **p_cur_setting = p_str_setting->p_variable;
if(*p_cur_setting)
free((char *)*p_cur_setting);
*p_cur_setting = strdup(value);
return;
}
p_str_setting++;
}
const struct parseconf_bool_setting *p_bool_setting = parseconf_bool_array;
while(p_bool_setting->p_setting_name != NULL)
{
if(strcmp(key, p_bool_setting->p_setting_name) == 0)
{
str_upper(value);
int *p_cur_setting = p_bool_setting->p_variable;
if(strcmp(value, "YES") == 0)
*p_cur_setting = 1;
else if(strcmp(value, "NO") == 0)
*p_cur_setting = 0;
else
ERR_EXIT("parseconf_load_setting");
return;
}
p_bool_setting++;
}
const struct parseconf_uint_setting *p_uint_setting = parseconf_uint_array;
while(p_uint_setting->p_setting_name != NULL)
{
if(strcmp(key, p_uint_setting->p_setting_name) == 0)
{
unsigned int *p_cur_setting = p_uint_setting->p_variable;
*p_cur_setting = atoi(value);
return;
}
p_uint_setting++;
}
}
主函数模块
该文件主要是用来控制FTP服务器的启动,这里的主进程模拟的是root进程,为了防止主进程退出,服务端和客户端一旦建立连接之后,通过while循环来不断的等待新的连接请求到来。 它的主要流程如下所示:
- 加载配置文件miniftp.conf到主进程中。
- 检测是否是root启动,普通用户没有权限启动ftp服务端。
- 主进程完成和客户端的连接,产生控制连接的套接字sockconn.
- 创建子进程,子进程用来开启会话,通过session结构体中的ctrl_fd来获得连接套接字。
- 父进程关闭sockconn.
代码如下所示:
session_t *p_sess;
int main(int argc,char *argv[])
{
parseconf_load_file("miniftp.conf");
if(getuid()!=0)
{
printf("miniftp : must be started as root.\n");
exit(EXIT_FAILURE);
}
session_t sess=
{
-1,-1,-1,-1,-1,-1,NULL,"","","",1,NULL,0,0,0
};
p_sess = &sess;
int listenfd=tcp_server(tunable_listen_address,tunable_listen_port);
printf("listenfd=%d\n",listenfd);
int sockConn;
struct sockaddr_in addrCli;
socklen_t addrlen;
while(1)
{
sockConn=accept(listenfd,(struct sockaddr*)&addrCli,&addrlen);
printf("sockConn=%d\n",sockConn);
if(sockConn<0)
{
perror("accept");
continue;
}
pid_t pid =fork();
if(pid==0)
{
close(listenfd);
sess.ctrl_fd=sockConn;
begin_session(&sess);
exit(EXIT_SUCCESS);
}
else{
close(sockConn);
}
}
}
会话体模块
session.h模块中定义了FTP会话所需要的数据成员,包含控制连接的套接字的文件描述符,父子进程通道描述符,数据连接描述符等信息。
#ifndef _SESSION_H_
#define _SESSION_H_
#include "common.h"
typedef struct session{
uid_t uid;
int ctrl_fd;
int parent_fd;
int child_fd;
int data_fd;
int pasv_listen_fd;
struct sockaddr_in *port_addr;
char cmdline[MAX_COMMOND_LINE_SIZE];
char cmd[MAX_CMD_SIZE];
char arg[MAX_ARG_SIZE];
int is_ascii;
char *rnfr_name;
}session_t;
void begin_session(session_t *sess);
#endif
session.c代码如下:
#include"session.h"
#include"ftpproto.h"
#include "privsock.h"
#include"privparent.h"
void begin_session(session_t *sess)
{
priv_sock_init(sess);
pid_t pid = fork();
if(pid == -1)
ERR_EXIT("session fork");
if(pid == 0)
{
priv_sock_set_child_context(sess);
handle_child(sess);
}
else
{
priv_sock_set_parent_context(sess);
handle_parent(sess);
}
}
下面我们先看一下FTP服务进程的流程,这也是本次项目的最重要的模块,它完成了对FTP命令行参数的解析,鉴权登录,文件传输(上传下载),空闲断开(控制空闲断开和数据空闲断开)等一系列功能。
FTP服务进程模块
对应的步骤如下所示:
- 接收客户端发送过来的命令行,利用str.c里面的函数将命令行进行分割:分割为命令+参数,命令和参数以空格作为分割符,在解析命令行之前需要删除\r和\n,相关代码如下所示
例如:cmdline=USER mnitjh,cmd=USER,arg=mnitjh;cmdline=PASS 123456,cmd=PASS,arg=123456
void str_trim_crlf(char *str)
{
char *p = str + (strlen(str)-1);
while(*p=='\r' || *p=='\n')
*p-- = '\0';
}
void str_split(const char *str, char *left, char *right, char token)
{
char *pos = strchr(str, token);
if(pos == NULL)
strcpy(left, str);
else
{
strncpy(left, str, pos-str);
strcpy(right, pos+1);
}
}
- 建立命令映射关系:定义一个结构体变量:用来保存命令和对应处理函数的地址,如下所示:
typedef struct ftpcmd
{
const char *cmd;
void(*cmd_handler)(session_t *sess);
}ftpcmd_t;
ftpcmd_t ctrl_cmds[] =
{
{"USER", do_user},
{"PASS", do_pass},
{"PWD", do_pwd},
{"TYPE", do_type},
{"PORT", do_port},
{"PASV", do_pasv},
{"LIST", do_list},
{"CWD", do_cwd},
{"RMD" , do_rmd },
{"MKD" , do_mkd },
{"DELE", do_dele},
{"SIZE", do_size},
{"RNFR", do_rnfr},
{"RNTO", do_rnto},
{"RETR", do_retr},
{"STOR", do_stor},
{"REST", do_rest},
};
- 遍历所有的命令:
查找服务器接收到的参数是否存储在该结构体内,若存在,则响应相关的命令处理函数。处理完相关的命令之后,服务器向客户端回应响应代码。查找命令响应函数的代码如下所示:
while(1)
{
memset(sess->cmdline, 0, MAX_COMMOND_LINE_SIZE);
memset(sess->cmd, 0, MAX_CMD_SIZE);
memset(sess->arg, 0, MAX_ARG_SIZE);
int ret =recv(sess->ctrl_fd,sess->cmdline,MAX_CMD_SIZE,0);
if(ret==0)
exit(EXIT_SUCCESS);
if(ret<0)
ERR_EXIT("recv");
str_trim_crlf(sess->cmdline);
str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
int table_size = sizeof(ctrl_cmds) / sizeof(ctrl_cmds[0]);
int i;
for(i=0; i<table_size; ++i)
{
if(strcmp(sess->cmd, ctrl_cmds[i].cmd) == 0)
{
if(ctrl_cmds[i].cmd_handler)
ctrl_cmds[i].cmd_handler(sess);
else
ftp_reply(sess, FTP_COMMANDNOTIMPL, "Unimplement command.");
break;
}
}
if(i >= table_size)
ftp_reply(sess, FTP_BADCMD, "Unknown command.");
}
常用命令的解析 1.鉴权登录:需要使用到两个函数分别是getpwnam(passwordname)和getspname(shadow password)。其中getpwnam返回的是一个结构体指针,它的类型是passwd,结构体包含的内容如下所示。函数的返回值为NULL,如果当前匹配的用户不存在的话。 其中name是用户输入的名称。
struct passwd *getpwnam(const char *name)
passwd结构体中包含的内容如下所示,参数解释如下所示
struct passwd {
char *pw_name;
char *pw_passwd;
uid_t pw_uid;
gid_t pw_gid;
char *pw_gecos;
char *pw_dir;
char *pw_shell;
};
USER响应函数的步骤:只需要将解析到的name作为参数传递给getpwnam即可,返回值若为空,代表客户端输入的用户名称不存在。之后回应相关的FTP响应代码即可,代码如下所示,这里需要注意一点:假设用户名称和系统中保存的用户名一致,我们需要存储当前用户ID的uid,为了验证密码所需要的。
static void do_user(session_t *sess)
{
struct passwd *pwd = getpwnam(sess->arg);
if(pwd != NULL)
sess->uid = pwd->pw_uid;
ftp_reply(sess, FTP_GIVEPWORD, "Please specify the password");
}
验证密码的步骤如下所示:这里读者可能会有困惑,为什么不直接获取输入的用户名称,而需要通过uid来获取结构体从而获得用户名,这不是多此一举嘛。其实是因为在USER命令向响应结束,那么下一次等待客户端发来PASS命令的时候,之前存储的命令行参数都会被清空,所以我们是没有办法直接获取到用户的名称的,所以需要定义用户的uid通过uid间接的访问到用户名。
其实也可以尝试一下,直接在session结构体中保存它的用户名,会觉得这样更方便一点。
此外,有读者可能困惑,为什么不直接使用明文进行对比呢,因为linux系统中,为了防止密码被泄漏,会使用加密算法对明文加密,加密之后的密码保存在shadow.h文件中。 我们没有办法从加密之后的密码推断之前的明文是什么,只能将客户端用户输入的密码利用crypt函数进行加密之后,和正确的明文加密密码进行对比,从而判断密码是否正确。
对应代码如下所示,需要注意的是需要更改当前用户的uid和当前用户的groupid,否则对应的用户名是root用户。
static void do_pass(session_t *sess)
{
struct passwd *pwd = getpwuid(sess->uid);
if(pwd == NULL)
{
ftp_reply(sess, FTP_LOGINERR, "Login incorrect.");
return;
}
struct spwd *spd = getspnam(pwd->pw_name);
if(spd == NULL)
{
ftp_reply(sess, FTP_LOGINERR, "Login incorrect.");
return;
}
char *encrypted_pw = crypt(sess->arg, spd->sp_pwdp);
if(strcmp(encrypted_pw, spd->sp_pwdp) != 0)
{
ftp_reply(sess, FTP_LOGINERR, "Login incorrect.");
return;
}
setegid(pwd->pw_gid);
seteuid(pwd->pw_uid);
chdir(pwd->pw_dir);
ftp_reply(sess, FTP_LOGINOK, "Login successful.");
}
后续再看:
void handle_child(session_t *sess)
{
send(sess->ctrl_fd,"220 (miniftp1.0.1)\r\n",strlen("220 (miniftp1.0.1)\r\n"),0);
while(1)
{
memset(sess->cmdline, 0, MAX_COMMOND_LINE_SIZE);
memset(sess->cmd, 0, MAX_CMD_SIZE);
memset(sess->arg, 0, MAX_ARG_SIZE);
handle_idel_connection();
int ret =recv(sess->ctrl_fd,sess->cmdline,MAX_CMD_SIZE,0);
if(ret==0)
exit(EXIT_SUCCESS);
if(ret<0)
ERR_EXIT("recv");
str_trim_crlf(sess->cmdline);
str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
int table_size = sizeof(ctrl_cmds) / sizeof(ctrl_cmds[0]);
int i;
for(i=0; i<table_size; ++i)
{
if(strcmp(sess->cmd, ctrl_cmds[i].cmd) == 0)
{
if(ctrl_cmds[i].cmd_handler)
ctrl_cmds[i].cmd_handler(sess);
else
ftp_reply(sess, FTP_COMMANDNOTIMPL, "Unimplement command.");
break;
}
}
if(i >= table_size)
ftp_reply(sess, FTP_BADCMD, "Unknown command.");
}
}
2.数据连接的建立过程:PORT模式和PASV模式的建立。 这里我们需要知道FTP服务进程中需要的数据连接的套接字并不是由FTP服务进程本身创建的,而是通过nobody进程创建数据连接套接字,两个进程通过套接字通信的方式,将创建的套接字发送给FTP服务进程。
那么为什么要使用nobody进程呢? PORT模式下:服务器需要connect客户端,服务器可能没有权限做这种事情,需要nobody进程来帮忙,此外普通用户没有权限绑定20端口。 PASV模式下:创建套接字以及对套接字的监听涉及到内核的相关操作,因此直接由ftp服务进程去做,是不安全的。 nobody进程和FTP服务进程的通信协议规定如下:
- 由于父子进程之间双方都需要发送和读取数据,所以使用socket_pair函数创建一对无名的,相互连接的套接字。socketpair创建的套接字是全双工通信,每一个套接字既可以读数据也可以写数据,例如:可以往sv[0]中写数据,从sv[1]中读数据;也可以往sv[1]中写数据,从sv[0]中读数据。
函数声明如下,其中domain为作用域type为套接字类型,protocol设置为0即可,sv代表创建的套接字。由于nobody进程和ftp服务进程用于本地通信,因此domain选用的是AF_UNIX,注意这里不再是我们经常使用的AF_INET(用于网络通信的)。
int socketpair(int domain, int type, int protocol, int sv[2]);
- 完成父子进程上下文的设置,由于文件描述符是父子进程共享的,因此需要关闭对方的描述符。
void priv_sock_set_parent_context(session_t *sess)
{
if(sess->child_fd != -1)
{
close(sess->child_fd );
sess->child_fd = -1;
}
}
void priv_sock_set_child_context(session_t *sess)
{
if(sess->parent_fd != -1)
{
close(sess->parent_fd);
sess->parent_fd = -1;
}
}
- 内部通讯机制还定义了一系列的函数如下所示,主要关注的是最后一组函数,发送文件描述符和接收文件描述符。由于之前创建的控制连接的文件描述符是在fork之前创建的,因此子进程是可以共享父进程的信息的。而此时,我们需要在nobody进程中创建数据连接的套接字,发送给FTP服务进程,两个进程是独立的,互不影响的。因此不能直接传递4个字节的整型变量,而需要传递一个打开的文件描述符。
#define PRIV_SOCK_GET_DATA_SOCK 1
#define PRIV_SOCK_PASV_ACTIVE 2
#define PRIV_SOCK_PASV_LISTEN 3
#define PRIV_SOCK_PASV_ACCEPT 4
#define PRIV_SOCK_RESULT_OK 1
#define PRIV_SOCK_RESULT_BAD 2
void priv_sock_send_cmd(int fd, char cmd);
char priv_sock_get_cmd(int fd);
void priv_sock_send_result(int fd, char res);
char priv_sock_get_result(int fd);
void priv_sock_send_int(int fd, int the_int);
int priv_sock_get_int(int fd);
void priv_sock_send_buf(int fd, const char *buf, unsigned int len);
void priv_sock_recv_buf(int fd, char *buf, unsigned int len);
void priv_sock_send_fd(int sock_fd, int fd);
int priv_sock_recv_fd(int sock_fd);
关于发送套接字和接收套接字的详细情况:可以看这篇博文:
传递文件描述符 关于msghdr的详细讲解
实现的内部使用到的函数是sendmsg和recvmsg函数。
ssize_t sendmsg(int sockfd,const msghdr *msg,int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
需要对msghdr结构体内部的成员进行初始化,内部的结构比较复杂。有兴趣的同学可以配合上面的博文看一下具体实现的细节。 最终发送套接字实际上将套接字作为辅助数据进行发送的。
- 需要注意的是:将需要发送的文件描述符send_fd的长度作为参数传送给CMSG_SPACE,就可以得到cmsghdr整个结构体的大小。
之后填充msg_control和msg_controllen即可。 - 填充cmsghdr结构体的步骤:
利用宏函数CMSG_FIRSTHDR,将msg的地址传入,既可以得到msghdr指向的第一个cmsghdr结构体的地址,对内部变量cmsg_len,cmsg_level,cmsg_type填充即可。 - 使用CMSG_DATA宏,将填充好的cmsghdr结构体地址传入,即可以得到对应的辅助数据地址。将需要传输的文件描述符放入该地址即可。
此外我们可以看到,在内部通讯模块中,还定义了2组宏定义,其中A组宏定义用来表述FTP服务进程向nobody进程请求获取数据连接套接字。B组宏定义,它用来代表nobody进程队FTP服务进程的应答。
nobody进程中定义了4个函数分别用来对应A组宏定义,如下所示:
static void privop_port_get_data_sock(session_t *sess);
static void privop_pasv_active(session_t *sess);
static void privop_pasv_listen(session_t *sess);
static void privop_pasv_accept(session_t *sess);
响应PORT命令时: 会话结构体中增加成员变量:port_addr:类型是struct sockaddr_in,用来保存客户端的IP地址和端口号。 因为后面在建立数据连接的时候,服务器20端口需要主动connect客户端,由于每次响应命令的时候,之前保存的参数是会被清空,因此需要保存记录客户端的ip地址和端口号。 响应PASV命令时: 1.从配置文件中获取对应的服务端的IP地址。 2.其次产生数据连接的监听套接字,会话结构体中增加成员变量pasv_listenfd。由于监听套接字也是由nobody进程产生的,故FTP服务进程会向nobody进程发送获取监听字的命令请求。 3.将获取到的服务端的ip地址和端口号格式化写入到text数组中,通过ftp_reply函数向客户端回复相应代码。 需要注意的是,这里响应成功并不能代表就可以成功建立数据连接,PORT命令中只是获取到了客户端的ip和port号,同理PASV命令中只是产生了监听套接字,等待客户端来连接。
对应的代码如下所示:
static void do_port(session_t *sess)
{
unsigned int v[6];
sscanf(sess->arg,"%u,%u,%u,%u,%u,%u,",&v[0],&v[1],&v[2],&v[3],&v[4],&v[5]);
sess->port_addr=(struct sockaddr_in*)malloc(sizeof(struct sockaddr_in));
unsigned char *p=(unsigned char*)&sess->port_addr->sin_port;
p[0]=v[4];
p[1]=v[5];
sess->port_addr->sin_family = AF_INET;
p = (unsigned char *)&(sess->port_addr->sin_addr);
p[0] = v[0];
p[1] = v[1];
p[2] = v[2];
p[3] = v[3];
ftp_reply(sess, FTP_PROTOK, "PORT command successful. Consider using PASV.");
}
static void do_pasv(session_t *sess)
{
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_LISTEN);
sess->pasv_listen_fd =priv_sock_get_int(sess->child_fd);
char ip[16] = {0};
int len=priv_sock_get_int(sess->child_fd);
priv_sock_recv_buf(sess->child_fd,ip,(unsigned int)len);
unsigned short port = (unsigned short)priv_sock_get_int(sess->child_fd);
unsigned v[4] = {0};
sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
char text[MAX_BUFFER_SIZE] = {0};
sprintf(text, "Entering Passive Mode (%u,%u,%u,%u,%u,%u).",
v[0],v[1],v[2],v[3], port>>8, port&0x00ff);
ftp_reply(sess, FTP_PASVOK, text);
}
static void privop_pasv_listen(session_t *sess)
{
char ip[16]="192.168.138.8";
unsigned int v[4]={0};
sscanf(ip,"%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
int sockfd=tcp_server(ip,0);
sess->pasv_listen_fd = sockfd;
struct sockaddr_in addr;
socklen_t addrlen=sizeof(struct sockaddr);
if(getsockname(sess->pasv_listen_fd,(struct sockaddr*)&addr,&addrlen)<0)
ERR_EXIT("getsockname");
unsigned short port = ntohs(addr.sin_port);
priv_sock_send_int(sess->parent_fd,sess->pasv_listen_fd);
priv_sock_send_int(sess->parent_fd, strlen(ip));
priv_sock_send_buf(sess->parent_fd, ip, strlen(ip));
priv_sock_send_int(sess->parent_fd, (int)port);
}
建立数据连接的过程: 以list命令为例子:
- 主动连接被激活,使用主动模式进行通信
- 被动连接被激活,使用被动模式进行通信
- 服务器响应150代码
- 列表的传输
- 传输结束之后,服务器响应226代码
- 关闭数据连接
定义了get_transfer_fd函数,分为主动模式下的连接和被动模式下的连接。get_transfer_fd函数包含以下三种情况:
- 被动模式和主动模式均没有激活
- 只有主动模式被激活
- 只有被动模式被激活。
- 其他情况(两种模式均被激活)都是错误的。
情况1:获取主动模式下的数据连接套接字:get_port_fd函数:成功建立连接:函数返回0,否则返回1。 1.FTP服务进程向nobody进程发送1个字符的大小:PORT_GET_DATA_SOCK 2.通过之前的内部通讯模块中的send_buf函数,send_int函数来发送客户端ip地址和端口号。 3.nobody服务进程在while循环中不停的等待FTP服务进程发送过来的消息,recv函数会阻塞等待,接收到1个字符的:PROT_GET_DATA_SOCK,运行相关的处理函数 4.nobody进程接收相关的ip地址和端口号。 5.nobody进程创建服务端的数据连接套接字,通过connect函数来主动连接客户端。连接成功或者失败,均发送相关的应答字符。成功的时候,将服务端产生的套接字通过前文中提到的套接字发送函数发送给FTP服务进程即可。 6.FTP服务进程接收到套接字之后,将其存储在会话结构体中的data_fd数据成员中。 7.是否成功建立通过之前定义的nobody进程向FTP服务进程的应答请求来判断。
static void privop_port_get_data_sock(session_t *sess)
{
char ip[16] = {0};
int len = priv_sock_get_int(sess->parent_fd);
priv_sock_recv_buf(sess->parent_fd, ip, len);
unsigned short port = (unsigned short)priv_sock_get_int(sess->parent_fd);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
int sock = tcp_client();
socklen_t addrlen = sizeof(struct sockaddr);
if(connect(sock, (struct sockaddr*)&addr, addrlen) < 0)
{
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return;
}
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
priv_sock_send_fd(sess->parent_fd, sock);
close(sock);
}
情况2:获取被动模式下的数据连接套接字: 和主动模式不同,被动模式下客户端connect服务端,因此只需要发送PORT_SOCK_PASV_ACCEPT请求即可。 对应的代码如下所示:
static void privop_pasv_accept(session_t *sess)
{
int sockConn;
struct sockaddr_in addr;
socklen_t addrlen;
if((sockConn = accept(sess->pasv_listen_fd, (struct sockaddr*)&addr, &addrlen)) < 0)
{
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return;
}
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
priv_sock_send_fd(sess->parent_fd, sockConn);
close(sess->pasv_listen_fd);
sess->pasv_listen_fd = -1;
close(sockConn);
}
对应的获取被动模式的套接字的代码如下所示:
int get_pasv_fd(session_t *sess)
{
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_ACCEPT);
char res = priv_sock_get_result(sess->child_fd);
if(res == PRIV_SOCK_RESULT_BAD)
return -1;
sess->data_fd = priv_sock_recv_fd(sess->child_fd);
return 0;
}
判断被动模式被激活有两种方法: 方法1:FTP服务进程向nobody进程发送PRIV_SOCK_PASV_ACTIVE请求,父进程产生监听套接字发送给子进程,通过判断套接字对应的文件描述符是否为-1,来判断被动模式是否被激活。 代码如下所示:
ftpproto.c
int pasv_active(session_t *sess)
{
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_ACTIVE);
int active = priv_sock_get_int(sess->child_fd);
if(active != -1)
{
if(port_active(sess))
ERR_EXIT("both port an pasv are active");
return 1;
}
return 0;
}
privparent.c
static void privop_pasv_active(session_t *sess)
{
int active = -1;
if(sess->pasv_listen_fd != -1)
active = 1;
priv_sock_send_int(sess->parent_fd, active);
}
方法2:需要注意的是fork之后的两个进程是读时共享,写时复制的原理。如果子进程中的成员变量不进行修改的话,拷贝后的数据指向内存中的同一块物理地址空间,虚拟地址空间是不一样的。如果子进程对父进程的资源进行修改的话,操作系统将为修改之后的资源重新分配物理空间,这样做的好处时可以节省内存,真的是太赞了!因此可以不定义privop_pasv_active这个函数,我们直接在PASV请求中,获取到对应的监听套接字,注意父子进程之间的数据只要发生改变了,它们之间是不会互相影响的,各自有自己的虚拟地址空间,是独立自主的。代码如下所示:获取到了被动模式下的监听套接字。
static void do_pasv(session_t *sess)
{
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_LISTEN);
sess->pasv_listen_fd =priv_sock_get_int(sess->child_fd);
char ip[16] = {0};
int len=priv_sock_get_int(sess->child_fd);
priv_sock_recv_buf(sess->child_fd,ip,(unsigned int)len);
unsigned short port = (unsigned short)priv_sock_get_int(sess->child_fd);
unsigned v[4] = {0};
sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
char text[MAX_BUFFER_SIZE] = {0};
sprintf(text, "Entering Passive Mode (%u,%u,%u,%u,%u,%u).",
v[0],v[1],v[2],v[3], port>>8, port&0x00ff);
ftp_reply(sess, FTP_PASVOK, text);
}
pasv_active部分代码如下所示:
int pasv_active(session_t *sess)
{
printf("sess->pasv_listen_fd==%d\n",sess->pasv_listen_fd);
if(sess->pasv_listen_fd != -1)
{
if(port_active(sess)==0)
{
printf("both mode can not be actived in the meantime");
exit(EXIT_FAILURE);
}
return 0;
}
return -1;
}
此外,判断主动模式是否被激活的代码如下所示,思路就是判断会话结构体中的port_addr指针是否为空,如果为空的话,则说明我们没有使用到主动模式的建立,因为给port_addr指向的结构体成员赋值的过程是在PORT命令中完成的。此外,还需要注意主动模式被激活之后,被动模式也不可以被激活,因此这种情况程序也会退出,报错。
int port_active(session_t *sess)
{
if(sess->port_addr)
{
if(pasv_active(sess)==0)
{
printf("both mode can not be actived in the meantime");
exit(EXIT_FAILURE);
}
else
return 0;
}
else
return -1;
}
最终整体的代码如下所示:
static int get_transfer_fd(session_t *sess)
{
if(port_active(sess)== -1 &&pasv_active(sess)== -1)
{
ftp_reply(sess, FTP_BADSENDCONN, "Use PORT or PASV first.");
return -1;
}
if(port_active(sess)==0&&get_port_fd(sess)==0)
{
if(sess->port_addr)
{
free(sess->port_addr);
sess->port_addr = NULL;
}
return 0;
}
if(pasv_active(sess)==0&&get_pasv_fd(sess)==0)
{
return 0;
}
else
return -1;
}
list命令对应的代码如下所示:
static void do_list(session_t *sess)
{
if(get_transfer_fd(sess) != 0)
{
printf("the connection is not succeed\n");
ERR_EXIT("connect_faliure");
}
ftp_reply(sess, FTP_DATACONN, "Here comes the directory listing.");
list_common(sess);
ftp_reply(sess, FTP_TRANSFEROK, "Directory send OK.");
close(sess->data_fd);
sess->data_fd = -1;
}
下一章节将讨论文件的上传,下载,限速等功能的实现,呜呼,写的太累了!
|