您的位置:首页 > 编程语言 > Java开发

MSN协议分析以及Java实现MSN登陆

2008-11-13 15:37 2823 查看
一、MSN协议分析部分

1.1 基本介绍

MSN是微软推出的IM工具,他的通信协议是微软自己提出的MSNP(即MSN Protocol)。当前MSN协议最高版本为MSNP18,但可获取的资料很少。这里仅仅以MSNP12做说明。

命令是服务器与客户交互的一种方式。MSNP12的命令使用纯ASCII码,对非ASCII码使用URL编码。命令的一般格式是:

XXX TrID PARAM1 PARAM2…

其中,XXX是一个3字符的命令串,TrID是一个流水号,PARAMx是参数。最简单的命令没有流水号和参数。一个典型的MSNP命令形如

VER 1 MSNP18
MSNP17 MSNP16 CVR0

       每个发送的MSN命令以回车换行作为一条命令的结束。具体命令的含义和使用参数可以参见参考资料[1]所示的MSN协议非官方wiki。

 

1.2 利用VER命令测试当前服务器支持版本

在服务器的messenger.hotmail.com的1863端口发送命令VER来测试。在本文中用”>”符号表示发送的命令,用”<”符号表示收到的命令。

>VER 1 MSNP18
CVR0

<VER 1 MSNP18

由以上可以看出,服务器当前支持MSNP18。类似的,可以发送多个参数的VER命令到服务器

>VER 1 MSNP18
MSNP12 CVR0

<VER 1 MSNP18
MSNP12

 

1.3 MSN登陆过程

MSN登录可以分为2个步骤:

(1)   
向服务器messenger.hotmail.com的1863端口请求实际登录地址

(2)   
在获得的新的服务器上进行登录

因此,在MSN登录过程可以看到2次产生登录请求,MSN服务器在第二次登陆时才真正去做用户认证。2次登陆过程是都是向服务器发送VER, CVR和USR这3个命令,然后处理服务器反馈的命令来进行下一步处理。

发送信息和返回信息的过程可以是异步的,也可以是同步的。这意味着用户可以发送一个命令后,进行阻塞等待回复。客户端收到回复后,再发出第二个命令。客户端也可以连续发送3个命令至服务器,而后再进行阻塞等待,获取服务器的回复后再进行后续处理。

两次登陆的不同点在于两次发送USR命令后得到的回复命令。在第一个服务器(messenger.hotmail.com)上发送USR命令后,服务器反馈时返回XFR命令。XFR命令表示连接中止。XFR的参数中包含了客户端第二次需要登陆的MSN服务器地址。而后客户端可以进行在新服务器上进行登录。在第二个服务器上发送USR命令后,服务器不再返回XFR命令,而是返回USR命令。返回的USR命令要求客户端提供用户名和密码,需要在认证服务器上进行登录,而后返回认证相关信息。

第一次登陆过程的过程的收发命令类似如下

>VER 1 MSNP12
CVR0

<VER 1 MSNP12

>CVR 2 0x0804
winnt 5.1 i386 MSNMSGR 8.1.0178 MSMSGS csdnjay@hotmail.com

<CVR 2 8.1.0178
8.1.0178 8.1.0178 http://msgruser.dlservice.microsoft.com/download/5/6/4/5646481F-33EF-4B08-AF00-4904F7677B89/ZH-CHS/Install_WLMessenger.exe http://get.live.com/cn

>USR 3 TWN I csdnjay@hotmail.com

<XFR 3 NS
207.46.108.93:1863 0 207.46.28.94:1863

这里得知第二次欲登陆的服务器为207.46.108.93,端口为1863

第二次登陆过程收发命令类似如下

>VER 1 MSNP12
CVR0

<VER 1 MSNP12

>CVR 2 0x0804
winnt 5.1 i386 MSNMSGR 8.1.0178 MSMSGS csdnjay@hotmail.com

<CVR 2
8.1.0178 8.1.0178 8.1.0178 http://msgruser.dlservice.microsoft.com/download/5/6/4/5646481F-33EF-4B08-AF00-4904F7677B89/ZH-CHS/Install_WLMessenger.exe http://get.live.com/cn

>USR 3 TWN I csdnjay@hotmail.com

<USR 3 TWN S
ct=1225934765,rver=5.5.4177.0,wp=FS_40SEC_0_COMPACT,lc=1033,id=507,ru=http:%2F%2Fmessenger.msn.com,tw=0,kpp=1,kv=4,ver=2.1.6000.1,rn=1lgjBfIL,tpf=b0735e3a873dfb5e75054465196398e0

