博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
miniFTP项目实战三
阅读量:3952 次
发布时间:2019-05-24

本文共 13119 字,大约阅读时间需要 43 分钟。

项目简介:

在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进程来为客户提供服务。同时每个ftp服务进程配套了nobody进程(内部私有进程),主要是为了做权限提升和控制。
实现功能:
除了基本的文件上传和下载功能,还实现模式选择、断点续传、限制连接数、空闲断开、限速等功能。
用到的技术:
socket、I/O复用、进程间通信、HashTable
欢迎技术交流q:2723808286
项目开源!!!

在这里插入图片描述

文章目录

3.1 服务进程处理FTP命令

服务进程负责处理FTP命令,包括对FTP命令的解析和调用对应的操作函数,服务进程在会话创建之后进入handle_child函数,在这个函数中,不断接收来自客户端的命令,并且解析命令,根据命令调用相关操作函数:

/** 循环从客户端接收数据,并解析命令和参数*/void handle_child(session_t *sess){
int ret, i = 0; //连接成功时,向客户端发送220命令主要是命令的格式:220后面加一个空格 ftp_relply(sess, FTP_GREET, "(GQ_miniFTP 0.1)"); while (1) {
//只初始化命令相关信息 memset(sess->cmdline, 0, MAX_COMMAND_LINE); memset(sess->cmd, 0, MAX_COMMAND); memset(sess->arg, 0, MAX_ARG); start_cmdio_alarm(); //处理完之后 重新设置闹钟,进行下一次计时 ret = readline(sess->ctl_fd, sess->cmdline, MAX_COMMAND_LINE); //读取命令 if (ret == -1) {
//出错 ERR_EXIT("readline"); } else if (ret == 0) {
//客户端断开连接,关闭服务进程,nobody进程暂时没有关闭 exit(EXIT_SUCCESS); } //解析处理FTP标准命令参数 开头是命令,空格之后的是参数, 所以要分割字符串 str_trim_crlf(sess->cmdline); //去除\r\n str_split(sess->cmdline, sess->cmd, sess->arg, ' '); //分隔字符串提取cmd arg str_upper(sess->cmd); //统一为大写字母 printf("%s=%s\n", sess->cmd, sess->arg); //遍历命令映射 处理命令 int size = sizeof(ctrl_cmds_map) / sizeof(ctrl_cmds_map[0]); for (i = 0; i < size; ++i) {
if (strcmp(ctrl_cmds_map[i].cmd, sess->cmd) == 0) {
if (ctrl_cmds_map[i].cmd_func != NULL) {
ctrl_cmds_map[i].cmd_func(sess); //调用相应操作函数 } else {
ftp_relply(sess, FTP_COMMANDNOTIMPL, "command Unimplement."); } break; } } if (i == size) ftp_relply(sess, FTP_BADCMD, "command Unkonwn."); }}

3.2 配置文件读取

vsftp都有一个配置文件,用来设置FTP服务器在连接过程中的各项参数。如下:

pasv_enable=trueport_enable=yeslisten_port=5021max_clients=3max_per_ip=2accept_timeout=60connect_timeout=60idle_session_timeout=300data_connection_timeout=900local_umask=077upload_max_rate=10240download_max_rate=102400listen_address=192.168.3.15

配置文件中读取配置配置项的值,代码中通过读取配置项的值来判断,配置项分为三类:

  • 开关型的配置项可以用int来表示

  • 整数参数的配置项可以用unsigned int

  • 字符串类型配置项目 可以const char*

所以我们要分别建立三种对应关系,每种关系都用一个表格来表示,如下:

static struct parseconf_bool_setting{
const char *p_setting_name; int *p_variable;};struct parseconf_bool_setting 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;};struct parseconf_uint_setting 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;};struct parseconf_str_setting parseconf_str_array[] = {
{
"listen_address", &tunable_listen_address }, {
NULL, NULL }};

将配置项名称(字符串)与配置项的值放在一个结构体中,每种数据类型的配置项都建立一个结构体数组来存储,配置文件的相关配置项。

在初始化的时候,就将配置选项字符串写进去

