0 引言 fetchmail是Eric S. Raymond组织编写的一款全功能的 IMAP 和 POP 客户程序. 它允许用户自动地从远程的 IMAP 和 POP 服务器上下载邮件, 并转发到指定邮箱中. 本文主要通过对其中的smtp.c文件中描述的ESMTP认证函数( SMTP_auth )进行分析, 并由此对ESMTP中的认证方式有一个大致的介绍.
1 概述 本文主要内容是分析fetchmail的代码, 而非详细叙述ESMTP的认证过程. 相关内容可以参看[1] [2] [3] 以及相关RFC. fetchmail实现的ESMTP认证方式包括: CRAM-MD5 PLAIN和LOGIN三种. 本文所给出的代码不能单独编译. 本文所用的fetchmail版本为6.3.4
2 精简代码及其注释
这里首先把源代码中用到的一些重要函数的功能介绍一下, 它们的具体实现可以不用关心:
o SockPrintf 函数原型: int SockPrintf(int sock, const char *format, ...) ; 函数定义所在文件: $FETCHMAIL/socket.c( $FETCHMAIL表示fechtmail所在目录.下同 ) 函数功能: 根据format指定的格式, 向文件描述符sock写入字符串. 该函数的参数列表是可变长的. 具体使用方法与printf相同. 函数返回值: 返回写入到sock的字节数.发生错误返回-1
o SockRead 函数原型: int SockWrite(int sock, char *buf, int size); 函数定义所在文件: $FETCHMAIL/socket.c 函数功能: 从文件描述符sock读入最多size个字节的信息到buf起始的内存区域. 函数返回值: 返回真正读入到buf的字节数. 发生错误返回-1
o from64tobits 函数原型: int from64tobits(void *out_, const char *in, int maxlen); 函数定义所在文件: $FETCHMAIL/base64.c 函数功能: 利用base64解码算法, 将字符串in解码. 将解码后的信息存入out_指定的内存区域中. 最多写入maxlen个字节到out_指定的内存区域.存入到out_内的信息不以NULL结尾 函数返回值: 返回实际写入到out_内的字节数. 发生错误返回-1
o to64frombits 函数原型: void to64frombits(char *out, const void *in_, int inlen); 函数定义所在文件: $FETCHMAIL/base64.c 函数功能: 将指定信息进行base64编码. in_表示指定信息存储的内存区域的起始地址, inlen表示指定信息的字节数. out表示将指定信息编码后存储到的位置. 存入到out内的信息是以NULL结尾字符串 函数返回值: 无
o hmac_md5 函数原型: void hmac_md5 (char *password, size_t pass_len, char *challenge, size_t chal_len, unsigned char *response, size_t resp_len); 函数定义所在文件: $FETCHMAIL/cram.c 函数功能: 将password和challenge按照hmac-md5[2]指定的方法计算出摘要, 并将摘要存储在response中. pass_len chal_len和resp_len分别表示password challenge和response的长度 函数返回值: 无
o SMTP_ok 函数原型: int SMTP_ok(int sock, char smtp_mode); 函数定义所在文件: $FETCHMAIL/smtp.c 函数功能: 读取SMTP服务器传来的状态信息. smtp_mode表示使用的协议是SMTP还是ESMTP. 函数返回值: 返回当前服务器状态值
o report 函数原型: void report (FILE *errfp, const char *message, ...); 函数定义所在文件: $FETCHMAIL/report.c 函数功能: 向指定文件写入信息. 主要用于程序报错 函数返回值: 无
这里给出的代码是精简之后的代码. 删除了一些调试信息和原有的注释. static void SMTP_auth(int sock, char smtp_mode, char *username, char *password, char *buf) /* 函数参数说明: * sock: 用于通信的套接字 * smtp_mode: 表示使用的协议是SMTP还是ESMTP. 可以忽略此参数 * username: 用户名 * passowrd: 密码 * buf: 向服务器发送EHLO后服务器的返回信息. 用于判断采用的认证方式 */ { int c; char *p = 0; char b64buf[512]; char tmp[512]; /* 变量声明 */ if (!username || !password) return; /* 非法参数 */ memset(b64buf, 0, sizeof(b64buf)); memset(tmp, 0, sizeof(tmp)); /* 临时变量初始化 */ if (strstr(buf, "CRAM-MD5")) { /* 采用CRAM-MD5的认证方式[3] */ unsigned char digest[16]; /* 临时变量. 用于存储hmac_md5计算出的摘要 */ memset(digest, 0, sizeof(digest)); SockPrintf(sock, "AUTH CRAM-MD5\r\n"); SockRead(sock, smtp_response, sizeof(smtp_response) - 1); strncpy(tmp, smtp_response, sizeof(tmp)); tmp[sizeof(tmp)-1] = '\0'; /* 向服务器提出使用CRAM-MD5认证方式. 读取服务器的回复信息, * 并把回复信息存储到tmp中 */ if (strncmp(tmp, "334 ", 4)) { SMTP_auth_error(sock, GT_("Server rejected the AUTH command.\n")); return; } /* 服务器返回状态码不等于334, 表示服务器端拒绝认证方式. 通过SMTP_auth_error函数报错并返回 */ p = strchr(tmp, ' '); p++; /* 跳过服务器返回的状态码 */ if (from64tobits(b64buf, p, sizeof(b64buf) - 1) <= 0) { SMTP_auth_error(sock, GT_("Bad base64 reply from server.\n")); return; } /* 服务器发送来消息格式是: * 状态码 字符串CRLF * 状态码在前面p=strchr(tmp,' ');p++两句执行后已经被跳过 * CRLF代表"\r\n" * 此时p指向了一个字符串. * 该字符串是由服务器随机产生的, 并且已经经过base64编码 * from64tobits的作用就是对该字符串进行解码. 如果发生错误则报错 */ hmac_md5(password, strlen(password), b64buf, strlen(b64buf), digest, sizeof(digest)); /* 将服务器返回的随机字符串和用户的密码通过hmac-md5[3]算法算出摘要. 并将摘要存储在digest中 */ snprintf(tmp, sizeof(tmp), "%s %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", username, digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15]); /* 此函数执行完后, tmp中则存储着用户名, 之后一个空格, * 然后是用16进制表示的摘要( 通过hmac-md5算法计算出的 ). 摘要用的16进制表示需使用小写字母. */ to64frombits(b64buf, tmp, strlen(tmp)); SockPrintf(sock, "%s\r\n", b64buf); /* 将tmp中存储的内容进行base64编码, 并传递给服务器端 */ SMTP_ok(sock, smtp_mode); /* 通过SMTP_ok函数查看是否正常通过认证 */ } else if (strstr(buf, "PLAIN")) { /* 采用PLAIN认证方式 */ int len; snprintf(tmp, sizeof(tmp), "^%s^%s", username, password); /* 将用户名和密码中间用'^'字符分割, 并将'^'字符至于用户名前 */ len = strlen(tmp); for (c = len - 1; c >= 0; c--) { if (tmp[c] == '^') tmp[c] = '\0'; } /* 将tmp中的'^'内容转换成'\0'. 换句话说, tmp将是以'\0'开头, * 而用户名和密码之间有一个'\0' */ to64frombits(b64buf, tmp, len); SockPrintf(sock, "AUTH PLAIN %s\r\n", b64buf); /* 将tmp中存储的内容前加上"AUTH PLAIN "后进行base64编码, 并传递给服务器端 */ SMTP_ok(sock, smtp_mode); /* 通过SMTP_ok函数查看是否正常通过认证 */ } else if (strstr(buf, "LOGIN")) { /* 采用LOGIN认证方式 */ SockPrintf(sock, "AUTH LOGIN\r\n"); SockRead(sock, smtp_response, sizeof(smtp_response) - 1); strncpy(tmp, smtp_response, sizeof(tmp)); tmp[sizeof(tmp)-1] = '\0'; /* 向服务器提出使用LOGIN认证方式. 读取服务器的回复信息, * 并把回复信息存储到tmp中 */ if (strncmp(tmp, "334 ", 4)) { SMTP_auth_error(sock, GT_("Server rejected the AUTH command.\n")); return; } /* 服务器返回状态码不等于334, 表示服务器端拒绝认证方式. 通过SMTP_auth_error函数报错并返回 */ p = strchr(tmp, ' '); p++; /* 跳过服务器返回的状态码 */ if (from64tobits(b64buf, p, sizeof(b64buf) - 1) <= 0) { SMTP_auth_error(sock, GT_("Bad base64 reply from server.\n")); return; } /* 服务器返回一个由base64编码的字符串. * 该字符串在之后的认证过程中不会用到. * 因此此处仅仅是查看该字符串的合法性, 而不必对其内容进行分析 */ to64frombits(b64buf, username, strlen(username)); SockPrintf(sock, "%s\r\n", b64buf); /* 将用户名进行base64编码后发送给服务器.*/ SockRead(sock, smtp_response, sizeof(smtp_response) - 1); strncpy(tmp, smtp_response, sizeof(tmp)); tmp[sizeof(tmp)-1] = '\0'; /* 读取服务器的回复信息, 并并把回复信息转存到tmp中 */ p = strchr(tmp, ' '); /* 跳过服务器的状态码 */ if (!p) { SMTP_auth_error(sock, GT_("Bad base64 reply from server.\n")); return; } /* 服务器返回的格式非法, 程序报错 */ p++; memset(b64buf, 0, sizeof(b64buf)); if (from64tobits(b64buf, p, sizeof(b64buf) - 1) <= 0) { SMTP_auth_error(sock, GT_("Bad base64 reply from server.\n")); return; } /* 服务器返回的信息格式是一个状态码加一个空格, 之后是一个由base64编码后的字符串 * 同样, 这个字符串在下面的认证过程中不会被用到, 因此此处也仅仅是检查其合法性 */ to64frombits(b64buf, password, strlen(password)); SockPrintf(sock, "%s\r\n", b64buf); /* 将密码进行base64编码后发送给服务器 */ SMTP_ok(sock, smtp_mode); /* 通过SMTP_ok函数查看是否正常通过认证 */ } return; }
3 认证过程分析 fetchmail中实现了ESMTP的三个认证方式: CRAM-MD5 PLAIN和LOGIN. 下面对这三种认证方式的流程进行一个总结.
CRAM-MD5: 客户端首先向服务器端发送一个字符串: "AUTH[SPACE]CRAM-MD5[CRLF]".其中[SPACE]表示一个空格;[CRLF]表示回车换行符, 即"\r\n". 下同. 如果服务器拒绝认证方式, 则返回一个字符串: "[NUM][SPACE]str". 其中[NUM]为三位数字的服务器状态码( 下同 ).当状态码不等于334表示拒绝认证方式. str是一个服务器端定义的字符串, 用于描述错误. 如果服务器接受认证方式, 则返回一个字符串: "[NUM][SPACE]str_base64".其中str_base64是一个随机字符串经过base64编码后的字符串. 客户端收到服务器的信息后, 执行如下操作: 首先利用base64解码算法将str_base64解码. 解码后的字符串存入str * call base64_decode * input str_base64 * output str
之后利用hmac-md5算法计算出一个摘要digest * call hmac_md5 * input password, str * output digest
将摘要用小写字母的16进制表示, 并把字符串"username "与它合并, 成为字符串tmp * string tmp = 'username digest'
将tmp进行base64编码 * call base64_encode * input tmp * output tmp_base64 最后, 客户端向服务器端发送字符串: "tmp_base64[CRLF]" 根据服务器的返回判断是否认证成功
PLAIN: 客户端首先做如下操作: * string tmp = '^username^password' * for each character in tmp * tmp[i] = '\0' where tmp[i] == '^' * call base64_encode * input tmp * output tmp_base64 最后, 客户端向服务器端发送字符串: "tmp_base64[CRLF]" 根据服务器的返回判断是否认证成功
LOGIN: 客户端首先[1] [2] 下一页
 |
频道声明:本频道的文章除部分特别声明禁止转载的专稿外,可以自由转载.但请务必注明出出处和原始作者 文章版权归本频道与文章作者所有.对于被频道转载文章的个人和网站,我们表示深深的谢意。
| 原始作者:佚名 |
录入时间:2007-1-2 3:23:13 |
| 信息来源:不详 |
投稿信箱:itqoo@126.com |
|
|
 |
|