>USR 4 TWN S
t=9zmW8WUaNGes6RPIvobJZxog26Re6uEiF8NfCot1WADagYV*xBD!yobWolu4iQma712fndE2L0cI1t0zCErX*yCKKFA9SnStf5SgUDBm3c7wxTEphaA*Wy*bZdj1nfiuu!&p=9m2c8ZG8!FTK82I9Gd15dopRFWPlLk3mpIJ2*PdD3!IP8CFu8I5wWGkLslOu1QC3EEaZQnIqwBZTtzpWWzu18FYFFY75fcvzb85e649MYCwu8Mi3AyIv8R9PsyYdTrE6tbUo3fzlu5RZmE2X0RXv!yO8yemZ!Xwm8s2nYsh3E3HqmNNRBEBPKZ7g$$

<USR 4 OK csdnjay@hotmail.com
1 0

<SBS 0 null

<MSG Hotmail
Hotmail 537

[ size = 537,
num = 537]

MIME-Version:
1.0

Content-Type:
text/x-msmsgsprofile; charset=UTF-8

LoginTime:
1225934767

EmailEnabled: 1

MemberIdHigh:
442365

MemberIdLow:
-1451244322

lang_preference:
2052

preferredEmail:

country: CN

PostalCode:

Gender:

Kid: 0

Age:

BDayPre:

Birthday:

Wallet:

Flags:
1073742915

sid: 507

MSPAuth:
9zmW8WUaNGes6RPIvobJZxog26Re6uEiF8NfCot1WADagYV*xBD!yobWolu4iQma712fndE2L0cI1t0zCErX*yCKKFA9SnStf5SgUDBm3c7wxTEphaA*Wy*bZdj1nfiuu!&p

ClientIP:
222.191.237.170

ClientPort:
22030

ABCHMigrated: 1

MPOPEnabled: 0

由以上的收发消息列表可以看出,第二次登陆在发送USR命令后,服务器不在返回XFR消息,而是返回USR命令。注意在返回的USR命令中包含类似的字符串

ct=1225934765,rver=5.5.4177.0,wp=FS_40SEC_0_COMPACT,lc=1033,id=507,ru=http:%2F%2Fmessenger.msn.com,tw=0,kpp=1,kv=4,ver=2.1.6000.1,rn=1lgjBfIL,tpf=b0735e3a873dfb5e75054465196398e0

客户端可以利用这个字符串做MSN认证具体获取ticket的方法,参见第二部分Java实现。若认证成功,客户端收到类似的USR 4 OK csdnjay@hotmail.com 1 0的命令。这时服务器将会再次返回SBS 0 null命令和MSG Hotmail Hotmail 537命令。MSG命令的第2个参数537表示,服务器将发送537个字节来进一步该账户相关信息。

此时MSN登陆过程的全过程已经结束。

 

1.4 获取好友列表和分组信息

为了获取MSN帐户好友以及改帐户分组的相关信息,还要发送SYN命令进行数据同步,这样MSN服务器才能发送好友列表信息。

>SYN 5 0 0

<SYN 5 2008-10-29T03:24:39.063-07:00
2008-10-27T23:57:40.58-07:00 2 3

<GTC A

<BLP BL

<PRP MFN ?

<PRP MBE N

<PRP WWE 0

<LSG 朋友 c274104c-8db9-47fc-9d0d-07d6fbea0ff1

<LSG 家人 24a9c4c5-221a-43b2-bc80-9935e35cf11c

<LSG 同事 4c60904d-8a23-42d8-a340-b1033e6877ef

<LST N=csdnss@hotmail.com
F=CSDNss@hotmail.com C=83c611d9-1f58-4577-b70c-3cb154e3a8a0 11 1 c274104c-8db9-47fc-9d0d-07d6fbea0ff1

<BPR HSB 1

<LST
N=chx1477@hotmail.com F=chx1477@hotmail.com C=1c0bf657-7ded-4837-a69d-cc6803543929 11 1 4c60904d-8a23-42d8-a340-b1033e6877ef

以上便是发送SYN命令后和发送后的服务器反馈信息。由此可以分析出该帐号的好友列表和分组信息。LSG表示该MSN用户分组信息,后面的字符串(例如,第一个LST中csdnss@hotmail.com 最后的c274104c-8db9-47fc-9d0d-07d6fbea0ff1)表示分组ID。LST表示联系人信息,其中N表示好友的邮件,F表示可能的昵称,整条消息最后的字符串(例如c274104c-8db9-47fc-9d0d-07d6fbea0ff1)表示该联系人所在的分组。在上例中,该帐号有三个分组,分别为朋友、家人和同事。字符串c274104c-8db9-47fc-9d0d-07d6fbea0ff1表示朋友分组,联系人csdnss@hotmail.com在朋友分组中。4c60904d-8a23-42d8-a340-b1033e6877ef表示同事分组,联系人chx1477@hotmail.com在同事分组中。

 