配置 配置文件的时候,使用两个接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MqCfLGHQ-1613482775198)(F:\destop\m笔记\图\image-20210205084817145.png)]

实现

先定义配置项名称,都是以字符串的形式放在tunable中,.c定义 .h声明,这些配置项都是要有初始值的。

然后我们自己创建一个配置文件。

然后编写代码去读取这个配置文件,读取相应的配置项。配置文件中配置项前后不要有空格和分号以方便读取,然后将此文件暂时放在程序目录下。

void parseconf_load_file(const char *path); //加载配置文件,读出每一项配置

void parseconf_load_setting(const char *setting); //对配置项进行解析,写入配置变量

如下:

//加载配置文件,逐行读取配置信息,对文件的解析void parseconf_load_file(const char *path){
FILE *fp; char setting_line[1024] = {
0}; fp = fopen(path, "r"); if (fp == NULL) {
ERR_EXIT("confg open filed"); } //循环读取配置文件中选项,写入相应变量 while (fgets(setting_line, 1023, fp) != NULL) {
if (strlen(setting_line) == 0 //未读取到 || setting_line[0] == '#' //注释的指令 || str_all_space(setting_line) == 1) //全是空格 continue; str_trim_crlf(setting_line); //去除\r\n parseconf_load_setting(setting_line); //将配置文件中的选项加载到相应变量中 // memset(setting_line, 0, strlen(setting_line)); strlen不行,因为strlen是以结束符\0为标志的,上面去除\r\n没了 memset(setting_line, 0, sizeof(setting_line)); } fclose(fp);}
//将配置文件加载到相应的配置项,对配置项的解析//遍历三张配置信息表,将配置信息放入相应的变量中void parseconf_load_setting(const char *setting){
char key[128] = {
0}; char val[128] = {
0}; while (isspace(*setting)) {
//去除左边可能存在的空格 ++setting; } str_split(setting, key, val, '='); if (strlen(val) == 0) {
//无配置内容,出错提示 fprintf(stderr, "missing value in config file for:%s", key); exit(EXIT_FAILURE); } //字符串配置选项读取 const struct parseconf_str_setting *p_str_setting = parseconf_str_array; while (p_str_setting->p_setting_name != NULL) {
if (strcmp(p_str_setting->p_setting_name, key) == 0) {
const char **p_cur_setting = p_str_setting->p_variable; // if (*p_cur_setting != NULL) {
free((char*)p_cur_setting); } //申请一块内存用来存放字符串,因为之前只是一个二级指针 *p_cur_setting = strdup(val); //malloc+strcpy return ; } ++p_str_setting; } free(*(char*)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(val); if (strcmp(val, "TRUE") == 0 || strcmp(val, "YES") == 0 || strcmp(val, "1") == 0 ) {
*p_bool_setting->p_variable = true; } else if (strcmp(val, "FALSE") == 0 || strcmp(val, "NO") == 0 || strcmp(val, "0") == 0 ) {
*p_bool_setting->p_variable = false; } else {
fprintf(stderr, "bad bool value in config file for: %s\n", key); exit(EXIT_FAILURE); } return ; } ++p_bool_setting; } //整数配置选项读取 //遍历unint表中的配置选项 const struct parseconf_uint_setting *p_uint_setting = parseconf_uint_array; while (p_uint_setting->p_setting_name != NULL) {
if (strcmp(p_uint_setting->p_setting_name, key) == 0) {
if (val[0] == '0') *(p_uint_setting->p_variable) = str_octal_to_uint(val); else *(p_uint_setting->p_variable) = atoi(val); return ; } ++p_uint_setting; } }

3.3用户登录验证

当客户端建立控制连接后,要进行用户登录验证,先确定用户是否存在,然后确定密码是否正确,流程如下:

image-20210216143205749

服务进程在接收到USER命令之后解析出用户名,根据用户名,通过getpwnam获取用户的相关信息,如果用户不存在getpwnam返回NULL,如果用户存在,保存uid,以便下面进行密码验证:

