Linux网络编程综合运用之MiniFtp实现(七)
上节中实现了配置文件的解析,这节来实现用户登录的验证,首先用客户端来登录vsftpd来演示登录的过程:
接着再连接miniftpd,来看下目前的效果:
接下来实现它,与协议相关的模块都是在ftpproto.c中完成的,目前的代码如下:
#include "ftpproto.h" #include "sysutil.h" #include "str.h" void handle_child(session_t *sess) { writen(sess->ctrl_fd, "220 (miniftpd 0.1)\r\n", strlen("220 (miniftpd 0.1)\r\n")); int ret; while (1) { memset(sess->cmdline, 0, sizeof(sess->cmdline)); memset(sess->cmd, 0, sizeof(sess->cmd)); memset(sess->arg, 0, sizeof(sess->arg)); ret = readline(sess->ctrl_fd, sess->cmdline, MAX_COMMAND_LINE); if (ret == -1) ERR_EXIT("readline"); else if (ret == 0) exit(EXIT_SUCCESS); printf("cmdline=[%s]\n", sess->cmdline); // 去除\r\n str_trim_crlf(sess->cmdline); printf("cmdline=[%s]\n", sess->cmdline); // 解析FTP命令与参数 str_split(sess->cmdline, sess->cmd, sess->arg, ' '); printf("cmd=[%s] arg=[%s]\n", sess->cmd, sess->arg); // 将命令转换为大写 str_upper(sess->cmd); // 处理FTP命令 } }
下面则开始编写逻辑:
#include "ftpproto.h" #include "sysutil.h" #include "str.h" void do_user(session_t *sess); void do_pass(session_t *sess); void handle_child(session_t *sess) { writen(sess->ctrl_fd, "220 (miniftpd 0.1)\r\n", strlen("220 (miniftpd 0.1)\r\n")); int ret; while (1) { memset(sess->cmdline, 0, sizeof(sess->cmdline)); memset(sess->cmd, 0, sizeof(sess->cmd)); memset(sess->arg, 0, sizeof(sess->arg)); ret = readline(sess->ctrl_fd, sess->cmdline, MAX_COMMAND_LINE); if (ret == -1) ERR_EXIT("readline"); else if (ret == 0) exit(EXIT_SUCCESS); printf("cmdline=[%s]\n", sess->cmdline); // 去除\r\n str_trim_crlf(sess->cmdline); printf("cmdline=[%s]\n", sess->cmdline); // 解析FTP命令与参数 str_split(sess->cmdline, sess->cmd, sess->arg, ' '); printf("cmd=[%s] arg=[%s]\n", sess->cmd, sess->arg); // 将命令转换为大写 str_upper(sess->cmd); // 处理FTP命令 if (strcmp("USER", sess->cmd) == 0) { do_user(sess); } else if (strcmp("PASS", sess->cmd) == 0) { do_pass(sess); } } } void do_user(session_t *sess) { //USER jjl } void do_pass(session_t *sess) { // PASS 123456 }
由于do_user和do_pass函数只为ftpproto.c内部使用,所以可以将其声明为static,如下:
#include "ftpproto.h" #include "sysutil.h" #include "str.h" static void do_user(session_t *sess); static void do_pass(session_t *sess); void handle_child(session_t *sess) { writen(sess->ctrl_fd, "220 (miniftpd 0.1)\r\n", strlen("220 (miniftpd 0.1)\r\n")); int ret; while (1) { memset(sess->cmdline, 0, sizeof(sess->cmdline)); memset(sess->cmd, 0, sizeof(sess->cmd)); memset(sess->arg, 0, sizeof(sess->arg)); ret = readline(sess->ctrl_fd, sess->cmdline, MAX_COMMAND_LINE); if (ret == -1) ERR_EXIT("readline"); else if (ret == 0) exit(EXIT_SUCCESS); printf("cmdline=[%s]\n", sess->cmdline); // 去除\r\n str_trim_crlf(sess->cmdline); printf("cmdline=[%s]\n", sess->cmdline); // 解析FTP命令与参数 str_split(sess->cmdline, sess->cmd, sess->arg, ' '); printf("cmd=[%s] arg=[%s]\n", sess->cmd, sess->arg); // 将命令转换为大写 str_upper(sess->cmd); // 处理FTP命令 if (strcmp("USER", sess->cmd) == 0) { do_user(sess); } else if (strcmp("PASS", sess->cmd) == 0) { do_pass(sess); } } } static void do_user(session_t *sess) { //USER jjl } static void do_pass(session_t *sess) { // PASS 123456 }
对于这两个命令的处理,则按vsftpd的来实现:
static void do_user(session_t *sess) { //USER jjl writen(sess->ctrl_fd, "331 Please specify the password.\r\n", strlen("331 Please specify the password.\r\n")); }
编译运行看下效果:
接下来处理do_pass(),继续来看一下正常的登录流程:
static void do_pass(session_t *sess) { // PASS 123456 writen(sess->ctrl_fd, "230 Login successful.\r\n", strlen("230 Login successful.\r\n")); }
编译运行:
当然这里还没有对用户名和密码进行真实的校验,下面则开始实现,先看下密码出错,vsftpd会有什么反馈:
如果故意将用户名输错呢?
依照上面的结果下面来进行用户名与密码的校验:
首先校验用户名,得根据用户名来获取系统相关信息,可以通过如下函数获取:
通过这个函数就可以判断用户存不存在,具体判断如下:
在上面这段代码中,我们发现代码不够精简灵活,每次服务端响应给客户端时,都要加一段字符如下:
像上面这些代码是有规律的,有必要封装成一个方法,一个参数传递代码,如:530;一个参数传递文本,如:Login incorrect.,然后在方法里面来生成上面的那些代码,所以声明一个方法:
#include "ftpproto.h" #include "sysutil.h" #include "str.h" static void ftp_reply(session_t *sess, int status, const char *text);//由于也是内部使用,所以声明成static static void do_user(session_t *sess); static void do_pass(session_t *sess); void handle_child(session_t *sess) { writen(sess->ctrl_fd, "220 (miniftpd 0.1)\r\n", strlen("220 (miniftpd 0.1)\r\n")); int ret; while (1) { memset(sess->cmdline, 0, sizeof(sess->cmdline)); memset(sess->cmd, 0, sizeof(sess->cmd)); memset(sess->arg, 0, sizeof(sess->arg)); ret = readline(sess->ctrl_fd, sess->cmdline, MAX_COMMAND_LINE); if (ret == -1) ERR_EXIT("readline"); else if (ret == 0) exit(EXIT_SUCCESS); printf("cmdline=[%s]\n", sess->cmdline); // 去除\r\n str_trim_crlf(sess->cmdline); printf("cmdline=[%s]\n", sess->cmdline); // 解析FTP命令与参数 str_split(sess->cmdline, sess->cmd, sess->arg, ' '); printf("cmd=[%s] arg=[%s]\n", sess->cmd, sess->arg); // 将命令转换为大写 str_upper(sess->cmd); // 处理FTP命令 if (strcmp("USER", sess->cmd) == 0) { do_user(sess); } else if (strcmp("PASS", sess->cmd) == 0) { do_pass(sess); } } } static void do_user(session_t *sess) { //USER jjl struct passwd *pw = getpwnam(sess->arg); if (pw == NULL) { // 用户不存在 writen(sess->ctrl_fd, "530 Login incorrect.\r\n", strlen("530 Login incorrect.\r\n")); return; } writen(sess->ctrl_fd, "331 Please specify the password.\r\n", strlen("331 Please specify the password.\r\n")); } static void do_pass(session_t *sess) { // PASS 123456 writen(sess->ctrl_fd, "230 Login successful.\r\n", strlen("230 Login successful.\r\n")); } static void ftp_reply(session_t *sess, int status, const char *text) { }
具体实现如下:
static void ftp_reply(session_t *sess, int status, const char *text) { char buf[1024] = {0}; sprintf(buf, "%d %s\r\n", status, text); writen(sess->ctrl_fd, buf, strlen(buf)); }
接着用这个方法来将代码进行替换,如下:
而对于上面的状态码,可以定义成常量,对于FTP有很多状态码,所以这里可以统一定义在一个头文件中,然后以后用它进行替换:
ftpcodes.h:
#ifndef _FTP_CODES_H_ #define _FTP_CODES_H_ #define FTP_DATACONN 150 #define FTP_NOOPOK 200 #define FTP_TYPEOK 200 #define FTP_PORTOK 200 #define FTP_EPRTOK 200 #define FTP_UMASKOK 200 #define FTP_CHMODOK 200 #define FTP_EPSVALLOK 200 #define FTP_STRUOK 200 #define FTP_MODEOK 200 #define FTP_PBSZOK 200 #define FTP_PROTOK 200 #define FTP_OPTSOK 200 #define FTP_ALLOOK 202 #define FTP_FEAT 211 #define FTP_STATOK 211 #define FTP_SIZEOK 213 #define FTP_MDTMOK 213 #define FTP_STATFILE_OK 213 #define FTP_SITEHELP 214 #define FTP_HELP 214 #define FTP_SYSTOK 215 #define FTP_GREET 220 #define FTP_GOODBYE 221 #define FTP_ABOR_NOCONN 225 #define FTP_TRANSFEROK 226 #define FTP_ABOROK 226 #define FTP_PASVOK 227 #define FTP_EPSVOK 229 #define FTP_LOGINOK 230 #define FTP_AUTHOK 234 #define FTP_CWDOK 250 #define FTP_RMDIROK 250 #define FTP_DELEOK 250 #define FTP_RENAMEOK 250 #define FTP_PWDOK 257 #define FTP_MKDIROK 257 #define FTP_GIVEPWORD 331 #define FTP_RESTOK 350 #define FTP_RNFROK 350 #define FTP_IDLE_TIMEOUT 421 #define FTP_DATA_TIMEOUT 421 #define FTP_TOO_MANY_USERS 421 #define FTP_IP_LIMIT 421 #define FTP_IP_DENY 421 #define FTP_TLS_FAIL 421 #define FTP_BADSENDCONN 425 #define FTP_BADSENDNET 426 #define FTP_BADSENDFILE 451 #define FTP_BADCMD 500 #define FTP_BADOPTS 501 #define FTP_COMMANDNOTIMPL 502 #define FTP_NEEDUSER 503 #define FTP_NEEDRNFR 503 #define FTP_BADPBSZ 503 #define FTP_BADPROT 503 #define FTP_BADSTRU 504 #define FTP_BADMODE 504 #define FTP_BADAUTH 504 #define FTP_NOSUCHPROT 504 #define FTP_NEEDENCRYPT 522 #define FTP_EPSVBAD 522 #define FTP_DATATLSBAD 522 #define FTP_LOGINERR 530 #define FTP_NOHANDLEPROT 536 #define FTP_FILEFAIL 550 #define FTP_NOPERM 550 #define FTP_UPLOADFAIL 553 #endif /* _FTP_CODES_H_ */
然后包含该头文件:
将其中写死的状态码改成常量:
再来编译运行:
接下来则继续做用户名和密码的验证:
这时可以将passwd中的user ID字段保存到session结构体中,便于验证密码时使用:
所以需要修改session结构体:
同时得修改对session的初始化:
然后在用户名验证成功之后则将值保存在session中:
接着来验证密码,再一次对用户名进行验证,根据刚才保存的uid,通过如下函数:
具体如下:
接下来需要获取用户所对应的密码,而它是保存在影子文件当中的,这里可以通过另外一个系统函数获得:
而其中的参数name就是传递用户名:
所以代码如下:
接下来需要用客户端传过来的用户密码跟影子文件中的密码进行比较:
而FTP客户端发过来的密码是明文的,所以在比较之前首先需要对密码进行加密,这里需要用到另外一个系统函数:
具体代码如下:
接下来则就可以进行密码匹对了:
接下来编译运行一下:
将头文件放到common.h中:
再次编辑运行:
原因是:
所以需要修改一下Makefile文件:
.PHONY:clean CC=gcc CFLAGS=-Wall -g BIN=miniftpd OBJS=main.o sysutil.o session.o ftpproto.o privparent.o str.o tunable.o parseconf.o LIBS=-lcrypt $(BIN):$(OBJS) $(CC) $(CFLAGS) $^ -o $@ $(LIBS) %.o:%.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f *.o $(BIN)
这时再编译运行一下:
由于输出530的地方有多处,所以先打一个log来定位一下看是哪出现的530:
static void do_user(session_t *sess) { //USER jjl struct passwd *pw = getpwnam(sess->arg); if (pw == NULL) { // 用户不存在 ftp_reply(sess, FTP_LOGINERR, "1Login incorrect."); return; } sess->uid = pw->pw_uid; ftp_reply(sess, FTP_GIVEPWORD, "Please specify the password."); } static void do_pass(session_t *sess) { // PASS 123456 struct passwd *pw = getpwuid(sess->uid); if (pw == NULL) { // 用户不存在 ftp_reply(sess, FTP_LOGINERR, "2Login incorrect."); return; } printf("name=[%s]\n", pw->pw_name); struct spwd *sp = getspnam(pw->pw_name); if (sp == NULL) { // 没有找到影子文件,则代表登录也是失败的 ftp_reply(sess, FTP_LOGINERR, "3Login incorrect."); return; } // 将明文进行加密 char *encrypted_pass = crypt(sess->arg, sp->sp_pwdp); // 验证密码 if (strcmp(encrypted_pass, sp->sp_pwdp) != 0) { ftp_reply(sess, FTP_LOGINERR, "4Login incorrect."); return; } ftp_reply(sess, FTP_LOGINOK, "Login successful."); }
再来运行看输出结果:
也就是这段代码出问题了:
而从输出结果来看,其pw->pw_name也是正常的:
这是为什么呢?猜有可能是没有权限访问getspnam函数来获得影子文件,因为我们是用root用户执行miniftpd程序,照道理是有权限访问,所以这里需要从头来查找原因,下面来从头将流程梳理一下:
进而转到session模块来了,往下看,这里貌似就发现问题了:
在正式解决之前,咱们先来看一下目前进程分布:
这样肯定就没法得到影子文件了,所以接下来解决它,方法其实很简单:
接下来再编译运行:
而如果故意将密码输错,会提示登录失败么?
这时看效果:
这时可再查看下进程状态:
而vsftpd的进程状态呢?
而我们的FTP服务进程是root用户,这是不对的,应该变成实际登录用户的进程,所以需要调整一下代码,在调整之前需要将调试log恢复:
再次编译运行,查看miniftpd的进程状态:
这次先学到这,里面用到很多系统函数,需要熟悉一下,下回继续~