miniFTP项目实战四
项目简介:
在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进程来为客户提供服务。同时每个ftp服务进程配套了nobody进程(内部私有进程),主要是为了做权限提升和控制。
实现功能:
除了基本的文件上传和下载功能,还实现模式选择、断点续传、限制连接数、空闲断开、限速等功能。
用到的技术:
socket、I/O复用、进程间通信、HashTable
欢迎技术交流q:2723808286
项目开源!!!
miniFTP项目实战一
miniFTP项目实战二
miniFTP项目实战三
miniFTP项目实战四
miniFTP项目实战五
miniFTP项目实战六
文章目录
传输目录列表
在用户登录之后,客户端会与服务器协商,传输的文件类型以及传输的类型,之后再LIST申请目录列表:
文件的传输类型一般都是ASCII,传输模式需要根据有无NAT防火墙来选择,具体的确保在之前以及介绍过了。
4.1 PASV模式
服务器被动连接,由nobody进程创建监听socket,将创建好的监听socket传递给服务进程,服务进程返回服务器的IP地址及端口号,其处理逻辑如下:
static void do_pasv(session_t *sess)
{
//由nobody进程创建监听套接字 ,并返回端口 服务进程中通过getlocalip获取IP地址
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_LISTEN);
unsigned short port = (int)priv_sock_get_int(sess->child_fd); //获取端口号
char ip[16] = {0};
int ret = getlocalip(ip);
if (ret == -1) printf("getlocalip filed\n");
char tmp[1024] = {0};
unsigned int v[4];
sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
sprintf(tmp, "Entering Passive Mode (%u,%u,%u,%u,%u,%u)",
v[0], v[1], v[2], v[3], port>>8, port&0xff);
ftp_relply(sess, FTP_PASVOK, tmp);
}
由于getlocalip(ip)获取的是点分形式的IP地址,所以通过sscanf格式化获取IP地址,然后格式化到tmp字符串中返回给客户端。
4.2 PORT模式
PORT模式下,服务器主动连接客户端,客户端的命令参数中说明了客户端的IP地址以及端口号,服务进程解析IP地址及端口号,保存在sess中,后续nobody进程根据sess中的IP地址进行连接,处理逻辑如下:
static void do_port(session_t *sess)
{
unsigned int v[6] = {0};
//直接使用sscanf格式化输入,提取相关数据
sscanf(sess->arg, "%u,%u,%u,%u,%u,%u", &v[2], &v[3], &v[4], &v[5], &v[0], &v[1]);
sess->port_addr = (struct sockaddr_in*)malloc(sizeof(struct sockaddr_in));
memset(sess->port_addr, 0, sizeof(struct sockaddr_in));
sess->port_addr->sin_family = AF_INET;
unsigned char *p = (unsigned char*)&sess->port_addr->sin_port;
p[0] = v[0];
p[1] = v[1];
p = (unsigned char*)&sess->port_addr->sin_addr;
p[0] = v[2];
p[1] = v[3];
p[2] = v[4];
p[3] = v[5];
//收到PORT后要回复
ftp_relply(sess, FTP_PORTOK, "PORT command sucessful. COnsider using PASV.");
//接下来客户端会发送LIST命令
}
4.3 LIST 命令中创建数据连接
PASV和PORT已经确定了双方的连接方式,LIST命令是传输当前目录的文件列表,首先应该创建数据传输通道!
数据传输通道创建完成之后,读取目录列表信息,经过提取信息后传输目录列表,最后关闭数据传输通道,回应226。
在数据传输完成之后要及时关闭数据传输通道,即socket,因为客户端处于一直接收的状态,只有服务器关闭socket客户端才会停止接收,作出反应,逻辑如下:
static void do_list(session_t *sess)
{
//获取数据传输通道的fd
if (get_transfer_fd(sess) == 0) {
return ;
}
//回应150
ftp_relply(sess, FTP_DATACONN, "Here comes the directory listing.");
//传输列表
list_common(sess, 1); //全部
//关闭数据通道 如果不及时关闭通道 客户端是不会接收停止的,即关闭之后客户端才会作出反应
close(sess->data_fd);
//回应226
ftp_relply(sess, FTP_TRANSFEROK, "Directory send ok.");
}
其中最重要的有两部分:
- 数据传输通道的创建,即nobody通过socket与客户端建立连接
- 传输目录列表,要读取当前目录的信息,并且获取文件的信息发送
4.4 数据传输通道的建立
会根据PORT还是PASV来创建数据传输通道,所以在连接之前首先判断是PORT模式还是PASV模式,处理逻辑如下:
/*
* 根据模式的不同建立数据连接通道
* PORT:主动连接客户端
* PASV:被动接受客户端连接
* */
int get_transfer_fd(session_t *sess)
{
int ret = 1;
//检测 PORT or PASV 是否都没有激活
if (!port_active(sess) && !pasv_active(sess)) {
ftp_relply(sess, FTP_BADSENDCONN, "Use PORT or PASV first.");
return 0;
}
//主动模式服务器绑定20端口 创建socket主动connect客户端,调用sysutil.c中实现的tcp_client
if (port_active(sess)) {
if (get_port_fd(sess) == 0) { //失败
ret = 0;
}
}
if (pasv_active(sess)) {
if (get_pasv_fd(sess) == 0) { //获取到之后就保存在sess->data_fd
ret = 0;
}
//监听socket作用就是 为数据连接通道做准备,每次数据连接完成之后都会断开,下一次重新连接
close(sess->pasv_listen_fd);
}
//malloc的地址已经没有利用价值了,以及绑定好啦
if (sess->port_addr != NULL) {
free(sess->port_addr);
sess->port_addr = NULL;
}
if (ret) start_data_alarm(); //在数据传输之前重新安装信号 并启动闹钟
return ret;
}
4.5 PORT模式下数据传输通道的创建
//获取PORT模式下数据传输通道的fd
int get_port_fd(session_t *sess)
{
//由nobody进程创建数据连接通道,服务进程向nobody发起一个PRIV_SOCK_GET_DATA_SOCK创建数据通道请求
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_GET_DATA_SOCK);
//然后向nobody发送一个int port
unsigned short port = ntohs(sess->port_addr->sin_port);
priv_sock_send_int(sess->child_fd, (int)port); //发送实际是short 强转为int
//然后向nobody发送IP地址 字符串
char *ip = inet_ntoa(sess->port_addr->sin_addr);
priv_sock_send_buf(sess->child_fd, ip, strlen(ip));
//接收应答判断
int res = priv_sock_get_result(sess->child_fd);
if (res == PRIV_SOCK_RESULT_BAD) {
printf("create data filed\n");
return 0;
} else if (res == PRIV_SOCK_RESULT_OK) {
sess->data_fd = priv_sock_recv_fd(sess->child_fd); //接收数据传输通道sock fd
}
return 1;
}
服务进程向nobody进程发送PRIV_SOCK_GET_DATA_SOCK,请求nobody进程建立数据传输通道,接着向nobody进程发送客户端的IP地址与端口号:
void privop_pasv_get_data_sock(session_t *sess)
{
//接收IP地址与端口
unsigned short port = priv_sock_get_int(sess->parent_fd);
char ip[16] = {0};
priv_sock_recv_buf(sess->parent_fd, ip, sizeof(ip));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port); //转换为网络字节序
addr.sin_addr.s_addr = inet_addr(ip);
int fd = tcp_client(20); //绑定20端口
if (fd == -1) {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return ;
}
//建立数据连接
if (connect_timeout(fd, &addr, tunable_connect_timeout) < 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, fd);
close(fd);
}
4.6PASV模式下数据传输通道的创建
int get_pasv_fd(session_t *sess)
{
//请求PASV 连接socket fd
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_ACCEPT);
char ret = priv_sock_get_result(sess->child_fd);
if (ret == PRIV_SOCK_RESULT_BAD) {
return 0;
} else if (ret == PRIV_SOCK_RESULT_OK) {
sess->data_fd = priv_sock_recv_fd(sess->child_fd);
}
return 1;
}
PASV模式下,服务进程向nobody进程发送PRIV_SOCK_PASV_ACCEPT,nobody通过accept_timeout等待客户端的连接请求,当连接建立之后,nobody向服务进程传递数据传输通道的文件描述符,nobody进程的响应如下:
//服务进程请求数据连接socket的时候会向nobody发送accept请求
void privop_pasv_accept(session_t *sess)
{
int data_fd = accept_timeout(sess->pasv_listen_fd, NULL, tunable_accept_timeout);
close(sess->pasv_listen_fd);
sess->pasv_listen_fd = -1;
if (data_fd == -1) {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return ;
} else {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
priv_sock_send_fd(sess->parent_fd, data_fd);
close(data_fd); //nobody进程不进行数据传输 断开
}
}
4.7目录的传输
无论PORT模式的数据传输,还是PASV模式的数据传输,对服务进程都一样!!!因为服务进程最终拿到的是数据传输通道的socket文件描述符,服务进程可以通过这个socket文件描述符向客户端发送数据。
在处理LIST命令的时候逻辑如下:
static void do_list(session_t *sess)
{
//获取数据传输通道的fd
if (get_transfer_fd(sess) == 0) {
return ;
}
//回应150
ftp_relply(sess, FTP_DATACONN, "Here comes the directory listing.");
//传输列表
list_common(sess, 1); //全部
//关闭数据通道 如果不及时关闭通道 客户端是不会接收停止的,即关闭之后客户端才会作出反应
close(sess->data_fd);
//回应226
ftp_relply(sess, FTP_TRANSFEROK, "Directory send ok.");
}
上面已经介绍了get_transfer_fd是如何创建数据传输通道的,下面就说一下list_common如何传输列表信息。
需要传输的列表信息有:
- 文件类型以及权限
- 文件连接数、uid、gid、大小
- 文件日期,分为两种格式
- 文件名,注意符号链接文件还要显式支持原文件名字
如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkMvU1PV-1613482997348)(F:\destop\m笔记\图\image-20210216181605784.png)]
打开当前目录
首先要打开当前目录,通过opendir函数可以打开目录,其函数原型如下:
DIR *opendir(const char *name);
//The opendir() function opens a directory stream corresponding to the directory name, and returns a
//pointer to the directory stream. The stream is positioned at the first entry in the directory.
//即根据路径打开一个目录,返回一个目录流指针,指针指向目录流中的第一个项目
然后通过readdir返回目录流所指向文件的信息,readdir函数原型如下:
struct dirent *readdir(DIR *dirp);
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
结构体中我们只需要关注d_name,即关注文件的名字,通过文件的名字可以获取文件的状态信息,stat函数原型如下:
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
/* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
主要就是通过stat结构体来获取要发送的信息!!!
- st_mode:保存文件类型及权限位,以及可以获取符号链接文件指向的源文件
- st_size:文件大小
- st_mtime:文件最后的修改时间
下面一一介绍:
获取文件类型及权限位
通过lstat函数获取文件的状态信息,然后根据statbuf.st_mode判断文件的类型与权限位,通过与宏定义相与的结果来判断。
得到的结果用一个数组来容纳,最后将每个部分的信息格式化到一个字符串中,发送字符串,获取文件类型与权限位的代码如下:
char perms[] = "----------"; //获取文件类型以及权限位 十个字符
mode_t mode = statbuf.st_mode; //statbuf.st_mode 中保存文件类型以及权限位
switch (mode & S_IFMT) {
case S_IFREG:perms[0] = '-'; break;
case S_IFDIR:perms[0] = 'd'; break;
case S_IFBLK:perms[0] = 'b'; break;
case S_IFLNK:perms[0] = 'l'; break;
case S_IFCHR:perms[0] = 'c'; break;
case S_IFSOCK:perms[0] = 's';break;
case S_IFIFO:perms[0] = 'p'; break;
default:break;
}
if (mode & S_IRUSR) perms[1] = 'r';
if (mode & S_IWUSR) perms[2] = 'w';
if (mode & S_IXUSR) perms[3] = 'x';
if (mode & S_IRGRP) perms[4] = 'r';
if (mode & S_IWGRP) perms[5] = 'w';
if (mode & S_IXGRP) perms[6] = 'x';
if (mode & S_IROTH) perms[7] = 'r';
if (mode & S_IWOTH) perms[8] = 'w';
if (mode & S_IXOTH) perms[9] = 'x';
// special perms
if (mode & S_ISUID) perms[3] = (perms[3] == 'x' ? 's' : 'S');
if (mode & S_ISGID) perms[6] = (perms[6] == 'x' ? 's' : 'S');
if (mode & S_ISVTX) perms[9] = (perms[9] == 'x' ? 's' : 'S');
获取连接数、uid、gid、文件大小
都是通过stat结构体直接获取:
char buf[1024] = {0}; //每次都要重新初始化
off = 0;
off += sprintf(buf, "%s ", perms); //添加文件类型 权限位
off += sprintf(buf + off, "%3ld %-8d %-8d ", statbuf.st_nlink, statbuf.st_uid, statbuf.st_gid); //连接数、uid、gid
off += sprintf(buf + off, "%8lu ", (unsigned long)statbuf.st_size); //添加文件大小
所有文件的信息都放在buf数组中,根据off来决定下一中属性存放的位置。
获取时间
先分析一下FTP中日期的格式:
drwxr-xr-x 3 1000 1000 4096 Feb 02 11:37 Desktop
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Documents
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Downloads
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Music
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Pictures
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Public
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Templates
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Videos
-rw-r--r-- 1 1000 1000 8980 Mar 21 2020 examples.desktop
drwxrwxr-x 9 1000 1000 4096 Feb 06 10:04 learn
-rw-r--r-- 1 1000 1000 2193 Mar 28 2020 vimrc
日期分为两种格式:
//如果文件时间新
drwxr-xr-x 3 1000 1000 4096 Feb 02 11:37 Desktop
//如果文件时间旧 或者是半年之前的文件
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Documents
首先要获取当前系统的时间,和文件最后一次修改时间进行比较,判断文件的格式
获取当前时间可以通过gettimeofday:
int gettimeofday(struct timeval *tv, struct timezone *tz); //tz为NULL表示当前系统时区
The functions gettimeofday() and settimeofday() can get and set the time as well as a timezone. The tv argument is a struct timeval (as specified in <sys/time.h>):
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
and gives the number of seconds and microseconds since the Epoch (see time(2)). The tz argument is a struct timezone:
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};
然后和stat中的struct timespec st_atim; /* Time of last access */
比较
如果文件时间比系统时间大,系统文件比文件时间早半年 表示文件是旧的,采用如下格式:
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Documents
p_date_format = “%b %e %Y”;
通过调用size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
格式化时间,但是需要一个struct tm *tm,需要将秒转换为结构体的形式,通过localtime:
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm); //格式化时间
struct tm *localtime(const time_t *timep); //将秒 转换为struct tm
代码如下:
//获取时间
char date_buf[64] = {0};
const char *p_date_format = "%b %e %H:%M";
struct timeval tv;
gettimeofday(&tv, NULL);
time_t local_time = tv.tv_sec;
if (statbuf.st_mtime > local_time || (local_time - statbuf.st_mtime) > 60*60*24*182)
p_date_format = "%b %e %Y";
struct tm *p_tm = localtime(&local_time);
strftime(date_buf, sizeof(date_buf), p_date_format, p_tm); //将时间按照对应格式格式化为字符串
off += sprintf(buf + off, "%s ", date_buf);
获取文件名
文件名的获取时要注意,符号链接文件要显式出与指向文件的关系,使用readlink函数获取实际指向的文件,将指向的文件保存在buf中。
所以符号链接文件和一般文件要分开获取文件名:
//获取文件名 符号链接文件要显式指向源文件
if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {0};
readlink(dt->d_name, tmp, sizeof(tmp));
sprintf(buf + off, "%s -> %s\r\n", dt->d_name, tmp);
} else {
sprintf(buf + off, "%s\r\n", dt->d_name);
}
整体融合
在获取到各个部分的信息之后,整合在buf中,通过writen发送给客户端,如下:
int list_common(session_t *sess, int detail)
{
DIR *dir = opendir("./"); //打开当前目录
struct dirent *dt; //从目录中获取文件
struct stat statbuf; //获取文件信息
int off = 0; //在整合的时候记录位置
if (dir == NULL) return 0;
//根据readdir遍历目录 使用lstat获取文件状态信息
//这里使用lstat,就是在符号链接文件的情况 查看链接文件的状态,而不是产看源文件
if (detail == 1) {
while ((dt = readdir(dir)) != NULL) {
if (lstat(dt->d_name, &statbuf) < 0 || dt->d_name[0] == '.') { //获取文件状态信息
continue;
}
char perms[] = "----------"; //获取文件类型以及权限位 十个字符
mode_t mode = statbuf.st_mode; //statbuf.st_mode 中保存文件类型以及权限位
switch (mode & S_IFMT) {
case S_IFREG:perms[0] = '-'; break;
case S_IFDIR:perms[0] = 'd'; break;
case S_IFBLK:perms[0] = 'b'; break;
case S_IFLNK:perms[0] = 'l'; break;
case S_IFCHR:perms[0] = 'c'; break;
case S_IFSOCK:perms[0] = 's';break;
case S_IFIFO:perms[0] = 'p'; break;
default:break;
}
if (mode & S_IRUSR) perms[1] = 'r';
if (mode & S_IWUSR) perms[2] = 'w';
if (mode & S_IXUSR) perms[3] = 'x';
if (mode & S_IRGRP) perms[4] = 'r';
if (mode & S_IWGRP) perms[5] = 'w';
if (mode & S_IXGRP) perms[6] = 'x';
if (mode & S_IROTH) perms[7] = 'r';
if (mode & S_IWOTH) perms[8] = 'w';
if (mode & S_IXOTH) perms[9] = 'x';
// special perms
if (mode & S_ISUID) perms[3] = (perms[3] == 'x' ? 's' : 'S');
if (mode & S_ISGID) perms[6] = (perms[6] == 'x' ? 's' : 'S');
if (mode & S_ISVTX) perms[9] = (perms[9] == 'x' ? 's' : 'S');
char buf[1024] = {0}; //每次都要重新初始化
off = 0;
off += sprintf(buf, "%s ", perms); //添加文件类型 权限位
off += sprintf(buf + off, "%3ld %-8d %-8d ", statbuf.st_nlink, statbuf.st_uid, statbuf.st_gid); //左对齐 添加连接数、uid、gid
off += sprintf(buf + off, "%8lu ", (unsigned long)statbuf.st_size); //添加文件大小
//获取时间
char date_buf[64] = {0};
const char *p_date_format = "%b %e %H:%M";
struct timeval tv;
gettimeofday(&tv, NULL);
time_t local_time = tv.tv_sec;
if (statbuf.st_mtime > local_time || (local_time - statbuf.st_mtime) > 60*60*24*182)
p_date_format = "%b %e %Y";
struct tm *p_tm = localtime(&local_time);
strftime(date_buf, sizeof(date_buf), p_date_format, p_tm); //将时间按照对应格式格式化为字符串
off += sprintf(buf + off, "%s ", date_buf);
//获取文件名 符号链接文件要显式指向源文件
if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {0};
readlink(dt->d_name, tmp, sizeof(tmp));
sprintf(buf + off, "%s -> %s\r\n", dt->d_name, tmp);
} else {
sprintf(buf + off, "%s\r\n", dt->d_name);
}
//通过sess中的数据通道socket发送
writen(sess->data_fd, buf, strlen(buf));
}
} else {
while ((dt = readdir(dir)) != NULL) {
if (lstat(dt->d_name, &statbuf) < 0 || dt->d_name[0] == '.') { //获取文件状态信息
continue;
}
char buf[1024] = {0}; //每次都要重新初始化
//获取文件名 符号链接文件要显式指向源文件
if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {0};
readlink(dt->d_name, tmp, sizeof(tmp));
sprintf(buf, "%s -> %s\r\n", dt->d_name, tmp);
} else {
sprintf(buf, "%s\r\n", dt->d_name);
}
//通过sess中的数据通道socket发送
writen(sess->data_fd, buf, strlen(buf));
}
}
closedir(dir);
return 1;
}
其中detail参数表示是否发送文件的详细信息。