static void do_user(session_t *sess){
//命令响应 后面记得加上\r\n 330后面记得加上空格 struct passwd *pw; pw = getpwnam(sess->arg); //根据用户名获取密码信息结构体 与/etc/passwd对应 if (pw == NULL) {
//用户不存在 ftp_relply(sess, FTP_LOGINERR, "user not exist."); return ; } sess->uid = pw->pw_uid; ftp_relply(sess, FTP_GIVEPWORD, "Please specify the password");}

接下来就是密码的验证了!!!

用户存在后,客户端会发送密码,此时进入do_pass操作函数,但是!!!这里解析出来的只是密码,并没有说明是哪一个用户的!!!

这时候就用到上面放在sess中的uid了,通过sess的uid就可以锁定用户,进行密码验证了。getpwuid函数可以通过uid获取passwd结构体,在passwd结构体中包含如下信息:

struct passwd {
char *pw_name; /* username */ char *pw_passwd; /* user password */ uid_t pw_uid; /* user ID */ gid_t pw_gid; /* group ID */ char *pw_gecos; /* user information */ char *pw_dir; /* home directory */ char *pw_shell; /* shell program */ };

可以看到结构体中有pw_passwd,那意味着直接比较解析出来的密码与pw_passwd吗???

不是的,实际密码是经过加密放在影子文件中的,可以通过getspnam来获取影子文件的相关信息(root用户),其函数声明如下:

#include 
struct spwd *getspnam(const char *name);struct spwd {
char *sp_namp; /* Login name */ char *sp_pwdp; /* Encrypted password */ long sp_lstchg; /* Date of last change(measured in days since1970-01-01 00:00:00 +0000 (UTC)) */ long sp_min; /* Min # of days between changes */ long sp_max; /* Max # of days between changes */ long sp_warn; /* # of days before password expires to warn user to change it */ long sp_inact; /* # of days after password expires until account is disabled */ long sp_expire; /* Date when account expires (measured in days since 970-01-01 00:00:00 +0000 (UTC)) */ unsigned long sp_flag; /* Reserved */};

加密后的密码放在sp_pwdp中,我们只需要将解析出来的密码进行加密,然后与sp_pwdp比较就可以知道密码是否正确了,通过crypt函数对密码进行加密,函数原型如下,使用crypt函数之后要在链接的时候加上-lcrypt

char *crypt(const char *key, const char *salt);//crypt()算法会接受一个最长可达8字符的密钥(即key),并施以数据加密算法(DES)的一种变体。salt参数指向一个两个字符的字符串,//用来改变DES算法。该函数返回一个指针,指向长度13个字符的字符串char* encrypted_pass = crypt(sess->arg, sp->sp_pwdp); //获取加密后的密码

通过比较加密获得的密码,与影子文件中的密码,就可以知道密码是否正确。

完整的验证过程如下:

static void do_pass(session_t *sess){
struct passwd *pw; struct spwd *sp; pw = getpwuid(sess->uid); if (pw == NULL) {
//用户不存在 ftp_relply(sess, FTP_LOGINERR, "user not exist."); return ; } //实际密码是保存在影子文件中,getspnam 可以根据用户名获取影子文件信息 //如下的操作只有root才可以,一般用户会返回NULL,所以在session中只将nobody进程中才设置uid sp = getspnam(pw->pw_name); if (sp == NULL) {
ftp_relply(sess, FTP_LOGINERR, "user not exist."); //首次运行的时候出错 return ; } //影子文件中的密码是加密之后的,所以要将明文密码进行加密,与影子文件中加密密码比较使用crypt()函数 //char *crypt(const char *key, const char *salt); 第一个参数是明文,第二个参数是种子(也就是加密过的密码) char* encrypted_pass = crypt(sess->arg, sp->sp_pwdp); //链接的时候-lcrypt if (strcmp(encrypted_pass, sp->sp_pwdp) == 0) {
//密码正确 signal(SIGURG, handle_sigurg); activate_sigurg(sess->ctl_fd); //开启接收信号 //验证之后,此时进程拥有者是root 要将进程转交给登录的用户 umask(tunable_local_umask); setegid(pw->pw_gid); seteuid(pw->pw_uid); chdir(pw->pw_dir); ftp_relply(sess, FTP_LOGINOK, "Login successful."); } else {
ftp_relply(sess, FTP_LOGINERR, "err password."); }}

验证成功之后,此时的服务进程还是属于root用户,我们需要将进程转交给登录的用户,即改变进程的uid、gid,将工作目录移动到当前用户目录。

3.4 nobody进程与服务进程的内部通信

nobody进程与服务进程之间通过socketpair产生的socket进行通信,如下:

// 内部进程自定义协议// 用于FTP服务进程和nobody进程进行通信//主要用于PASV模式下绑定20端口  和PORT模式下获取数据连接套接字// FTP服务进程向nobody进程请求的命令#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// nobody进程对FTP服务进程的应答#define PRIV_SOCK_RESULT_OK    1#define PRIV_SOCK_RESULT_BAD    2void priv_sock_init(session_t *sess);void priv_sock_close(session_t *sess);void priv_sock_set_parent_context(session_t *sess);void priv_sock_set_child_context(session_t *sess);//其中cmd就是上面的宏定义 nobody进程与服务进程之间就通过下面的函数通信 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);

内部通信初始化就是创建一对socket然后分配给sess,父子进程要分别关闭对方的socket:

void priv_sock_init(session_t *sess){
int sockfds[2]; //分别是父、子进程用到的socketfd if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) < 0) ERR_EXIT("session socketpair"); sess->parent_fd = sockfds[0]; sess->child_fd = sockfds[1];}

nobody进程执行handle_parent函数来不断接收服务进程的消息:

void handle_parent(session_t *sess){
char cmd; /* 以root用户启动的时候 gid、uid都是0,所以要获取用户登录相关信息 */ struct passwd *pw = getpwnam("nobody"); //获取用户登录相关信息 if (setegid(pw->pw_gid) < 0) ERR_EXIT("session setegid"); //先设置组ID,然后设置用户ID if (seteuid(pw->pw_uid) < 0) ERR_EXIT("session seteuid"); minimize_privilege(); //获取绑定20端口权限 while (1) {
//从服务进程读取信息,这里的命令不是FTP标准命令,而是内部命令,nobody进程与客户端之间才是FTP标准命令 // read(sess->parent_fd, &cmd, 1); cmd = priv_sock_get_cmd(sess->parent_fd); //解析FTP内部命令参数 switch (cmd) {
case PRIV_SOCK_GET_DATA_SOCK: privop_pasv_get_data_sock(sess); break; case PRIV_SOCK_PASV_ACTIVE: privop_pasv_active(sess); break; case PRIV_SOCK_PASV_LISTEN: privop_pasv_listen(sess); break; case PRIV_SOCK_PASV_ACCEPT: privop_pasv_accept(sess); break; default: break; } }}