1.5 获取好友具体信息

当然仅仅依靠SYN返回联系人的信息是不够全面的。SYN命令仅返回所有的组,联系人列表以及部分联系人的昵称。若要想进一步获取更多联系人的相关信息,可以继续发送CHG 命令,以获取联系人昵称和签名信息。

>CHG 6 NLN 0x5004802C

<BPR HSB 1

<CHG 6 NLN 0

<MSG Hotmail
Hotmail 289

[MIME-Version:
1.0

Content-Type:
text/x-msmsgsinitialmdatanotification; charset=UTF-8

 

Mail-Data:
<MD><E><I>1</I><IU>0</IU><O>0</O><OU>0</OU></E><Q><QTM>409600</QTM><QNM>204800</QNM></Q></MD>

Inbox-URL:
/cgi-bin/HoTMaiL

Folders-URL:
/cgi-bin/folders

Post-URL: http://www.hotmail.com
]

<ILN 6 NLN csdn_jay@163.com
Jin%20Jian 2254295084 %3Cmsnobj%20Creator%3D%22csdn_jay%40163.com%22%20Type%3D%223%22%20SHA1D%3D%22dlhdylsYA70%2Fzci5JUbQo5OYd1o%3D%22%20Size%3D%227064%22%20Location%3D%220%22%20Friendly%3D%22agBhAHkAAAA%3D%22%2F%3E

<UBX csdn_jay@163.com
123

[<Data><PSM>AGAIN</PSM><CurrentMedia></CurrentMedia><MachineGuid>{6B9562F0-1957-432A-8ACD-A04CD7961841}</MachineGuid></Data>]

<ILN 6 NLN
chx1477@hotmail.com (R)流浪狗 1985859628
%3Cmsnobj%20Creator%3D%22chx1477%40hotmail.com%22%20Type%3D%223%22%20SHA1D%3D%22cmJCJjUVJ%2Bx1g5BUj5TlTOAoFIo%3D%22%20Size%3D%2218476%22%20Location%3D%220%22%20Friendly%3D%220W5%2FZwAA%22%2F%3E

<UBX
chx1477@hotmail.com 118

[<Data><PSM></PSM><CurrentMedia></CurrentMedia><MachineGuid>{7840D262-7F06-46B8-9573-114AEE3530F5}</MachineGuid></Data>]

以上便是在发送CHG和发送后的服务器反馈信息。ILN命令中第2个参数为联系人email,第3个参数为联系人的昵称。UBX命令后面跟着的XML数据表示联系人的具体信息,其中在PSM数据段中表示就是联系人签名。在本例中chx1477@hotmail.com联系人的昵称为(R)流浪狗,这个联系人没有签名。csdn_jay@163.com联系人的昵称为Jin Jian,而签名为AGAIN。

       这里需要说明的是UBX命令后面的数字是指XML数据的字节数。有的联系人的签名中可能存在非英文字符(例如中文字符,法文字符或日文字符等),这样在程序读取时候需要做特殊处理。

 

二、Java核心实现

2.1 Socket连接部分

Socket socket =
new Socket(address, port);

InputStream socketInputStream
= new DataInputStream(socket.getInputStream());

OutputStream socketOutputStream
= socket.getOutputStream();

OutputStream
buffered = new BufferedOutputStream(socketOutputStream);

OutputStreamWriter
writer = new OutputStreamWriter(buffered, "ASCII");

 

2.2 发送消息部分

void
writeCommandToSocket(String command)

              throws IOException,
UnsupportedEncodingException {

       writer.write(command + "/r/n");

       writer.flush();

}

 

2.3 获取消息部分

String
readLine() throws IOException {

       byte[] ba = new byte[1024];

 

       int n = 0;

       while(n < 1024) {

              int b =
getSocketInputStream().read();

              if((b == '/r') || (b == '/n')) {

                     if(b == '/r') {

                            getSocketInputStream().skip(1);

                     }

                     break;

              } else {

                     ba
= (byte)b;

                     n ++;

              }

       }

      

       return new String(ba, 0, n,
"UTF-8");

}

 

2.4 获取ticket部分

客户端通过https连接MSN认证服务器login.live.com,之后便可以获取登陆凭证ticket。

在连接时需要设置Authorization属性为

Passport1.4
OrgVerb=GET,OrgURL=https%3A%2F%2Flogin.live.com%2Flogin2.srf,sign-in=csdnjay%40hotmail.com,pwd=abcdefg,ct=1226474476,rver=5.5.4177.0,wp=FS_40SEC_0_COMPACT,lc=1033,id=507,ru=http:%2F%2Fmessenger.msn.com,tw=0,kpp=1,kv=4,ver=2.1.6000.1,rn=1lgjBfIL,tpf=b0735e3a873dfb5e75054465196398e0

