1313import ssl
1414import time
1515
16- from http .client import HTTPConnection , HTTPSConnection , HTTPException
17- from urllib .parse import urlsplit
18-
1916import jinja2
17+ import requests
2018
2119from lxml import etree , html
2220
4745log = logging .getLogger ("check_https" )
4846
4947
48+ class SiteInfo :
49+
50+ def __init__ (self ):
51+ self .domain = None
52+ self .ssllabs_grade = None
53+ self .secure_connection_works = None
54+ self .can_load_https_page = None
55+ self .mixed_content = None
56+ self .sts = None
57+ self .https_redirects_to_http = None
58+ self .http_redirects_to_https = None
59+ self .checks = []
60+
61+ def new_check (self ):
62+ c = Check ()
63+ self .checks .append (c )
64+ return c
65+
66+
5067class Check :
5168
5269 def __init__ (self ):
@@ -70,73 +87,62 @@ def __init__(self, status):
7087 self .status = status
7188
7289
73- def fetch_through_redirects (http , stop = None ):
74- path = "/"
75- url = None
90+ def fetch_through_redirects (url ):
7691 tree = None
7792 while True :
78- http .request ("GET" , path , headers = {"User-Agent" : USER_AGENT })
79- resp = http .getresponse ()
80- new_location = None
81- if resp .status == 200 :
82- tree = html .parse (resp )
83- for meta in META_XPATH (tree ):
84- m = re .match ("0;\s*url=['\" ](.+?)['\" ]" , meta .get ("content" ))
85- if m is not None :
86- new_location = m .groups ()[0 ]
87- elif resp .status in (301 , 302 , 303 , 307 ):
88- resp .read ()
89- new_location = resp .getheader ("Location" )
90- if new_location is not None :
91- url = urlsplit (new_location )
92- if stop is not None and stop (url ):
93- break
93+ cont = False
94+ resp = requests .get (
95+ url ,
96+ verify = "moz-certs.pem" ,
97+ headers = {"User-Agent" : USER_AGENT },
98+ timeout = 10 ,
99+ stream = True ,
100+ )
101+ try :
102+ if resp .status_code != 200 :
103+ raise Not200 (resp .status_code )
104+ tree = html .parse (resp .raw )
105+ finally :
94106 resp .close ()
95- if url . netloc and url . netloc != http . host :
96- raise ValueError ( "{} redirects to a different domain." . format ( url . netloc ))
97- path = url . path
98- if not path . startswith ( "/" ) :
99- path = "/" + path
100- continue
101- if resp . status != 200 :
102- raise Not200 ( resp . status )
103- break
104- return url , resp , tree
107+ # Check for sneaky <meta> redirects.
108+ for meta in META_XPATH ( tree ):
109+ m = re . match ( "0;\s* url=[' \" ](.+?)[' \" ]" , meta . get ( "content" ))
110+ if m is not None :
111+ url = m . groups ()[ 0 ]
112+ cont = True
113+ break
114+ if not cont :
115+ break
116+ return resp , tree
105117
106118
107119def has_mixed_content (tree ):
108120 s = MIXED_CONTENT_XPATH (tree )
109121 return len (s ) >= 1
110122
111123
112- def check_one_site (site ):
113- domain = site ["domain" ]
114- log .info ("Checking {}" .format (domain ))
115-
116- site ["status" ] = "bad"
117- site ["checks" ] = checks = []
118- good_connection = Check ()
119- checks .append (good_connection )
124+ def check_secure_connection (info ):
125+ # Guilty until proven innocent.
126+ info .secure_connection_works = False
127+ good_connection = info .new_check ()
120128 try :
121- addrs = socket .getaddrinfo (domain , 443 , socket .AF_INET , proto = socket .IPPROTO_TCP )
129+ addrs = socket .getaddrinfo (info . domain , 443 , socket .AF_INET , proto = socket .IPPROTO_TCP )
122130 except socket .gaierror :
123- log . warning ("DNS lookup for {} failed!" . format ( domain ) )
131+ good_connection . fail ("DNS lookup failed." )
124132 return
125- info = random .choice (addrs )
126- sock = socket .socket (info [0 ], info [1 ], info [2 ])
133+ addr_info = random .choice (addrs )
134+ sock = socket .socket (addr_info [0 ], addr_info [1 ], addr_info [2 ])
127135 sock .settimeout (10 )
128136 context = ssl .SSLContext (ssl .PROTOCOL_SSLv23 )
129-
130137 context .options |= ssl .OP_NO_SSLv2 | ssl .OP_NO_SSLv3
131138 # Some platforms (OS X) do not have OP_NO_COMPRESSION
132139 context .options |= getattr (ssl , "OP_NO_COMPRESSION" , 0 )
133-
134140 context .verify_mode = ssl .CERT_REQUIRED
135141 context .check_hostname = True
136142 context .load_verify_locations ("moz-certs.pem" )
137- secure_sock = context .wrap_socket (sock , server_hostname = domain )
143+ secure_sock = context .wrap_socket (sock , server_hostname = info . domain )
138144 try :
139- secure_sock .connect (info [4 ])
145+ secure_sock .connect (addr_info [4 ])
140146 except ConnectionRefusedError :
141147 good_connection .fail ("Nothing is listening on port 443." )
142148 return
@@ -157,37 +163,37 @@ def check_one_site(site):
157163 error_name = errno .errorcode [e .errno ]
158164 good_connection .fail ("<code>connect()</code> returns with error {}." .format (error_name ))
159165 return
166+ finally :
167+ secure_sock .close ()
160168 msg = "A verified TLS connection can be established. "
161- grade = site .get ("ssllabs_grade" )
162- if grade is not None :
163- msg += "<a href=\" https://www.ssllabs.com/ssltest/analyze.html?d={}\" >SSL Labs grade</a> is " + grade + "."
169+ if info .ssllabs_grade is not None :
170+ msg += "<a href=\" https://www.ssllabs.com/ssltest/analyze.html?d={}\" >SSL Labs grade</a> is " + info .ssllabs_grade + "."
164171 else :
165172 msg += "(<a href=\" https://www.ssllabs.com/ssltest/analyze.html?d={}\" >SSL Labs report</a>)"
166- good_connection .succeed (msg .format (domain ))
173+ info .secure_connection_works = True
174+ good_connection .succeed (msg .format (info .domain ))
167175
168- mediocre = False
169176
170- https_load = Check ()
171- checks .append (https_load )
172- http = HTTPSConnection (domain , context = context )
173- http .sock = secure_sock
177+ def check_https_page (info ):
178+ info .can_load_https_page = False
179+ https_load = info .new_check ()
174180 try :
175- def stop_on_http_or_domain_change (url ):
176- return url .scheme == "http" or (url .netloc and url .netloc != domain )
177- final , resp , tree = fetch_through_redirects (http , stop_on_http_or_domain_change )
178- if final is not None and final .scheme == "http" :
181+ resp , tree = fetch_through_redirects ("https://{}" .format (info .domain ))
182+ info .https_redirects_to_http = resp .url .startswith ("http://" )
183+ if info .https_redirects_to_http :
179184 https_load .fail ("The HTTPS site redirects to HTTP." )
180185 return
181- if tree is not None and has_mixed_content (tree ):
186+ info .mixed_content = tree is not None and has_mixed_content (tree )
187+ if info .mixed_content :
182188 https_load .fail ("The HTML page loaded over HTTPS has mixed content." )
183189 return
184- good_sts = Check ()
185- checks .append (good_sts )
186- sts = resp .getheader ("Strict-Transport-Security" )
190+ good_sts = info .new_check ()
191+ sts = resp .headers .get ("Strict-Transport-Security" )
187192 if sts is not None :
188193 m = re .search ("max-age=(\d+)" , sts )
189194 if m is not None :
190195 age = int (m .group (1 ))
196+ info .sts = age
191197 if age >= 2592000 :
192198 good_sts .succeed ("<code>Strict-Transport-Security</code> header is set with a long <code>max-age</code> directive." )
193199 else :
@@ -196,48 +202,69 @@ def stop_on_http_or_domain_change(url):
196202 good_sts .fail ("<code>Strict-Transport-Security</code> header doesn't contain a <code>max-age</code> directive." )
197203 else :
198204 good_sts .fail ("<code>Strict-Transport-Security</code> header is not set." )
199- if good_sts .failed :
200- mediocre = True
201- except socket .timeout :
205+ except requests .Timeout :
202206 https_load .fail ("Requesting HTTPS page times out." )
203207 return
204208 except Not200 as e :
205209 https_load .fail ("The HTTPS site returns an error status ({}) on request." .format (e .status ))
206210 return
207- except OSError as e :
208- err_msg = errno .errorcode [e .errno ]
209- https_load .fail ("Encountered error ({}) while loading HTTPS site." .format (err_msg ))
210- return
211- finally :
212- http .close ()
211+ info .can_load_https_page = True
213212 https_load .succeed ("A page can be successfully fetched over HTTPS." )
214213
215- http_redirect = Check ()
216- checks . append ( http_redirect )
217- http = HTTPConnection ( domain )
214+
215+ def check_http_page ( info ):
216+ http_redirect = info . new_check ( )
218217 try :
219- def stop_on_https (url ):
220- return url .scheme == "https"
221- final , resp , tree = fetch_through_redirects (http , stop_on_https )
222- if final is not None and final .scheme == "https" :
218+ resp , tree = fetch_through_redirects ("http://{}" .format (info .domain ))
219+ if resp .url .startswith ("https://" ):
220+ info .http_redirects_to_https = True
223221 http_redirect .succeed ("HTTP site redirects to HTTPS." )
224222 else :
223+ info .http_redirects_to_https = False
225224 http_redirect .fail ("HTTP site doesn't redirect to HTTPS." )
226- mediocre = True
227- except HTTPException :
228- http_redirect .fail ("Encountered HTTP error while loading HTTP site." )
225+ except requests .Timeout :
226+ http_redirect .fail ("The HTTP site times out." )
229227 return
230228 except Not200 as e :
231229 http_redirect .fail ("The HTTP site returns an error status ({}) on request." .format (e .status ))
232230 return
233- except OSError as e :
234- err_msg = errno .errorcode [e .errno ]
235- http_redirect .fail ("Encountered error ({}) while loading HTTP site." .format (err_msg ))
236- return
237- finally :
238- http .close ()
239231
240- site ["status" ] = "mediocre" if mediocre else "good"
232+
233+ def check_site (site ):
234+ log .info ("Checking {}" .format (site ["domain" ]))
235+ info = SiteInfo ()
236+ info .domain = site ["domain" ]
237+ info .ssllabs_grade = site .get ("ssllabs_grade" )
238+
239+ check_secure_connection (info )
240+ if not info .secure_connection_works :
241+ log .info ("Couldn't connect securely to {}, so aborting further checks." .format (info .domain ))
242+ return info
243+
244+ check_https_page (info )
245+ if not info .can_load_https_page :
246+ log .info ("Couldn't load HTTPS page for {}, so aborting further checks." .format (info .domain ))
247+ return info
248+
249+ check_http_page (info )
250+
251+ return info
252+
253+
254+ def set_site_template_data_from_info (site , info ):
255+ site ["checks" ] = info .checks
256+
257+ if (not info .secure_connection_works or
258+ info .https_redirects_to_http or
259+ info .mixed_content ):
260+ status = "bad"
261+ elif (not info .can_load_https_page or
262+ not info .http_redirects_to_https or
263+ info .sts is None or info .sts <= 2592000 ):
264+ status = "mediocre"
265+ else :
266+ status = "good"
267+ site ["status" ] = status
241268
242269
243270def regenerate_everything (ssllabs_grades_file ):
@@ -259,23 +286,26 @@ def regenerate_everything(ssllabs_grades_file):
259286 domain = site ["domain" ]
260287 if domain in ssllabs_grades :
261288 site ["ssllabs_grade" ] = ssllabs_grades [domain ]
262- futures .append (executor .submit (check_one_site , site ))
289+ futures .append (executor .submit (check_site , site ))
263290 with executor :
264291 while True :
265292 done , not_done = concurrent .futures .wait (futures , timeout = 1 )
266293 print ("{}/{}" .format (len (done ), len (done ) + len (not_done )))
267294 if not not_done :
268295 break
296+ infos = {}
269297 for f in futures :
270- # This will raise an exception if check_one_site did.
271- f . result ()
298+ info = f . result ()
299+ infos [ info . domain ] = info
272300
273301 for listing in meta ["listings" ]:
274302 if "external" in listing :
275303 continue
276304 for cat in listing ["data" ]["categories" ]:
277305 cat_status = collections .Counter ()
278306 for site in cat ["sites" ]:
307+ info = infos [site ["domain" ]]
308+ set_site_template_data_from_info (site , info )
279309 cat_status [site ["status" ]] += 1
280310 cat ["counts" ] = cat_status
281311
0 commit comments