转载地址:http://pxwzi.baihongyu.com/

你可能感兴趣的文章
背包问题 V2 51Nod - 1806 ( 多重背包 )
查看>>
最少拦截系统 HDU - 1257 ( 动态规划 )
查看>>
瞌睡 (网易笔试题)
查看>>
分苹果 (网易笔试题)
查看>>
已知前序遍历和中序遍历求二叉树
查看>>
已知后序遍历和中序遍历求二叉树
查看>>
使用最小花费爬楼梯 (LeetCode - 746)
查看>>
勾股数 (迅雷笔试题)
查看>>
平安夜杀手 (科大讯飞笔试题)
查看>>
计算器 (贝壳笔试题)
查看>>
Prime Path POJ - 3126 ( 素数+搜索)
查看>>
迷宫问题 POJ - 3984 ( 搜索 最短路 记录路径 )
查看>>
全排列 51Nod - 1384 ( 搜索dfs / STL - next_permutation函数 )
查看>>
Catch That Cow HDU - 2717 ( 搜索 )
查看>>
Oil Deposits HDU - 1241 ( 搜索DFS )
查看>>
2019 网易校园招聘---[小易的字典]
查看>>
1001 害死人不偿命的(3n+1)猜想 (15 分)
查看>>
1003 我要通过! (20 分)
查看>>
1004 成绩排名 (20 分)
查看>>
1005 继续(3n+1)猜想 (25 分)
查看>>