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)
", strlen("220 (miniftpd 0.1)
"));
    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]
", sess->cmdline);
        // 去除

        str_trim_crlf(sess->cmdline);
        printf("cmdline=[%s]
", sess->cmdline);
        // 解析FTP命令与参数
        str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
        printf("cmd=[%s] arg=[%s]
", 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)
", strlen("220 (miniftpd 0.1)
"));
    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]
", sess->cmdline);
        // 去除

        str_trim_crlf(sess->cmdline);
        printf("cmdline=[%s]
", sess->cmdline);
        // 解析FTP命令与参数
        str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
        printf("cmd=[%s] arg=[%s]
", 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)
", strlen("220 (miniftpd 0.1)
"));
    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]
", sess->cmdline);
        // 去除

        str_trim_crlf(sess->cmdline);
        printf("cmdline=[%s]
", sess->cmdline);
        // 解析FTP命令与参数
        str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
        printf("cmd=[%s] arg=[%s]
", 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.
", strlen("331 Please specify the password.
"));
}

编译运行看下效果:

接下来处理do_pass(),继续来看一下正常的登录流程:

static void do_pass(session_t *sess)
{
    // PASS 123456
    writen(sess->ctrl_fd, "230 Login successful.
", strlen("230 Login successful.
"));
}

编译运行:

当然这里还没有对用户名和密码进行真实的校验,下面则开始实现,先看下密码出错,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)
", strlen("220 (miniftpd 0.1)
"));
    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]
", sess->cmdline);
        // 去除

        str_trim_crlf(sess->cmdline);
        printf("cmdline=[%s]
", sess->cmdline);
        // 解析FTP命令与参数
        str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
        printf("cmd=[%s] arg=[%s]
", 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.
", strlen("530 Login incorrect.
"));
        return;
    }
    writen(sess->ctrl_fd, "331 Please specify the password.
", strlen("331 Please specify the password.
"));
}

static void do_pass(session_t *sess)
{
    // PASS 123456
    writen(sess->ctrl_fd, "230 Login successful.
", strlen("230 Login successful.
"));
}

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
", 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]
", 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的进程状态:

这次先学到这,里面用到很多系统函数,需要熟悉一下,下回继续~

原文地址:https://www.cnblogs.com/webor2006/p/4601010.html