上述字符串由如下部分组成

Passport1.4
OrgVerb=GET,OrgURL=" + encode(visitUrl) + ",sign-in=" +
encode(passport) + ",pwd=" + encode(password) + "," + challengeStr

其中chanllengeStr即在收到在第二次登陆服务器发送USR命令后返回的USR命令的中的字符串(具体内容参见1.3节所示),visitUrl为https://login.live.com/login2.srf。若这一过程认证失败,则ticket获取失败。

具体的代码如下

private String
getLoginTicket(String visitUrl, String passport,

                
String password, String challengeStr)

                
throws IOException {

Pattern ticketPattern =
Pattern.compile(".*from-PP='([^']*)'.*");

      HttpsURLConnection conn = null;

      try {

          conn = (HttpsURLConnection) new
URL(visitUrl).openConnection();

          conn.setUseCaches(false);

         
conn.setRequestProperty("Authorization",

                  "Passport1.4
OrgVerb=GET,OrgURL="

                          + encode(visitUrl)

                          +
",sign-in="

                          + encode(passport)

                          + ",pwd="

                          + encode(password) +
","

                          + challengeStr);

          switch (conn.getResponseCode()) {

              case 200: //success

                  Matcher matcher =
ticketPattern.matcher(conn

                         
.getHeaderField("Authentication-Info"));

                  if (matcher.matches()) {

                      return matcher.group(1);

                     }

                     return null;

                     case 302: //redirect

                 
       visitUrl =
conn.getHeaderField("Location");

                         return
getLoginTicket(visitUrl, passport, password, challengeStr);

                     case 401: //failed

                         return null;

                     default:

                
}

            
} finally {

                
if (conn != null) {

                     conn.disconnect();

                
}

            
}

            
return null;

         }

 

三、注意事项

【注意事项一】关于DataInputStream

在socket收到消息时,需要注意返回的内容可能有非英文字符,例如1.5节所提及到的UBX命令。在收到UBX消息时候,该消息已经指定了下面将要收到的字节数目。由于中文字符的是双字节或三字节,若直接利用socket. getInputStream方法进行读取,获取的中文部分出错或者造成读取阻塞。因此在2.1节所示的连接部分需要把InputStream利用DataInputStream进行封装。

InputStream socketInputStream
= new DataInputStream(socket.getInputStream());

 

【注意事项二】关于发送命令

       做MSN协议实现的时候,需要在命令结束发送/r/n以表示命令结束,如2.2节所示的实现,在命令后追加/r/n。

 

【注意事项三】关于接收命令

       由于MSN服务器有时发送来的消息的数目是不确定的,例如1.4节所示的发送SYN命令后反馈得到的LSG和LST命令和1.5节所示的发送CHG命令后反馈得到的ILN命令。因此,读取服务器发送来的消息时,又不能以/r/n作为消息的结束标志。这样如果一直等待,就会在读取过程造成阻塞。

解决这个问题有2种方式。第一是利用多线程建立事件响应来处理收到的反馈。第二是先判断数据流中是否有可用信息。若无可用信息,则等待一段时间,再进行下一次判断。若连续多次都无可用信息,则退出循环。

下面给出第二种方式的具体实现

int n = 0;

while(true) {

       if(getSocketInputStream().available()
> 0) {

              n = 0;

              String command =
readLineFromSocket();

              //收到命令做处理

       } else {

              try {

                     Thread.sleep(1000);

              } catch (InterruptedException e) {

                     e.printStackTrace();

              }

              n ++;

              System.out.println("等待" + n +"秒");

              if(n >= waitingSeconds) break;

       }

}

       上面这个程序表示在连续等待waitingSeconds秒后,此时仍然没有可以读取的数据,程序退出且不在读取。

 

【注意事项四】关于socket接收和发送

在做socket发送命令给服务器的时候,发送端要有唯一的入口。这就意味着在实现时只能从一个统一控制的地方获取inputStream和writer,而不能在程序中随时通过socket.getInputStream或经过包装过的reader中获取相应的数据。最后需要指出的是reader是面向字符的,而InputStream是面向字节的。

 

四、参考资料以及相关文档

[1] 介绍MSN协议非官方wiki http://msnpiki.msnfanatic.com/index.php/Main_Page

[2] 介绍DataInputStream的使用 http://www.javanb.com/j2se/1/5298.html

[3] 介绍MSN协议简介http://blog.csdn.net/bripengandre/archive/2008/03/20/2199924.aspx

[4] 介绍MSNP12的一种Java实现 http://java-jml.sourceforge.net/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息