1+ import socket
2+ import re
3+ from datetime import datetime , timedelta , timezone
4+
5+
6+ def get_tld (domain ):
7+ parts = domain .strip ().lower ().split ("." )
8+ if len (parts ) < 2 :
9+ return ""
10+ return parts [- 1 ]
11+
12+
13+ def normalize_domain (domain ):
14+ domain = domain .strip ()
15+ if not domain :
16+ return ""
17+
18+ if "://" in domain :
19+ domain = domain .split ("://" , 1 )[1 ]
20+ domain = domain .split ("/" , 1 )[0 ]
21+ domain = domain .split ("@" , 1 )[- 1 ]
22+
23+ if domain .startswith ("[" ) and "]" in domain :
24+ return ""
25+ if ":" in domain :
26+ host , port = domain .rsplit (":" , 1 )
27+ if re .fullmatch (r"\d+" , port ):
28+ domain = host
29+ else :
30+ return ""
31+
32+ domain = domain .strip ().strip ("." ).lower ()
33+ if not domain :
34+ return ""
35+
36+ try :
37+ domain = domain .encode ("idna" ).decode ("ascii" )
38+ except UnicodeError :
39+ return ""
40+
41+ if "." not in domain or ".." in domain or len (domain ) > 253 :
42+ return ""
43+
44+ labels = domain .split ("." )
45+ for label in labels :
46+ if not label or len (label ) > 63 :
47+ return ""
48+ if label .startswith ("-" ) or label .endswith ("-" ):
49+ return ""
50+ if not re .fullmatch (r"[a-z0-9-]+" , label ):
51+ return ""
52+ if re .fullmatch (r"\d+" , label ):
53+ return ""
54+
55+ if re .fullmatch (r"\d{1,3}(?:\.\d{1,3}){3}" , domain ):
56+ return ""
57+
58+ return domain
59+
60+
61+ def query_whois_server (server , domain ):
62+ with socket .create_connection ((server , 43 ), timeout = 10 ) as sock :
63+ sock .sendall ((domain + "\r \n " ).encode ("utf-8" ))
64+ chunks = []
65+ while True :
66+ data = sock .recv (4096 )
67+ if not data :
68+ break
69+ chunks .append (data )
70+ return b"" .join (chunks ).decode ("utf-8" , errors = "replace" )
71+
72+
73+ def find_whois_server (domain ):
74+ iana_response = query_whois_server ("whois.iana.org" , domain )
75+ refer_server = ""
76+ for line in iana_response .splitlines ():
77+ line = line .strip ()
78+ if not line or ":" not in line :
79+ continue
80+ key , value = line .split (":" , 1 )
81+ key = key .strip ().lower ()
82+ value = value .strip ()
83+ if key == "whois" and value :
84+ return value
85+ if key == "refer" and value :
86+ refer_server = value
87+ return refer_server
88+
89+
90+ def parse_whois_text (text ):
91+ data = {}
92+ dns_list = []
93+ status_list = []
94+
95+ field_map = {
96+ "domain name" : "domain" ,
97+ "domain" : "domain" ,
98+ "registrar" : "registrar" ,
99+ "sponsoring registrar" : "registrar" ,
100+ "registrar organization" : "registrar" ,
101+ "creation date" : "creation_date" ,
102+ "created" : "creation_date" ,
103+ "created on" : "creation_date" ,
104+ "registered on" : "creation_date" ,
105+ "registration time" : "creation_date" ,
106+ "registration date" : "creation_date" ,
107+ "registry expiry date" : "expiry_date" ,
108+ "expiration date" : "expiry_date" ,
109+ "expiry date" : "expiry_date" ,
110+ "expire" : "expiry_date" ,
111+ "expires on" : "expiry_date" ,
112+ "expiration time" : "expiry_date" ,
113+ "paid-till" : "expiry_date" ,
114+ "renewal date" : "expiry_date" ,
115+ "updated date" : "updated_date" ,
116+ "last updated on" : "updated_date" ,
117+ "last modified" : "updated_date" ,
118+ "changed" : "updated_date" ,
119+ "updated date time" : "updated_date" ,
120+ "modification time" : "updated_date" ,
121+ "registrar iana id" : "iana_id" ,
122+ "iana id" : "iana_id" ,
123+ "iana_id" : "iana_id" ,
124+ }
125+
126+ status_keys = {"status" , "domain status" }
127+ dns_keys = {"name server" , "nserver" , "nserver1" , "nserver2" , "nserver3" , "nserver4" }
128+
129+ for raw_line in text .splitlines ():
130+ line = raw_line .strip ()
131+ if not line or ":" not in line :
132+ continue
133+
134+ key , value = line .split (":" , 1 )
135+ key = key .strip ().lower ()
136+ value = value .strip ()
137+
138+ if not value :
139+ continue
140+
141+ mapped_key = field_map .get (key )
142+ if mapped_key :
143+ if mapped_key == "domain" :
144+ data [mapped_key ] = value .lower ()
145+ elif mapped_key not in data :
146+ data [mapped_key ] = value
147+ continue
148+
149+ if key in status_keys :
150+ status_value = value .split ()[0 ].strip ().lower ()
151+ if status_value and status_value not in status_list :
152+ status_list .append (status_value )
153+ continue
154+
155+ if key in dns_keys :
156+ dns_value = value .split ()[0 ].strip ("." ).lower ()
157+ if dns_value and dns_value not in dns_list :
158+ dns_list .append (dns_value )
159+ continue
160+
161+ if key == "dnssec" and "dnssec" not in data :
162+ data ["dnssec" ] = value
163+
164+ data ["status_list" ] = status_list
165+ data ["dns_list" ] = dns_list
166+ return data
167+
168+
169+ def parse_datetime (value ):
170+ value = value .strip ()
171+ if not value :
172+ return None
173+
174+ iso_value = value .replace ("Z" , "+00:00" )
175+ try :
176+ return datetime .fromisoformat (iso_value )
177+ except ValueError :
178+ pass
179+
180+ iso_value = re .sub (r"([+-]\d{2})(\d{2})$" , r"\1:\2" , iso_value )
181+ try :
182+ return datetime .fromisoformat (iso_value )
183+ except ValueError :
184+ pass
185+
186+ normalized_value = re .sub (r"\s+UTC$" , " +0000" , value , flags = re .IGNORECASE )
187+ normalized_value = re .sub (r"\s+GMT$" , " +0000" , normalized_value , flags = re .IGNORECASE )
188+
189+ patterns = [
190+ "%Y-%m-%d %H:%M:%S" ,
191+ "%Y-%m-%d" ,
192+ "%d-%b-%Y" ,
193+ "%d-%b-%Y %H:%M:%S" ,
194+ "%Y.%m.%d %H:%M:%S" ,
195+ "%Y/%m/%d %H:%M:%S" ,
196+ "%Y/%m/%d" ,
197+ "%Y.%m.%d" ,
198+ "%Y-%m-%d %H:%M:%S %z" ,
199+ "%Y.%m.%d %H:%M:%S %z" ,
200+ "%Y-%m-%dT%H:%M:%S.%f%z" ,
201+ "%Y-%m-%dT%H:%M:%S%z" ,
202+ "%a %b %d %H:%M:%S %Y" ,
203+ ]
204+
205+ for pattern in patterns :
206+ try :
207+ return datetime .strptime (normalized_value , pattern )
208+ except ValueError :
209+ pass
210+ return None
211+
212+
213+ def format_datetime_to_utc8 (value ):
214+ dt = parse_datetime (value )
215+ if not dt :
216+ return value
217+ utc8 = timezone (timedelta (hours = 8 ))
218+ if dt .tzinfo is None :
219+ dt = dt .replace (tzinfo = timezone .utc )
220+ dt = dt .astimezone (utc8 )
221+ return dt .strftime ("%Y-%m-%d %H:%M:%S UTC+8" )
222+
223+
224+ def translate_status (status ):
225+ status_map = {
226+ "ok" : "正常" ,
227+ "inactive" : "未激活" ,
228+ "active" : "已激活" ,
229+ "clientdeleteprohibited" : "客户端禁止删除" ,
230+ "clienthold" : "客户端暂停解析" ,
231+ "clientrenewprohibited" : "客户端禁止续费" ,
232+ "clienttransferprohibited" : "客户端禁止转移" ,
233+ "clientupdateprohibited" : "客户端禁止更新" ,
234+ "pendingcreate" : "创建处理中" ,
235+ "pendingdelete" : "删除处理中" ,
236+ "pendingrenew" : "续费处理中" ,
237+ "pendingrestore" : "恢复处理中" ,
238+ "pendingtransfer" : "转移处理中" ,
239+ "pendingupdate" : "更新处理中" ,
240+ "serverdeleteprohibited" : "注册局禁止删除" ,
241+ "serverhold" : "注册局暂停解析" ,
242+ "serverrenewprohibited" : "注册局禁止续费" ,
243+ "servertransferprohibited" : "注册局禁止转移" ,
244+ "serverupdateprohibited" : "注册局禁止更新" ,
245+ }
246+ return status_map .get (status .strip ().lower (), status )
247+
248+
249+ def get_domain_age_text (creation_date_str ):
250+ dt = parse_datetime (creation_date_str )
251+ if not dt :
252+ return ""
253+ if dt .tzinfo is None :
254+ dt = dt .replace (tzinfo = timezone .utc )
255+ now = datetime .now (timezone .utc )
256+ years = int ((now - dt ).days / 365.2425 )
257+ if years > 0 :
258+ return f"{ years } 年老米"
259+ return ""
260+
261+
262+ def normalize_dnssec (value ):
263+ text = value .strip ().lower ()
264+ if not text or text in ("unsigned" , "unsigned delegation" , "no" , "false" , "inactive" ):
265+ return "未配置"
266+ if text in ("signed" , "yes" , "true" , "active" ):
267+ return "已配置"
268+ return value
269+
270+
271+ def print_row (label , value ):
272+ print (f"{ label } : { value } " )
273+
274+
275+ def get_whois_server (domain ):
276+ whois_server = find_whois_server (domain )
277+ if whois_server :
278+ return whois_server
279+
280+ tld = get_tld (domain )
281+ if not tld :
282+ return ""
283+
284+ return f"whois.nic.{ tld } "
285+
286+
287+ def main ():
288+ domain = input ("请输入你的域名: " ).strip ()
289+ if not domain :
290+ print ("域名格式无效" )
291+ return
292+
293+ domain = normalize_domain (domain )
294+ if not domain :
295+ print ("域名格式无效" )
296+ return
297+
298+ try :
299+ whois_server = get_whois_server (domain )
300+ if not whois_server :
301+ print ("无法识别域名后缀" )
302+ return
303+
304+ result = query_whois_server (whois_server , domain )
305+ info = parse_whois_text (result )
306+
307+ display_domain = info .get ("domain" , domain )
308+ age_text = get_domain_age_text (info .get ("creation_date" , "" ))
309+ if age_text :
310+ print_row ("域名" , f"{ display_domain } { age_text } " )
311+ else :
312+ print_row ("域名" , display_domain )
313+
314+ print_row ("注册商" , info .get ("registrar" , "未知" ))
315+
316+ creation_date = info .get ("creation_date" )
317+ expiry_date = info .get ("expiry_date" )
318+ updated_date = info .get ("updated_date" )
319+
320+ print_row ("注册日" , format_datetime_to_utc8 (creation_date ) if creation_date else "未知" )
321+ print_row ("过期日" , format_datetime_to_utc8 (expiry_date ) if expiry_date else "未知" )
322+
323+ if updated_date :
324+ print_row ("更新日" , format_datetime_to_utc8 (updated_date ))
325+
326+ if info .get ("iana_id" ):
327+ print_row ("IANA_ID" , info ["iana_id" ])
328+
329+ if info .get ("status_list" ):
330+ print_row ("状态" , " " .join (translate_status (s ) for s in info ["status_list" ]))
331+ else :
332+ print_row ("状态" , "未知" )
333+
334+ if info .get ("dns_list" ):
335+ print_row ("DNS" , " " .join (info ["dns_list" ]))
336+ else :
337+ print_row ("DNS" , "未知" )
338+
339+ print_row ("DNSSEC" , normalize_dnssec (info .get ("dnssec" , "未配置" )))
340+ except Exception as e :
341+ print (f"查询失败: { e } " )
342+
343+
344+ if __name__ == "__main__" :
345+ main ()
0 commit comments