myftp开发文档
Posted _Camille
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了myftp开发文档相关的知识,希望对你有一定的参考价值。
myftp开发文档
本文针对myftp项目的各个功能做出详细概述。
项目简介:提供文件存储和访问服务,实现信息共享
开发环境: CentOs、C/C++、Gcc、Makefile、Editplus、LeapFtp客户端, Git主要技术: socket、IO复用、IPC、HashTable
项目特点:
1、实现 Ftp内部标准命令USER、PASS、PORT、 PASV、IST、STOR、RETR 、RNFR、RNTO、MKD、RMD…
2、实现配置文件的解析
3、实现用户鉴权登录功能
4、实现FTP两种模式<主动模式port,被动模式pasv>的数据连接建立5、实现文件的上传、续传、下载、续载功能,上传下载限速功能
6、实现系统的空闲断开<数据连接断开,控制连接断开>,系统的连接数限制<客户连接数限制,每IP连接数限制>项目难点nobody进程协助ftp进程绑定20端口
●数据连接建立过程中的主动模式以及被动模式的理解
●每I连接数限制的实现过程,为统计每IP的连接数,需要借助两个Hash表完成映射
准备工具
Xshell6,云服务器(虚拟机),leapftp(客户端交互开发),editplus(方便代码编辑与查看)
鉴权登录
if(getuid() != 0)
printf("myftp : must be started as root. \\n");
exit(EXIT_FAILURE);
连接客户端
利用socket套接字来连接客户端,创建监听套接字,绑定ip和端口号,开始监听,accept获取连接套接字,连接客户端,准备进行会话,session文件定义会话结构。
命令映射
对客户端发起的命令做出相应的解析:
ftpproto服务进程来处理
在session会话结构中增加命令行、命令、命令参数三个字段,命令行负责接收客户端发来的命令,在str文件中解析命令行(去掉命令行尾部的\\r\\n,通过空格分割命令行),将命令和参数分别放入session中的对应字段。此时获取到命令以及参数,定义命令处理结构体的数组,数组中每个结构体内部为命令及相应的命令处理函数,每个处理函数参数都为session结构体,因为其内部有控制连接套接字。每当接收到命令时,通过遍历数组找到对应的处理函数,若是没有找到,则此命令未实现回复500 unknown ,定义FTP代码文件,通过回应相应代码来响应客户端。再封装一个回复函数。
void ftp_reply(session_t *sess, unsigned int code, const char *text)
char buffer[MAX_BUFFER_SIZE] = 0;
sprintf(buffer, "%d %s\\r\\n", code, text);
send(sess->ctrl_fd, buffer,strlen(buffer),0);
鉴权登录user、pass
在session结构体中增加字段uid为用户id号,
struct passwd
char * pw_name; /* Username, POSIX.1 /
char * pw_passwd; / Password /
__uid_t pw_uid; / User ID, POSIX.1 /
__gid_t pw_gid; / Group ID, POSIX.1 /
char * pw_gecos; / Real Name or Comment field实名或注释字段 /
char * pw_dir; / Home directory主目录, POSIX.1 /
char * pw_shell; / Shell Program, POSIX.1 */
;
struct passwd * getpwnam(char * name);
当知道使用者名称时,可以透过getpwnam来得知所有关於该使用者的相关资讯。
因此我们可以通过下面的方式来获取用户信息。
struct passwd *pwd = getpwnam(sess->arg);//参数为使用者名称
if(pwd != NULL)
sess->uid = pwd->pw_uid;//保存用户IDuid
ftp_reply(sess,FTP_GIVEPWORD, "Please specify the password");
当要验证密码pass时,再次通过uid(已在上一步保存在session中)来获取用户信息。
Linux系统中把用户密码保存在etc/shadow中,当然保存的都是加密的。
再来认识一个结构体:
struct spwd
char sp_namp; / Login name /
char sp_pwdp; / Encrypted password加密密码 /
long int sp_lstchg; / Date of last change上次更改日期 /
long int sp_min; / Minimum number of days between changes 更改之间的最小天数/
long int sp_max; /* Maximum number of days between changes 更改间隔的最大天数*/
long int sp_warn; /* Number of days to warn user to change the password警告用户更改密码的天数 /
long int sp_inact; / Number of days the account may be inactive帐户可能处于非活动状态的天数 /
long int sp_expire; / Number of days since 1970-01-01 until account expires自1970-01-01起到帐户到期的天数 /
unsigned long int sp_flag; / Reserved */
;
想要获取上面这个结构体的信息,需要通过getspnam这个函数,这个函数的参数为用户名,再上一步我们已经保存了uid,uid对应的那个passwd结构体中有用户名。
char * crypt(const char * key, const char * salt)
crypt()算法会接受一个最长可达8字符的密钥(即key),并施以数据加密算法(DES)的一种变体。salt参数指向一个两个字符的字符串,用来扰动(改变)DES算法。该函数返回一个指针,指向长度13个字符的字符串。
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;
//更改ftp服务进程
setegid(pwd->pw_gid);
seteuid(pwd->pw_uid);
chdir(pwd->pw_dir);//调整目录为宿主目录
ftp_reply(sess,FTP_LOGINOK,"Login successful.");
为什么要设置gid和uid,方便我们查看进程时能明确看到当前是那个用户的进程组。
pwd
char cwd[MAX_CWD_SIZE] = 0;
getcwd(cwd,MAX_CWD_SIZE);
char text[MAX_BUFFER_SIZE] = 0;
sprintf(text,"\\"%s\\"",cwd);
ftp_reply(sess,FTP_MKDIROK,text);
type
文件类型,即可指定ASCII码,二进制等。
在session结构体中添加is_ascii字段判断文件类型。
static void do_type(session_t *sess)
if(strcmp(sess->arg,"A") ==0 ||strcmp(sess->arg,"a")==0)
sess->is_ascii = 1;
ftp_reply(sess,FTP_TYPEOK,"Switching toASCII mode");
else if(strcmp(sess->arg,"I")==0 ||strcmp(sess->arg ,"i")==0)
sess->is_ascii=0;
ftp_reply(sess,FTP_TYPEOK,"Switching to Binary mode");
else
ftp_reply(sess,FTP_BADCMD,"Unrecognised TYPE command.");
port主动模式
首先在session中添加字段struct sockaddr_In类型的字段,里面包含客户端端口和地址(方便日后解析出来的数据存储在里面)。
当得到客户端的这些信息后,服务器创建一个套接字主动连接客户端。(使用connect)
这里需要提到,客户端发的user pass等命令是通过sess->ctrl_fd套接字发送的。
现在我们需要一个数据连接套接字来实现客户端和服务器的数据传输,暨在session增加数据连接套接字data_fd。而这个data_fd,就是服务器创建的连接客户端的套接字。
static void do_port(session_t *sess)
unsigned int v[6] = 0;
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));
//填充协议家族
sess->port_addr->sin_family = AF_INET;
//填充port
unsigned char *p = (unsigned char *)&(sess->port_addr->sin_port);
p[0] = v[4];
p[1] = v[5];
//填充ip
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.");
pasv被动模式
首先在session中添加pasv_listen_fd的字段,是被动模式服务器的监听套接字,被动模式顾名思义服务器将自己的地址和端口发送个客户端,然后接收客户端的连接accept,而accept的返回值则就是数据连接套接字。
static void do_pasv(session_t *sess)
char ip[16] = "服务器ip";
unsigned int v[4] = 0;
sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
//0代表生成默认端口号
int sockfd = tcp_server(ip, 0);
struct sockaddr_in addr;
socklen_t addrlen = sizeof(struct sockaddr);
if(getsockname(sockfd, (struct sockaddr*)&addr, &addrlen) < 0)
ERR_EXIT("getsockname");
sess->pasv_listen_fd = sockfd;
unsigned short port = ntohs(addr.sin_port);
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);
list
再来认识个结构体
struct __dirstream (DIR)
struct __dirstream
void *__fd;
char *__data;
int __entry_data;
char *__ptr;
int __entry_ptr;
size_t __allocation;
size_t __size;
__libc_lock_define (, __lock)
;
typedef struct __dirstream DIR;
DIR *opendir(const char *pathname),即打开文件目录,返回的就是指向DIR结构体的指针,而该指针由以下几个函数使用:struct dirent *readdir(DIR *dp);
void rewinddir(DIR *dp);
int closedir(DIR *dp); //有打开就有关闭
long telldir(DIR *dp);
void seekdir(DIR *dp,long loc);
struct dirent
long d_ino; //inode number 索引节点号
off_t d_off; //offset to this dirent 在目录文件中的偏移
unsigned short d_reclen; //length of this d_name 文件名长
unsigned char d_type; //the type of d_name 文件类型
char d_name [NAME_MAX+1];// file name (null-terminated) 文件名,最长255字符
struct stat
mode_t st_mode; //文件访问权限
ino_t st_ino; //索引节点号
dev_t st_dev; //文件使用的设备号
dev_t st_rdev; //设备文件的设备号
nlink_t st_nlink; //文件的硬连接数
uid_t st_uid; //所有者用户识别号
gid_t st_gid; //组识别号
off_t st_size; //以字节为单位的文件容量
time_t st_atime; //最后一次访问该文件的时间
time_t st_mtime; //最后一次修改该文件的时间
time_t st_ctime; //最后一次改变该文件状态的时间
blksize_t st_blksize; //包含该文件的磁盘块的大小
blkcnt_t st_blocks; //该文件所占的磁盘块
;
int stat(const char *file_name, struct stat *buf);的作用就是获取文件名为file_name的文件的详细信息,存储在stat结构体中。
struct tm
int tm_sec; /* 秒,范围从 0 到 59 /
int tm_min; / 分,范围从 0 到 59 /
int tm_hour; / 小时,范围从 0 到 23 /
int tm_mday; / 一月中的第几天,范围从 1 到 31 /
int tm_mon; / 月份,范围从 0 到 11 /
int tm_year; / 自 1900 起的年数 /
int tm_wday; / 一周中的第几天,范围从 0 到 6 /
int tm_yday; / 一年中的第几天,范围从 0 到 365 /
int tm_isdst; / 夏令时 */
;
localtime函数:
struct tm *localtime(const time_t *timep);使用 timerp的值来填充 tm 结构。timerp的值被分解为 tm 结构,并用本地时区表示。
1.数据连接建立
在数据连接之前,需要先确认数据协商,暨是主动连接还是被动连接。
2.回复150
3.传输列表
4.回复226
组织权限和时间
char* statbuf_get_perms(struct stat *sbuf)
static char perms[] = "----------";
mode_t mode = sbuf->st_mode;
switch(mode & S_IFMT)
case S_IFREG:
perms[0] = '-';
break;
case S_IFDIR:
perms[0] = 'd';
break;
case S_IFCHR:
perms[0] = 'c';
break;
case S_IFIFO:
perms[0] = 'p';
break;
case S_IFBLK:
perms[0] = 'b';
break;
case S_IFLNK:
perms[0] = 'l';
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';
return perms;
char* statbuf_get_date(struct stat *sbuf)
static char date[64] = 0;
struct tm *ptm = localtime(&sbuf->st_mtime);
strftime(date, 64, "%b %e %H:%M", ptm);
return date;
int port_active(session_t *sess)
if(sess->port_addr != NULL)
return 1;
return 0;
int pasv_active(session_t *sess)
if(sess->pasv_listen_fd != -1)
return 1;
return 0;
static int get_transfer_fd(session_t *sess)
if(!port_active(sess) && !pasv_active(sess))
//425 Use PORT or PASV first.
ftp_reply(sess, FTP_BADSENDCONN, "Use PORT or PASV first.");
return -1;
if(port_active(sess))
int sock = tcp_client();
socklen_t addrlen = sizeof(struct sockaddr);
if(connect(sock, (struct sockaddr*)sess->port_addr, addrlen) < 0)
return -1;
//保存数据连接套接字
sess->data_fd = sock;
if(pasv_active(sess))
int sockConn;
struct sockaddr_in addr;
socklen_t addrlen;
if((sockConn = accept(sess->pasv_listen_fd, (struct sockaddr*)&addr, &addrlen)) < 0)
return -1;
sess->data_fd = sockConn;
if(sess->port_addr)
free(sess->port_addr);
sess->port_addr = NULL;
return 0;
void list_common(session_t *sess)
DIR *dir = opendir(".");
if(dir == NULL)
ERR_EXIT("opendir");
struct stat sbuf;
char buf[MAX_BUFFER_SIZE] = 0;
unsigned int offset = 0;
struct dirent *dt;
while((dt = readdir(dir)))
if(stat(dt->d_name, &sbuf)<0)
ERR_EXIT("stat");
if(dt->d_name[0] == '.')
continue;
const char *perms = statbuf_get_perms(&sbuf);
offset = sprintf(buf, "%s", perms);
offset += sprintf(buf+offset, "%3d %-8d %-8d %8u ",
(int)sbuf.st_nlink, sbuf.st_uid, sbuf.st_gid, (unsigned int)sbuf.st_size);
const char *pdate = statbuf_get_date(&sbuf);
offset += sprintf(buf+offset, "%s ", pdate);
sprintf(buf+offset, "%s\\r\\n", dt->d_name);
//buf drwxrwxr-x 2 1000 1000 114 Dec 05 2020 93
send(sess->data_fd, buf, strlen(buf), 0);
closedir(dir);
static void do_list(session_t *sess)
//1 创建数据连接以上是关于myftp开发文档的主要内容,如果未能解决你的问题,请参考以下文章