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

miniFTP项目实战一
miniFTP项目实战二
miniFTP项目实战三
miniFTP项目实战四
miniFTP项目实战五
miniFTP项目实战六

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=true
port_enable=yes
listen_port=5021
max_clients=3
max_per_ip=2
accept_timeout=60
connect_timeout=60
idle_session_timeout=300
data_connection_timeout=900
local_umask=077
upload_max_rate=10240
download_max_rate=102400
listen_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用户登录验证

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

服务进程在接收到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 <shadow.h>
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 2 void 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;
}
}
}

miniFTP项目实战三的更多相关文章

  1. miniFTP项目实战五

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  2. miniFTP项目实战六

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  3. miniFTP项目实战四

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  4. miniFTP项目实战二

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  5. 【SSH项目实战三】脚本密钥的批量分发与执行

    [SSH项目实战]脚本密钥的批量分发与执行 标签(空格分隔): Linux服务搭建-陈思齐 ---本教学笔记是本人学习和工作生涯中的摘记整理而成,此为初稿(尚有诸多不完善之处),为原创作品,允许转载, ...

  6. PHP之MVC项目实战(三)

    本文主要包括以下内容 标准错误错误处理 http操作 PDO 文件操作 标准错误错误处理 PHP在语法层面上发生的错误 两个过程: 触发阶段(发生一个错误) 处理阶段(如何处理该错误) 触发阶段 系统 ...

  7. React-Native 之 项目实战(三)

    前言 本文有配套视频,可以酌情观看. 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我. 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关. 如文中内容对 ...

  8. python项目实战三个小实例

    1.   让用户输入圆的半径,告诉用户圆的面积: import math while True:     # 用户输入     r = input("请输入圆的半径:")     ...

  9. miniFTP项目集合

    项目简介 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进 ...

随机推荐

  1. 数据库表的自增ID createDate和updateDate 用JPA注解代替触发器实现

    对于数据库表的自增ID , createDate和updateDate 等字段,用JPA注解代替触发器实现,效率会高很多. 由于这些属性很多entity都有 可以写成两个基本entity :BaseE ...

  2. 管理员的基本防范措施 Linux系统安全及应用

    系统安全及应用一.账号安全基本措施① 系统账号清理② 密码安全控制③ 命令历史限制④ 终端自动注销二.SU命令切换用户① 用途及用法② 验证密码③ 限制使用su命令的用户④ 查看su操作记录补充三.L ...

  3. asp.net c#从SQL2008读取图片显示到网页

    //图像数据表:tx//字段id (nvarchar(50) ,image(image)//tgav为图片ID,实质为上传前的主名 (省略了.jpg) using System; using Syst ...

  4. springboot-1-入门

    springboot-1-入门 1.springboot简介,背景 简化Spring应用开发的一个框架: 整个Spring技术栈的一个大整合: J2EE开发的一站式解决方案: 2.极简hellowor ...

  5. UBUNTU 16.04 LTS SERVER 手动升级 MariaDB 到最新版 10.2

    UBUNTU 16.04 LTS SERVER 手动升级 MariaDB 到最新版 10.2 1. 起因 最近因为不同软件的数据问题本来只是一些小事弄着弄着就越弄越麻烦了,期间有这么个需求,没看到有中 ...

  6. Win10离线安装.net3.5

    起因 工作原因需要安装vs2008,但是依赖.net3.5,寻找可以离线安装的版本 尝试 下载.net framework sp1完整包 dotnetfx35.exe 可选下载 语言包 dotnetf ...

  7. sentry_sdk 错误日志监控 Flask配置

    https://www.cnblogs.com/sui776265233/p/11348169.html 开源的平台,为小服务日志监控统一管理 pip install --upgrade sentry ...

  8. 用postman进行web端自动化测试

    概括说一下,web接口自动化测试就是模拟人的操作来进行功能自动化,主要用来跑通业务流程. 主要有两种请求方式:post和get,get请求一般用来查看网页信息:post请求一般用来更改请求参数,查看结 ...

  9. 构建后端第5篇之---Idea 查看继承 实现关系图

    first question: how to show a class  children class :  move mousrmark to class name , Ctrl + H how t ...

  10. webSocket实现多人聊天功能

    webSocket实现多人在线聊天 主要思路如下: 1.使用vue构建简单的聊天室界面 2.基于nodeJs 的webSocket开启一个socket后台服务,前端使用H5的webSocket来创建一 ...