1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | var deprecate = require('depd')('connect');
|
13 | var utils = require('../utils')
|
14 | , parseurl = require('parseurl')
|
15 | , Cache = require('../cache')
|
16 | , fresh = require('fresh');
|
17 | var merge = require('utils-merge');
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | module.exports = function staticCache(options){
|
60 | var options = options || {}
|
61 | , cache = new Cache(options.maxObjects || 128)
|
62 | , maxlen = options.maxLength || 1024 * 256;
|
63 |
|
64 | return function staticCache(req, res, next){
|
65 | var key = cacheKey(req)
|
66 | , ranges = req.headers.range
|
67 | , hasCookies = req.headers.cookie
|
68 | , hit = cache.get(key);
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | req.on('static', function(stream){
|
74 | var headers = res._headers
|
75 | , cc = utils.parseCacheControl(headers['cache-control'] || '')
|
76 | , contentLength = headers['content-length']
|
77 | , hit;
|
78 |
|
79 |
|
80 | if (headers['set-cookie']) return hasCookies = true;
|
81 |
|
82 |
|
83 | if (hasCookies) return;
|
84 |
|
85 |
|
86 | if (!contentLength || contentLength > maxlen) return;
|
87 |
|
88 |
|
89 | if (headers['content-range']) return;
|
90 |
|
91 |
|
92 |
|
93 | if ( cc['no-cache']
|
94 | || cc['no-store']
|
95 | || cc['private']
|
96 | || cc['must-revalidate']) return;
|
97 |
|
98 |
|
99 | if (hit = cache.get(key)){
|
100 | if (headers.etag == hit[0].etag) {
|
101 | hit[0].date = new Date;
|
102 | return;
|
103 | } else {
|
104 | cache.remove(key);
|
105 | }
|
106 | }
|
107 |
|
108 |
|
109 | if (null == stream) return;
|
110 |
|
111 |
|
112 | var arr = [];
|
113 |
|
114 |
|
115 | stream.on('data', function(chunk){
|
116 | arr.push(chunk);
|
117 | });
|
118 |
|
119 |
|
120 | stream.on('end', function(){
|
121 | var cacheEntry = cache.add(key);
|
122 | delete headers['x-cache'];
|
123 | cacheEntry.push(200);
|
124 | cacheEntry.push(headers);
|
125 | cacheEntry.push.apply(cacheEntry, arr);
|
126 | });
|
127 | });
|
128 |
|
129 | if (req.method == 'GET' || req.method == 'HEAD') {
|
130 | if (ranges) {
|
131 | next();
|
132 | } else if (!hasCookies && hit && !mustRevalidate(req, hit)) {
|
133 | res.setHeader('X-Cache', 'HIT');
|
134 | respondFromCache(req, res, hit);
|
135 | } else {
|
136 | res.setHeader('X-Cache', 'MISS');
|
137 | next();
|
138 | }
|
139 | } else {
|
140 | next();
|
141 | }
|
142 | }
|
143 | };
|
144 |
|
145 | module.exports = deprecate.function(module.exports,
|
146 | 'staticCache: use varnish or similar reverse proxy caches');
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | function respondFromCache(req, res, cacheEntry) {
|
160 | var status = cacheEntry[0]
|
161 | , headers = merge({}, cacheEntry[1])
|
162 | , content = cacheEntry.slice(2);
|
163 |
|
164 | headers.age = (new Date - new Date(headers.date)) / 1000 || 0;
|
165 |
|
166 | switch (req.method) {
|
167 | case 'HEAD':
|
168 | res.writeHead(status, headers);
|
169 | res.end();
|
170 | break;
|
171 | case 'GET':
|
172 | if (fresh(req.headers, headers)) {
|
173 | headers['content-length'] = 0;
|
174 | res.writeHead(304, headers);
|
175 | res.end();
|
176 | } else {
|
177 | res.writeHead(status, headers);
|
178 |
|
179 | function write() {
|
180 | while (content.length) {
|
181 | if (false === res.write(content.shift())) {
|
182 | res.once('drain', write);
|
183 | return;
|
184 | }
|
185 | }
|
186 | res.end();
|
187 | }
|
188 |
|
189 | write();
|
190 | }
|
191 | break;
|
192 | default:
|
193 |
|
194 | res.writeHead(500, '');
|
195 | res.end();
|
196 | }
|
197 | }
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 | function mustRevalidate(req, cacheEntry) {
|
209 | var cacheHeaders = cacheEntry[1]
|
210 | , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '')
|
211 | , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '')
|
212 | , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0;
|
213 |
|
214 | if ( cacheCC['no-cache']
|
215 | || cacheCC['must-revalidate']
|
216 | || cacheCC['proxy-revalidate']) return true;
|
217 |
|
218 | if (reqCC['no-cache']) return true;
|
219 |
|
220 | if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge;
|
221 |
|
222 | if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge;
|
223 |
|
224 | return false;
|
225 | }
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | function cacheKey(req) {
|
238 | return parseurl(req).path;
|
239 | }
|