本文共 17522 字,大约阅读时间需要 58 分钟。
项目简介:
在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进程来为客户提供服务。同时每个ftp服务进程配套了nobody进程(内部私有进程),主要是为了做权限提升和控制。 实现功能: 除了基本的文件上传和下载功能,还实现模式选择、断点续传、限制连接数、空闲断开、限速等功能。 用到的技术: socket、I/O复用、进程间通信、HashTable 欢迎技术交流q:2723808286 项目开源!!!
在用户登录之后,客户端会与服务器协商,传输的文件类型以及传输的类型,之后再LIST申请目录列表:
文件的传输类型一般都是ASCII,传输模式需要根据有无NAT防火墙来选择,具体的确保在之前以及介绍过了。
服务器被动连接,由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字符串中返回给客户端。
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命令}
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.");}
其中最重要的有两部分:
会根据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;}
//获取PORT模式下数据传输通道的fdint 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);}
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进程不进行数据传输 断开 }}
无论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如何传输列表信息。
需要传输的列表信息有:
如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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结构体来获取要发送的信息!!!
下面一一介绍:
通过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 permsif (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');
都是通过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、gidoff += sprintf(buf + off, "%8lu ", (unsigned long)statbuf.st_size); //添加文件大小
所有文件的信息都放在buf数组中,根据off来决定下一中属性存放的位置。
先分析一下FTP中日期的格式:
drwxr-xr-x 3 1000 1000 4096 Feb 02 11:37 Desktopdrwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Documentsdrwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Downloadsdrwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Musicdrwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Picturesdrwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Publicdrwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Templatesdrwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Videos-rw-r--r-- 1 1000 1000 8980 Mar 21 2020 examples.desktopdrwxrwxr-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): 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 Documentsp_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参数表示是否发送文件的详细信息。
转载地址:http://vxwzi.baihongyu.com/