問題描述
falcon發(fā)送報警郵件,使用的是回調(diào)的方式,將需要發(fā)送的郵件推送到某個郵件接口,而該接口就是由mail-provider組件提供的
詳見:https://github.com/open-falcon/mail-provider
我們公司使用的是exchange郵件服務(wù)組件,但在實際測試的過程中發(fā)現(xiàn),告警郵件無法發(fā)送,將mail-provider組件中發(fā)送郵件的代碼提取出來后單獨運行也無法發(fā)送郵件
深入代碼
深入查看代碼,其郵件用的是一個封裝好的庫:https://github.com/toolkits/smtp ,通過對該開源庫的分析,找到了問題所在,先科普幾個smtp的知識
smtp服務(wù)
如何調(diào)用其發(fā)送一封郵件
其底層 跟我們平時telnet訪問郵件的587 25端口類似,都是先發(fā)送一個EHLO,然后再訪問
登陸方式
登陸方式 目前profix分為:{"LOGIN", "PLAIN", "CRAM-MD5"},但是Exchange為:{GSSAPI NTLM LOGIN}
只有LOGIN的登陸方式是共有的,其中toolkits/smtp并未將LOGIN的登陸方式寫進(jìn)去,只是很簡單了實現(xiàn)了PLAIN的登陸方式,導(dǎo)致與exchange不兼容
如何查看支持的登陸方式?
telnet 連接上郵件服務(wù)器的587端口后輸入EHLO,在AUTH那里能看到開放的登陸方式
EHLO
250-mail.gridsum.com Hello [202.103.147.235]
250-SIZE 209715200
250-PIPELINING
250-DSN
250-ENHANCEDSTATUSCODES
250-STARTTLS
250-AUTH GSSAPI NTLM LOGIN
250-8BITMIME
250-BINARYMIME
250 CHUNKING
LOGIN
使用login方式的驗證序列如下 (C:表示Client,S:表示Server)
C:auth login ------------------------------------------------- 進(jìn)行用戶身份認(rèn)證
S:334 VXNlcm5hbWU6 ----------------------------------- BASE64編碼“Username:”
C:Y29zdGFAYW1heGl0Lm5ldA== ----------------------------------- 用戶名,使用BASE64編碼
S:334 UGFzc3dvcmQ6 -------------------------------------BASE64編碼"Password:"
C:MTk4MjIxNA== ----------------------------------------------- 密碼,使用BASE64編碼
S:235 auth successfully -------------------------------------- 身份認(rèn)證成功
(Base64 編碼計算:http://tool.114la.com/base64.html )
PLAIN
基于明文的SMTP驗證,詳見:http://www.ietf.org/internet-drafts/draft-ietf-sasl-plain-08.txt
其發(fā)送用戶名與口令的格式應(yīng)該是“<NULL>tim<NULL>tanstaaftanstaaf”?!皌im”是用戶名,后邊的字符串是口令,NULL是ASCII的0(所以無法使用telnet登錄)。
CARM-MD5
CRAM-MD5即是一種Keyed-MD5驗證方式,CRAM是“Challenge-Response Authentication Mechanism”的所寫。所謂Keyed-MD5,是將Clieng與Server共享的一個Key作為一部分MD5的輸入,正好郵件系統(tǒng)的用戶口令可以作為這個Key。具體的交互如下:
S: * OK IMAP4 Server
C: A0001 AUTHENTICATE CRAM-MD5
S: + PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+ -------- Server發(fā)送BASE64編碼的Timestamp、Hostname等給Client
C: dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw ------- Client將收到的信息加上用戶名和口令,編碼為BASE64發(fā)送給Server
S: A0001 OK CRAM authentication successful ----------- Server使用該用戶的口令進(jìn)行MD5運算,如果得到相同的輸出則認(rèn)證成功
Keyed-MD5的計算公式為:
MD5 ( (tanstaaftanstaaf XOR opad),MD5((tanstaaftanstaaf XOR ipad), 1896.697170952@postoffice.reston.mci.net) ),其中
MD5()為標(biāo)準(zhǔn)的MD5算法,“tanstaaftanstaaf”為用戶口令,“1896.697170952@postoffice.reston.mci.net”是從Server發(fā)送過來的Timestamp和Hostname等,ipad和opad為Keyed-MD5算法特定的常數(shù)。上面的公式得出的digest為"b913a602c7eda7a495b4e6e7334d3890",加上用戶名,即"tim b913a602c7eda7a495b4e6e7334d3890"進(jìn)行BASE64的編碼,得到上面發(fā)送給Server的“dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw”。
修復(fù)
重寫toolkit/smtp這個服務(wù),新增Login的認(rèn)證方式,同時必須打開TLS,防止密碼泄露
實現(xiàn)一個Login的auth,只需要實現(xiàn):Start與Next方法即可
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS {
advertised := false
for _, mechanism := range server.Auth {
if mechanism == "LOGIN" {
advertised = true
break
}
}
if !advertised {
return "", nil, errors.New("gomail: unencrypted connection")
}
}
if server.Name != a.host {
return "", nil, errors.New("gomail: wrong host name")
}
return "LOGIN", nil, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
switch {
case bytes.Equal(fromServer, []byte("Username:")):
return []byte(a.username), nil
case bytes.Equal(fromServer, []byte("Password:")):
return []byte(a.password), nil
default:
return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer)
}
}