@@ -6,6 +6,9 @@ var request = require('request');
6
6
var version = require ( '../package.json' ) . version ;
7
7
var relationDebugDescription = require ( './relationDebugDescription' ) ;
8
8
var prettyBytes = require ( 'pretty-bytes' ) ;
9
+ var urlModule = require ( 'url' ) ;
10
+ var net = require ( 'net' ) ;
11
+ var tls = require ( 'tls' ) ;
9
12
10
13
var checkFragments = require ( './transforms/checkFragments' ) ;
11
14
@@ -40,7 +43,9 @@ module.exports = function (options) {
40
43
return false ;
41
44
}
42
45
43
- var relationTypeExclusions = [ ] ;
46
+ var relationTypeExclusions = [
47
+ 'HtmlPreconnectLink'
48
+ ] ;
44
49
45
50
if ( ! options . recursive ) {
46
51
relationTypeExclusions . push ( 'HtmlAnchor' ) ;
@@ -55,67 +60,77 @@ module.exports = function (options) {
55
60
crossorigin : false
56
61
} ;
57
62
58
- function logHttpResult ( status , url , redirects , relations ) {
63
+ function logResult ( status , url , redirects , relations ) {
59
64
redirects = redirects || [ ] ;
60
65
relations = relations || [ ] ;
61
66
62
67
var at = _ . uniq ( relations . map ( relationDebugDescription ) ) . join ( '\n ' ) ;
63
68
var skip = shouldSkip ( url ) ;
64
69
65
- if ( status !== 200 ) {
66
- var invalidStatusReport = {
70
+ if ( status === false ) {
71
+ errorCount += 1 ;
72
+ t . push ( null , {
73
+ ok : false ,
74
+ skip : skip ,
75
+ name : 'should accept connections' ,
76
+ operator : 'error' ,
77
+ expected : [ 'connection accepted' , url ] . join ( ' ' ) ,
78
+ actual : [ status , url ] . join ( ' ' ) ,
79
+ at : at
80
+ } ) ;
81
+ } else if ( status !== 200 && status !== true ) {
82
+ errorCount += 1 ;
83
+ t . push ( null , {
67
84
ok : false ,
68
85
skip : skip ,
69
86
name : 'should respond with HTTP status 200' ,
70
87
operator : 'error' ,
71
88
expected : [ 200 , url ] . join ( ' ' ) ,
72
89
actual : [ status , url ] . join ( ' ' ) ,
73
90
at : at
74
- } ;
75
-
76
- errorCount += 1 ;
77
- t . push ( null , invalidStatusReport ) ;
91
+ } ) ;
78
92
}
79
93
80
- var report = {
81
- ok : true ,
82
- skip : skip ,
83
- name : 'URI should have no redirects - ' + url ,
84
- operator : 'noRedirects' ,
85
- expected : [ 200 , url ] . join ( ' ' ) ,
86
- at : at
87
- } ;
94
+ if ( typeof status !== 'boolean' ) {
95
+ var report = {
96
+ ok : true ,
97
+ skip : skip ,
98
+ name : 'URI should have no redirects - ' + url ,
99
+ operator : 'noRedirects' ,
100
+ expected : [ 200 , url ] . join ( ' ' ) ,
101
+ at : at
102
+ } ;
88
103
89
- if ( redirects . length ) {
90
- var log = [ ] . concat ( { redirectUri : url } , redirects ) . map ( function ( item , idx , arr ) {
91
- if ( arr [ idx + 1 ] ) {
92
- item . statusCode = arr [ idx + 1 ] . statusCode ;
93
- } else {
94
- item . statusCode = 200 ;
95
- }
104
+ if ( redirects . length ) {
105
+ var log = [ ] . concat ( { redirectUri : url } , redirects ) . map ( function ( item , idx , arr ) {
106
+ if ( arr [ idx + 1 ] ) {
107
+ item . statusCode = arr [ idx + 1 ] . statusCode ;
108
+ } else {
109
+ item . statusCode = 200 ;
110
+ }
96
111
97
- return item ;
98
- } ) ;
112
+ return item ;
113
+ } ) ;
99
114
100
- var logLine = log . map ( function ( redirect ) {
101
- return [ redirect . statusCode , redirect . redirectUri ] . join ( ' ' ) ;
102
- } ) . join ( ' --> ' ) ;
115
+ var logLine = log . map ( function ( redirect ) {
116
+ return [ redirect . statusCode , redirect . redirectUri ] . join ( ' ' ) ;
117
+ } ) . join ( ' --> ' ) ;
103
118
104
- report . actual = logLine ;
119
+ report . actual = logLine ;
105
120
106
- if ( log [ 0 ] . statusCode !== 302 ) {
107
- report . ok = false ;
121
+ if ( log [ 0 ] . statusCode !== 302 ) {
122
+ report . ok = false ;
123
+ }
124
+ } else {
125
+ report . actual = [ status , url ] . join ( ' ' ) ;
108
126
}
109
- } else {
110
- report . actual = [ status , url ] . join ( ' ' ) ;
111
- }
112
127
113
- if ( ! report . ok ) {
114
- errorCount += 1 ;
128
+ if ( ! report . ok ) {
129
+ errorCount += 1 ;
130
+ }
131
+ t . push ( null , report ) ;
115
132
}
116
133
117
- t . push ( null , report ) ;
118
-
119
134
// Check for mixed-content warnings
120
135
var secureSourceRelations = relations . filter ( function ( relation ) {
121
136
return relation . type !== 'HtmlAnchor' && relation . from . nonInlineAncestor . url . indexOf ( 'https:' ) === 0 ;
@@ -156,6 +171,42 @@ module.exports = function (options) {
156
171
}
157
172
}
158
173
174
+ function tryConnect ( url , relations , attempt ) {
175
+ var urlObj = urlModule . parse ( url ) ;
176
+ var hostname = urlObj . hostname ;
177
+ var isTls = urlObj . protocol === 'https:' ;
178
+ var port = urlObj . port ? parseInt ( urlObj . port , 10 ) : ( isTls ? 443 : 80 ) ;
179
+ attempt = attempt || 1 ;
180
+ return function ( callback ) {
181
+ ( isTls ? tls : net ) . connect ( port , hostname , function ( ) {
182
+ logResult ( true , url , undefined , relations ) ;
183
+
184
+ callback ( undefined , true ) ;
185
+ } ) . on ( 'error' , function ( error ) {
186
+ var code = error . code ;
187
+ var status = false ;
188
+ if ( code ) {
189
+ // Some servers send responses that request apparently handles badly when using the HEAD method...
190
+ if ( code === 'HPE_INVALID_CONSTANT' && attempt === 1 ) {
191
+ return tryConnect ( url , relations , attempt + 1 ) ( callback ) ;
192
+ }
193
+
194
+ if ( code === 'ENOTFOUND' ) {
195
+ status = 'DNS Missing' ;
196
+ } else {
197
+ status = code ;
198
+ }
199
+ } else {
200
+ status = 'Unknown error' ;
201
+ }
202
+
203
+ logResult ( status , url , undefined , relations ) ;
204
+
205
+ callback ( undefined , false ) ;
206
+ } ) ;
207
+ } ;
208
+ }
209
+
159
210
function httpStatus ( url , relations , attempt ) {
160
211
attempt = attempt || 1 ;
161
212
@@ -192,7 +243,7 @@ module.exports = function (options) {
192
243
status = 'Unknown error' ;
193
244
}
194
245
195
- logHttpResult ( status , url , undefined , relations ) ;
246
+ logResult ( status , url , undefined , relations ) ;
196
247
197
248
callback ( undefined , status ) ;
198
249
} else {
@@ -213,7 +264,7 @@ module.exports = function (options) {
213
264
redirects = res . request . redirects || [ ] ;
214
265
var firstRedirectStatus = redirects [ 0 ] && redirects [ 0 ] . statusCode ;
215
266
216
- logHttpResult ( status , url , redirects , relations ) ;
267
+ logResult ( status , url , redirects , relations ) ;
217
268
218
269
callback ( undefined , firstRedirectStatus || status ) ;
219
270
}
@@ -316,12 +367,13 @@ module.exports = function (options) {
316
367
concurrency : options . concurrency || 100
317
368
} )
318
369
. queue ( checkFragments ( t ) )
319
- . queue ( function ( assetGraph , callback ) {
370
+ . queue ( function checkUrls ( assetGraph , callback ) {
320
371
var hrefMap = { } ;
321
372
322
373
hrefMap = _ . groupBy ( assetGraph . findRelations ( {
323
374
crossorigin : true ,
324
- href : / ^ (?: h t t p s ? : ) ? \/ \/ /
375
+ href : / ^ (?: h t t p s ? : ) ? \/ \/ / ,
376
+ type : query . not ( 'HtmlPreconnectLink' )
325
377
} , true ) , function ( relation ) {
326
378
var url = relation . to . url . replace ( / # .* $ / , '' ) ;
327
379
@@ -346,6 +398,32 @@ module.exports = function (options) {
346
398
}
347
399
) ;
348
400
} )
401
+ . queue ( function checkPreconnects ( assetGraph , callback ) {
402
+ var hrefMap = { } ;
403
+
404
+ hrefMap = _ . groupBy ( assetGraph . findRelations ( {
405
+ crossorigin : true ,
406
+ href : / ^ (?: h t t p s ? : ) ? \/ \/ / ,
407
+ type : 'HtmlPreconnectLink'
408
+ } , true ) , function ( relation ) {
409
+ var url = relation . to . url . replace ( / # .* $ / , '' ) ;
410
+
411
+ return url ;
412
+ } ) ;
413
+
414
+ var hrefs = Object . keys ( hrefMap ) ;
415
+
416
+ t . push ( {
417
+ name : 'Connecting to ' + hrefs . length + ' hosts (checking <link rel="preconnect" href="...">'
418
+ } ) ;
419
+ async . parallelLimit (
420
+ hrefs . map ( function ( url ) {
421
+ return tryConnect ( url , hrefMap [ url ] ) ;
422
+ } ) ,
423
+ 20 ,
424
+ callback
425
+ ) ;
426
+ } )
349
427
// .writeStatsToStderr()
350
428
. run ( function ( ) {
351
429
var results = t . close ( ) ;
0 commit comments