一、MX记录的应用
Email是迄今为止互联网上最成功的应用了,试想一个触网者如果没有属于自己的Email邮箱,那将难以称作“网民”。互联网网上同Email相关的应用也增多。我们编写网络应用时,经常需要涉及将Email应用嵌入到自己的应用程序中。这种应用往往是将第三方的Email相关组件拿来使用,完成Email的撰写、发送、收娶解码。在发送Email的过程中,NT平台一般采用NT系统自带的CDO组件来完成,而这类组件往往依靠在本机的服务或一个特定的SMTP服务器来实现,不能够确保信件是否到达,而且由于存在中继延迟,还有丢失信件的可能。所以很多高手采用直接向目的邮箱的服务器投递Email的办法来实现Email发送,而实现此功能关键在于三个要点:
1、邮件的MIME编码
2、SMTP会话控制
3、MX记录的获龋
通过我的了解,互联网上关于1、2两点的文章很多,相应的参考代码很多,而对于如何定位目标邮箱接收服务器-获得邮箱域名的MX记录却很少详细讨论,搜索相关的组件得到一个ASP环境的服务器组件(收费),没有详细的源码可以参考。基于此,我通过学习Java语言的机会编写了一个类似的组件,以助大家了解这方面的细节。
二、DNS记录分类以及原理
我们知道域名系统是互联网的基石,我们能够通过亲切的类似yahoo.com这样的名字来访问自己感兴趣的网站完全是域名系统的功劳。当您在浏览器敲下www.yahoo.com时,您的浏览器是不知道这个域名指向的网页该往哪儿去取,便会询问设定好的域名解析服务器(DNS),域名解析服务器会寻找自己的记录库是否存在请求的域名记录,如果存在会直接返回,不存在就向上一级域名解析服务器请求(转发请求),直到最后找到此域名的解析记录返回给您的浏览器,告诉您的浏览器该到哪一个IP地址上去获得相关的内容。这样一个将互联网域名转换为对应的IP的过程叫做解析。
域名记录有多种,常见的有 A(地址)记录、别名(CNAME)记录、MX(邮件交换)记录。A记录就是一个域名对应一个IP的记录,刚才提到的www.yahoo.com对应某个IP地址即是A记录的查询过程。别名记录主要用于用一个已经存在A记录的域名替代需要解析的域名,譬如www.yahoo.com查询时会得到www.yahoo.akadns.net 的别名值,而进一步解析www.yahoo.akadns.net会得到IP地址64.58.76.177。邮件交换记录是用于邮件投递时采用的。譬如您需要发送一封到 abc@yahoo.com的Email,那么一般情况下,您的ISP的SMTP服务器(专业术语成为邮件代理,即代替用户直接投递邮件)会接受您发出的信件,然后SMTP服务器查询yahoo.com的MX记录,看应当将邮件发送到哪一个服务器上去,如果查询到yahoo.com的MX记录,那么您的ISP的SMTP服务器会建立到MX记录指定的服务器的TCP连接,并传出您的Email到目的服务器上去,完成Email的投递过程。
如果能够获得某个域名的MX记录,即获得了向目的邮箱直接投递的途径,而要获得这个MX记录,您必须能够正确的向域名服务器DNS提出您的请求,同时也要能够理解DNS返回的信息,读出自己需要的内容。这里面的细节相对复杂,我这里简单介绍一下:
客户向DNS的53端口发出的UDP报文,然后服务器查询(中间可能经过对此转发或者称作递归查询)后发回客户机需要的记录,也是UDP报文。此类报文的一般格式为:
| 2字节的标识 | 2字节的标志 | 2字节的问题个数 | 2字节的资源记录数 | 2字节的授权资源记录个数 | 2字节额外的资源记录数 | 查询的域名(不固定长度) | 针对请求的应答资源记录(长度不固定)| 授权资源记录(长度不固定) | 额外记录信息(长度不固定)
标识字段用于指出报文的编号,一般由客户指定,DNS服务器返回信息时带上此标识,告诉客户端回答的是哪一个请求。
标志字段的16比特划分为8个子字段,从左至右(高位到低位)分别为:
QR 1 bit :0 查询报文 1 响应报文
Opcode 4 bit :通常为0,表示标准查询 ,1 反向查询,2 服务器状态查询
AA 1 bit :用于服务器返回报文,表示是否是授权回答
TC 1 bit :由于UDP自身长度限制,往往会截断512字节后的内容,该位表示是否可截断
RD 1 bit :该为用于在查询报文中设置,并由服务器响应报文中返回。该位告诉服务器必须处理此查询,如果该位为0,且服务器返回的授权回答个数为0,那么服务器必须返回一个能够解答该查询的其他服务器的列表
RA 1 bit :如果服务器支持递归,那么服务器在响应报文中设定该位。
随后的3bit必须为0
rcode 4 bit :最后为返回码,0 无差错,3 名字差错,即在服务器上不存在要查询的域名的记录,一般用于从最终的授权名字服务器返回。
查询问题部分由查询名字 查询类型 查询类组成。查询名字由多个标识符的序列组成,每一个标识首字节说明该标识符的长度,最终由字节0表示名字结束。譬如cn..yahoo.com由2 c n 5 y a h o o 3 c o m 0组成。如果此域名后面还用到,一般在后面采用压缩格式,那么首字节不是长度了,而是一个最高位为1的字节,一般是0xc0,因为不会出现长度超过64的标识符(由于域名的规定)。压缩格式的标志字节后是该域名的原标识的偏移值。查询类型为2字节,1 表示A记录查询 5 表示CNAME记录查询 15 表示MX记录查询。类表示是否是Internet数据。
应答报文中的应答记录由 域名(长度不固定) 类型(2字节) 类(2字节) 生存时间(4字节,秒数) 资源数据长度(2字节) 资源数据(不固定)。域名的格式同查询域名格式相同。类型、类的解释同查询问题部分。资源数据根据记录类型不同而不同。
如果我们按照上面的格式同时结合自己的需求,向有域名解析功能的服务器发出UDP报问候就可以收到DNS服务器发回的应答,这样我们就可以获得想的记录。所以,主要难点在报文的构造和应答报文的分析。
三、编写MX记录组件
启动VJ。要求建立一个DLL工程,然后修改主类名为自己想要的名字,该工程建立完毕。如果一切无误的话,就会得到一个DLL,具有域名MX记录查询功能。具体代码见后面的源代码分析。关键就是结合自己的查询问题构造查询报文和解释得到的应答报文,同服务器的网络连接方式是UDP方式。
为了调试方便,我添加了一个Main函数入口,这样可以在DOS窗口调用jvew来察看域名MX记录的结果,同时也方便了调试。
四、应用举例及扩充
有了这样一个查询组件,可以直接获得域名的MX记录,可以帮助我们获得投递Email的方法。同时,我们也知道了该域名邮件接收服务器的位置,通过试探不同得用户名,我们还可以获得该域名的邮局存在那些邮箱用户名。你可能奇怪自己的邮箱如何被他人获得,很可能被一些Email搜索器通过查询MX记录,得到邮件服务器的地址,然后通过特别的用户名产生算法得到不同的用户名,一一试探从而得到邮箱。在你的应用程序中加入此功能,可以直接将邮件传递到目标务器,不依赖于服务器上的组件。甚至您可以编写邮件转发服务器,服务于您的用户(一般的发件服务器不对外开放的)。
五、源码及分析
import java.net.*;
import java.io.*;
import java.util.StringTokenizer;
//import java.util.*;
/**
* This class is designed to be packaged with a COM DLL output format.
* The class has no standard entry points, other than the constructor.
* Public methods will be exposed as methods on the default COM interface.
* @com.register ( clsid=20AE856F-E2F8-488E-B41E-753E6BEBD375, typelib=36611559-AFAB-479E-8572-9A0B1F06CCDA )
*/
public class MXDNS
{
private String theDnsServer; //当前DNS服务器的地址
private DatagramPacket outPk; //发送数据包
private DatagramPacket inPk; //接收数据包
private DatagramSocket UDPSocket ; //UDP陶接字
private InetAddress DNSServerIP; //域名解析服务器的IP
private int position,id,length;//分析的DNS记录时需要的变量
private String DMname; //查询的域名
private MXRec mxrecs; //DNS应答的记录
private static int DNS_PORT=53; //DNS服务的端口
private byte[] pkdata=new byte[512] ; //得到512字节的数据包
public MXDNS() //构造函数
{
id=(new java.util.Date()).getSeconds()* 60 * (new java.util.Random()).nextInt();
//获得唯一ID
}
//以下为暴露的属性和方法
public void setDnsServer(String dnsserver)
{
theDnsServer=dnsserver; //设定当前的DNS服务器的名字或者IP
}
public String getMXRecords(String dm) //获得所有DNSMX记录数组
{
return(getMXRecords(dm,theDnsServer));
}
public String getMXRecords(String dm,String DNSServer)
{
try
{
DNSServerIP=InetAddress.getByName(DNSServer) ;
//获得DNS服务器的InetAddress
outPk=new DatagramPacket(pkdata,pkdata.length,DNSServerIP,DNS_PORT );
//外发的数据报
UDPSocket=new DatagramSocket(DNS_PORT); //数据包端口
makeDNSQuery(id,dm); //产生查询报文
UDPSocket.send(outPk); //发送数据报文
inPk=new DatagramPacket(pkdata,pkdata.length );
UDPSocket.receive(inPk); //接收返回的应答报文
pkdata=inPk.getData();//获得字接
length=pkdata.length;
}
catch(UnknownHostException ue)
{
}
catch(SocketException se)
{
}
catch(IOException ioe)
{
}
return(getResponse()); //分析返回的数据报文,得到记录结果
}
public void makeDNSQuery(int id,String dm)
{//在PKdate byte数组中产生查询数据
for(int i=0;i<512;i++)
{
pkdata[i]=0;
}
pkdata[0]=(byte)(id>>8);
pkdata[1]=(byte)(id & 0xff); //查询的标识2字节
pkdata[2]=(byte)1; //Qrbit位为1,表示是查询报文
pkdata[3]=(byte)0;
pkdata[4]=(byte)0;
pkdata[5]=(byte)1; //1个问题
pkdata[6]=(byte)0; //资源记录数、授权资源记录、额外资源记录均为0个,因为是查询报文
pkdata[7]=(byte)0;
pkdata[8]=(byte)0;
pkdata[9]=(byte)0;
pkdata[10]=(byte)0;
pkdata[11]=(byte)0;
StringTokenizer st=new StringTokenizer(dm,".");//.分隔域名
String label;
position=12; //从第12字节开始生成查询问题
while(st.hasMoreTokens ())
{
label=st.nextToken();
pkdata[position++]=(byte)(label.length() & 0xFF);//转换为字节
byte[] b=label.getBytes();
for(int j=0;j {
pkdata[position++]=b[j];
}
}
pkdata[position++]=(byte)0;//以0结束域名
pkdata[position++]=(byte)0;
pkdata[position++]=(byte)15; //查询类型15表示MX记录查询
pkdata[position++]=(byte)0;
pkdata[position++]=(byte)1; //Internet数据记录查询
}//构造查询完成
private String getResponse() //获得反馈信息
{
String temp="";
int qCount=((pkdata[4] & 0xff)<<8) |(pkdata[5] & 0xFF); //获得问题数
if( qCount<0)
{return("");} //问题个数小于0,返回空字符串
int aCount=((pkdata[6] & 0xff)<<8)|(pkdata[7] & 0xff);//获得应答问题数
if (aCount<0)
{return(""); } //问题个数小于0,返回空字符串
position=12;//查询问题部分起始位置
for(int i=0;i {
DMname="";
position=Proc(position);
position+=4; //增加长度字节部分查询类型、查询类
}
for(int i=0;i {
DMname="";
position=Proc(position);
position+=10; //类型2字节、类2字节、生存时间4字节、资源长度2字节共10字节
int pref=(pkdata[position++]<<8) | (pkdata[position++] & 0xff); //得到MX记录的交换器基数
DMname="";
position=Proc(position); //得到交换器的值,一般为一个标准域名
if( temp.equalsIgnoreCase("") )
{temp="" +pref +" "+DMname; } //返回数据
else
{temp= temp + "," +pref +" "+DMname;}
}
return(temp);
}
private int Proc(int position)
{//该过程从pkdata字节数组中寻找域名
int len=(pkdata[position++]&0xff); //取得将要处理标识符的长度
if(len==0)//没有其他标识符,结束返回
{return position;}
int offset;//偏移
do{
if((len & 0xc0)==0xc0) //压缩格式么
{
if(position>=length) //超过包的大小,那么偏移显然错误
{return(-1);}
offset=((len&0x3f)<<8)|(pkdata[position++] & 0xff); //获得压缩源标识符的偏移
Proc(offset);//递归调用获得压缩前的名称
return(position);
}
else //非压缩格式
{
if((position + len)>length) //超过长度
{ return(-1);}
DMname+=new String(pkdata,position,len); //获得域名标识符各个部分
position+=len;
}
if (position>length)
{return(-1);}
len=pkdata[position++] & 0xff;//最后是0结尾么
if(len!=0)
{
DMname+=".";//加上.构成完整域名
}
}while(len!=0);
return(position);
}
public static void main(String args[]) throws Exception
{
MXDNS mx=new MXDNS();
String s=mx.getMXRecords("sina.com","202.96.128.68");
System.out.println(s);
int k=System.in.read() ;
}
}
|