st 1.2.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.travis.yml +11 -2
- package/README.md +28 -22
- package/bin/server.js +75 -66
- package/package.json +12 -10
- package/st.js +505 -449
- package/test/basic.js +133 -0
- package/test/cli/basic-test.js +32 -0
- package/test/cli/common.js +108 -0
- package/test/cli/host-test.js +92 -0
- package/test/common.js +66 -0
- package/test/cors.js +19 -0
- package/test/dot-common.js +44 -0
- package/test/dot-false.js +19 -0
- package/test/dot-true.js +20 -0
- package/test/explicit-cache-control.js +12 -0
- package/test/fd-limit.js +10 -0
- package/test/fixtures/.dotted-dir/.index.html +11 -0
- package/test/fixtures/.dotted-dir/index.html +11 -0
- package/test/fixtures/index.html +11 -0
- package/test/fixtures/space in filename.txt +1 -0
- package/test/gzip-after-no-gzip.js +38 -0
- package/test/middleware.js +17 -0
- package/test/multi-mount.js +251 -0
- package/test/no-cache.js +19 -0
- package/test/no-content-maxage.js +10 -0
- package/test/no-cors.js +14 -0
- package/test/no-fd-cache.js +8 -0
- package/test/no-gzip-accepted-no-cache.js +32 -0
- package/test/no-gzip-accepted.js +31 -0
- package/test/no-gzip.js +18 -0
- package/test/parent-path.js +8 -0
- package/test/passthrough.js +75 -0
- package/test/preset-cache-control.js +65 -0
package/st.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var mime = require('mime')
|
|
6
|
-
var path = require('path')
|
|
7
|
-
var fs
|
|
1
|
+
const mime = require('mime')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const url = require('url')
|
|
4
|
+
let fs
|
|
8
5
|
try {
|
|
9
6
|
fs = require('graceful-fs')
|
|
10
7
|
} catch (e) {
|
|
11
8
|
fs = require('fs')
|
|
12
9
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// default caching options
|
|
23
|
-
var defaultCacheOptions = {
|
|
10
|
+
const zlib = require('zlib')
|
|
11
|
+
const Neg = require('negotiator')
|
|
12
|
+
const http = require('http')
|
|
13
|
+
const AC = require('async-cache')
|
|
14
|
+
const FD = require('fd')
|
|
15
|
+
const bl = require('bl')
|
|
16
|
+
|
|
17
|
+
const defaultCacheOptions = {
|
|
24
18
|
fd: {
|
|
25
19
|
max: 1000,
|
|
26
|
-
maxAge: 1000 * 60 * 60
|
|
20
|
+
maxAge: 1000 * 60 * 60
|
|
27
21
|
},
|
|
28
22
|
stat: {
|
|
29
23
|
max: 5000,
|
|
@@ -31,29 +25,39 @@ var defaultCacheOptions = {
|
|
|
31
25
|
},
|
|
32
26
|
content: {
|
|
33
27
|
max: 1024 * 1024 * 64,
|
|
34
|
-
length:
|
|
35
|
-
return n.length
|
|
36
|
-
},
|
|
28
|
+
length: (n) => n.length,
|
|
37
29
|
maxAge: 1000 * 60 * 10
|
|
38
30
|
},
|
|
39
31
|
index: {
|
|
40
32
|
max: 1024 * 8,
|
|
41
|
-
length:
|
|
42
|
-
return n.length
|
|
43
|
-
},
|
|
33
|
+
length: (n) => n.length,
|
|
44
34
|
maxAge: 1000 * 60 * 10
|
|
45
35
|
},
|
|
46
36
|
readdir: {
|
|
47
37
|
max: 1000,
|
|
48
|
-
length:
|
|
49
|
-
return n.length
|
|
50
|
-
},
|
|
38
|
+
length: (n) => n.length,
|
|
51
39
|
maxAge: 1000 * 60 * 10
|
|
52
40
|
}
|
|
53
41
|
}
|
|
54
42
|
|
|
43
|
+
// lru-cache doesn't like when max=0, so we just pretend
|
|
44
|
+
// everything is really big. kind of a kludge, but easiest way
|
|
45
|
+
// to get it done
|
|
46
|
+
const none = {
|
|
47
|
+
max: 1,
|
|
48
|
+
length: () => Infinity
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const noCaching = {
|
|
52
|
+
fd: none,
|
|
53
|
+
stat: none,
|
|
54
|
+
index: none,
|
|
55
|
+
readdir: none,
|
|
56
|
+
content: none
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
function st (opt) {
|
|
56
|
-
|
|
60
|
+
let p, u
|
|
57
61
|
if (typeof opt === 'string') {
|
|
58
62
|
p = opt
|
|
59
63
|
opt = arguments[1]
|
|
@@ -63,540 +67,592 @@ function st (opt) {
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
if (!opt)
|
|
67
|
-
|
|
70
|
+
if (!opt) {
|
|
71
|
+
opt = {}
|
|
72
|
+
} else {
|
|
73
|
+
opt = Object.assign({}, opt)
|
|
74
|
+
}
|
|
68
75
|
|
|
69
|
-
if (!p)
|
|
70
|
-
|
|
76
|
+
if (!p) {
|
|
77
|
+
p = opt.path
|
|
78
|
+
}
|
|
79
|
+
if (typeof p !== 'string') {
|
|
80
|
+
throw new Error('no path specified')
|
|
81
|
+
}
|
|
71
82
|
p = path.resolve(p)
|
|
72
|
-
if (!u)
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
if (!u) {
|
|
84
|
+
u = opt.url
|
|
85
|
+
}
|
|
86
|
+
if (!u) {
|
|
87
|
+
u = ''
|
|
88
|
+
}
|
|
89
|
+
if (u.charAt(0) !== '/') {
|
|
90
|
+
u = '/' + u
|
|
91
|
+
}
|
|
75
92
|
|
|
76
93
|
opt.url = u
|
|
77
94
|
opt.path = p
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
const m = new Mount(opt)
|
|
97
|
+
const fn = m.serve.bind(m)
|
|
81
98
|
fn._this = m
|
|
82
99
|
return fn
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.fdman = FD()
|
|
97
|
-
|
|
98
|
-
// cache basically everything
|
|
99
|
-
var c = this.getCacheOptions(opt)
|
|
100
|
-
this.cache = {
|
|
101
|
-
fd: AC(c.fd),
|
|
102
|
-
stat: AC(c.stat),
|
|
103
|
-
index: AC(c.index),
|
|
104
|
-
readdir: AC(c.readdir),
|
|
105
|
-
content: AC(c.content)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
this._cacheControl =
|
|
109
|
-
c.content.maxAge === false
|
|
110
|
-
? undefined
|
|
111
|
-
: typeof c.content.cacheControl == 'string'
|
|
112
|
-
? c.content.cacheControl
|
|
113
|
-
: opt.cache === false
|
|
114
|
-
? 'no-cache'
|
|
115
|
-
: 'public, max-age=' + (c.content.maxAge / 1000)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// lru-cache doesn't like when max=0, so we just pretend
|
|
119
|
-
// everything is really big. kind of a kludge, but easiest way
|
|
120
|
-
// to get it done
|
|
121
|
-
var none = { max: 1, length: function() {
|
|
122
|
-
return Infinity
|
|
123
|
-
}}
|
|
124
|
-
var noCaching = {
|
|
125
|
-
fd: none,
|
|
126
|
-
stat: none,
|
|
127
|
-
index: none,
|
|
128
|
-
readdir: none,
|
|
129
|
-
content: none
|
|
130
|
-
}
|
|
102
|
+
class Mount {
|
|
103
|
+
constructor (opt) {
|
|
104
|
+
if (!opt) {
|
|
105
|
+
throw new Error('no options provided')
|
|
106
|
+
}
|
|
107
|
+
if (typeof opt !== 'object') {
|
|
108
|
+
throw new Error('invalid options')
|
|
109
|
+
}
|
|
110
|
+
if (!(this instanceof Mount)) {
|
|
111
|
+
return new Mount(opt)
|
|
112
|
+
}
|
|
131
113
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
114
|
+
this.opt = opt
|
|
115
|
+
this.url = opt.url
|
|
116
|
+
this.path = opt.path
|
|
117
|
+
this._index = opt.index === false ? false
|
|
118
|
+
: typeof opt.index === 'string' ? opt.index
|
|
119
|
+
: true
|
|
120
|
+
this.fdman = FD()
|
|
121
|
+
|
|
122
|
+
// cache basically everything
|
|
123
|
+
const c = this.getCacheOptions(opt)
|
|
124
|
+
this.cache = {
|
|
125
|
+
fd: AC(c.fd),
|
|
126
|
+
stat: AC(c.stat),
|
|
127
|
+
index: AC(c.index),
|
|
128
|
+
readdir: AC(c.readdir),
|
|
129
|
+
content: AC(c.content)
|
|
130
|
+
}
|
|
139
131
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// load and fd disposal is important to control.
|
|
149
|
-
var c = {
|
|
150
|
-
fd: set('fd'),
|
|
151
|
-
stat: set('stat'),
|
|
152
|
-
index: set('index'),
|
|
153
|
-
readdir: set('readdir'),
|
|
154
|
-
content: set('content'),
|
|
132
|
+
this._cacheControl =
|
|
133
|
+
c.content.maxAge === false
|
|
134
|
+
? undefined
|
|
135
|
+
: typeof c.content.cacheControl === 'string'
|
|
136
|
+
? c.content.cacheControl
|
|
137
|
+
: opt.cache === false
|
|
138
|
+
? 'no-cache'
|
|
139
|
+
: 'public, max-age=' + (c.content.maxAge / 1000)
|
|
155
140
|
}
|
|
156
141
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return c
|
|
165
|
-
}
|
|
142
|
+
getCacheOptions (opt) {
|
|
143
|
+
let o = opt.cache
|
|
144
|
+
const set = (key) => {
|
|
145
|
+
return o[key] === false
|
|
146
|
+
? Object.assign({}, none)
|
|
147
|
+
: Object.assign(Object.assign({}, d[key]), o[key])
|
|
148
|
+
}
|
|
166
149
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
150
|
+
if (o === false) {
|
|
151
|
+
o = noCaching
|
|
152
|
+
} else if (!o) {
|
|
153
|
+
o = {}
|
|
154
|
+
}
|
|
170
155
|
|
|
171
|
-
|
|
172
|
-
p = p.replace(/%2e/ig, '.')
|
|
156
|
+
const d = defaultCacheOptions
|
|
173
157
|
|
|
174
|
-
|
|
175
|
-
|
|
158
|
+
// should really only ever set max and maxAge here.
|
|
159
|
+
// load and fd disposal is important to control.
|
|
160
|
+
const c = {
|
|
161
|
+
fd: set('fd'),
|
|
162
|
+
stat: set('stat'),
|
|
163
|
+
index: set('index'),
|
|
164
|
+
readdir: set('readdir'),
|
|
165
|
+
content: set('content')
|
|
166
|
+
}
|
|
176
167
|
|
|
177
|
-
|
|
178
|
-
|
|
168
|
+
c.fd.dispose = this.fdman.close.bind(this.fdman)
|
|
169
|
+
c.fd.load = this.fdman.open.bind(this.fdman)
|
|
179
170
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return 403
|
|
171
|
+
c.stat.load = this._loadStat.bind(this)
|
|
172
|
+
c.index.load = this._loadIndex.bind(this)
|
|
173
|
+
c.readdir.load = this._loadReaddir.bind(this)
|
|
174
|
+
c.content.load = this._loadContent.bind(this)
|
|
175
|
+
return c
|
|
186
176
|
}
|
|
187
177
|
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
// get the path component from a URI
|
|
179
|
+
getUriPath (u) {
|
|
180
|
+
let p = url.parse(u).pathname // eslint-disable-line
|
|
190
181
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
catch (e) {
|
|
195
|
-
// if decodeURIComponent failed, we weren't given a valid URL to begin with.
|
|
196
|
-
return false
|
|
197
|
-
}
|
|
182
|
+
// Encoded dots are dots
|
|
183
|
+
p = p.replace(/%2e/ig, '.')
|
|
198
184
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
u = u.substr(this.url.length)
|
|
202
|
-
if (u.charAt(0) !== '/') u = '/' + u
|
|
185
|
+
// encoded slashes are /
|
|
186
|
+
p = p.replace(/%2f|%5c/ig, '/')
|
|
203
187
|
|
|
204
|
-
|
|
205
|
-
|
|
188
|
+
// back slashes are slashes
|
|
189
|
+
p = p.replace(/[/\\]/g, '/')
|
|
206
190
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
191
|
+
// Make sure it starts with a slash
|
|
192
|
+
p = p.replace(/^\//, '/')
|
|
193
|
+
if ((/[/\\]\.\.([/\\]|$)/).test(p)) {
|
|
194
|
+
// traversal urls not ever even slightly allowed. clearly shenanigans
|
|
195
|
+
// send a 403 on that noise, do not pass go, do not collect $200
|
|
196
|
+
return 403
|
|
197
|
+
}
|
|
211
198
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
p = path.join('/', p.substr(this.path.length))
|
|
217
|
-
var u = path.join(this.url, p).replace(/\\/g, '/')
|
|
218
|
-
return u
|
|
219
|
-
}
|
|
199
|
+
u = path.normalize(p).replace(/\\/g, '/')
|
|
200
|
+
if (u.indexOf(this.url) !== 0) {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
220
203
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
204
|
+
try {
|
|
205
|
+
u = decodeURIComponent(u)
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// if decodeURIComponent failed, we weren't given a valid URL to begin with.
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
226
210
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
211
|
+
// /a/b/c mounted on /path/to/z/d/x
|
|
212
|
+
// /a/b/c/d --> /path/to/z/d/x/d
|
|
213
|
+
u = u.substr(this.url.length)
|
|
214
|
+
if (u.charAt(0) !== '/') {
|
|
215
|
+
u = '/' + u
|
|
216
|
+
}
|
|
230
217
|
|
|
231
|
-
|
|
232
|
-
// If we got a 403, then it's explicitly forbidden.
|
|
233
|
-
if (req.sturl === 403 || (!this.opt.dot && (/(^|\/)\./).test(req.sturl))) {
|
|
234
|
-
res.statusCode = 403
|
|
235
|
-
res.end('Forbidden')
|
|
236
|
-
return true
|
|
218
|
+
return u
|
|
237
219
|
}
|
|
238
220
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (typeof req.sturl !== 'string' || req.sturl == '') {
|
|
243
|
-
if (typeof next === 'function') next()
|
|
244
|
-
return false
|
|
221
|
+
// get a path from a url
|
|
222
|
+
getPath (u) {
|
|
223
|
+
return path.join(this.path, u)
|
|
245
224
|
}
|
|
246
225
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// if we're in passthrough, AND got a next function, we can
|
|
253
|
-
// fall through to that. otherwise, we already returned true,
|
|
254
|
-
// send an error.
|
|
255
|
-
if (er) {
|
|
256
|
-
if (this.opt.passthrough === true && er.code === 'ENOENT' && next)
|
|
257
|
-
return next()
|
|
258
|
-
return this.error(er, res)
|
|
226
|
+
// get a url from a path
|
|
227
|
+
getUrl (p) {
|
|
228
|
+
p = path.resolve(p)
|
|
229
|
+
if (p.indexOf(this.path) !== 0) {
|
|
230
|
+
return false
|
|
259
231
|
}
|
|
232
|
+
p = path.join('/', p.substr(this.path.length))
|
|
233
|
+
const u = path.join(this.url, p).replace(/\\/g, '/')
|
|
234
|
+
return u
|
|
235
|
+
}
|
|
260
236
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
237
|
+
serve (req, res, next) {
|
|
238
|
+
if (req.method !== 'HEAD' && req.method !== 'GET') {
|
|
239
|
+
if (typeof next === 'function') {
|
|
240
|
+
next()
|
|
241
|
+
}
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// querystrings are of no concern to us
|
|
246
|
+
if (!req.sturl) {
|
|
247
|
+
req.sturl = this.getUriPath(req.url)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// don't allow dot-urls by default, unless explicitly allowed.
|
|
251
|
+
// If we got a 403, then it's explicitly forbidden.
|
|
252
|
+
if (req.sturl === 403 || (!this.opt.dot && (/(^|\/)\./).test(req.sturl))) {
|
|
253
|
+
res.statusCode = 403
|
|
254
|
+
res.end('Forbidden')
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Falsey here means we got some kind of invalid path.
|
|
259
|
+
// Probably urlencoding we couldn't understand, or some
|
|
260
|
+
// other "not compatible with st, but maybe ok" thing.
|
|
261
|
+
if (typeof req.sturl !== 'string' || req.sturl === '') {
|
|
262
|
+
if (typeof next === 'function') {
|
|
263
|
+
next()
|
|
264
|
+
}
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
266
267
|
|
|
267
|
-
|
|
268
|
+
const p = this.getPath(req.sturl)
|
|
269
|
+
|
|
270
|
+
// now we have a path. check for the fd.
|
|
271
|
+
this.cache.fd.get(p, (er, fd) => {
|
|
272
|
+
// inability to open is some kind of error, probably 404
|
|
273
|
+
// if we're in passthrough, AND got a next function, we can
|
|
274
|
+
// fall through to that. otherwise, we already returned true,
|
|
275
|
+
// send an error.
|
|
268
276
|
if (er) {
|
|
269
|
-
if (
|
|
277
|
+
if (this.opt.passthrough === true && er.code === 'ENOENT' && next) {
|
|
270
278
|
return next()
|
|
271
279
|
}
|
|
272
|
-
end()
|
|
273
280
|
return this.error(er, res)
|
|
274
281
|
}
|
|
275
282
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
// we may be about to use this, so don't let it be closed by cache purge
|
|
284
|
+
this.fdman.checkout(p, fd)
|
|
285
|
+
// a safe end() function that can be called multiple times but
|
|
286
|
+
// only perform a single checkin
|
|
287
|
+
const end = this.fdman.checkinfn(p, fd)
|
|
288
|
+
|
|
289
|
+
this.cache.stat.get(fd + ':' + p, (er, stat) => {
|
|
290
|
+
if (er) {
|
|
291
|
+
if (next && this.opt.passthrough === true && this._index === false) {
|
|
292
|
+
return next()
|
|
293
|
+
}
|
|
294
|
+
end()
|
|
295
|
+
return this.error(er, res)
|
|
285
296
|
}
|
|
286
|
-
}
|
|
287
297
|
|
|
288
|
-
|
|
289
|
-
if (ims) ims = new Date(ims).getTime()
|
|
290
|
-
if (ims && ims >= stat.mtime.getTime()) {
|
|
291
|
-
res.statusCode = 304
|
|
292
|
-
res.end()
|
|
293
|
-
return end()
|
|
294
|
-
}
|
|
298
|
+
const isDirectory = stat.isDirectory()
|
|
295
299
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
if (isDirectory) {
|
|
301
|
+
end() // we won't need this fd for a directory in any case
|
|
302
|
+
if (next && this.opt.passthrough === true && this._index === false) {
|
|
303
|
+
// this is done before if-modified-since and if-non-match checks so
|
|
304
|
+
// cached modified and etag values won't return 304's if we've since
|
|
305
|
+
// switched to !index. See Issue #51.
|
|
306
|
+
return next()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
302
309
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
let ims = req.headers['if-modified-since']
|
|
311
|
+
if (ims) {
|
|
312
|
+
ims = new Date(ims).getTime()
|
|
313
|
+
}
|
|
314
|
+
if (ims && ims >= stat.mtime.getTime()) {
|
|
315
|
+
res.statusCode = 304
|
|
316
|
+
res.end()
|
|
317
|
+
return end()
|
|
318
|
+
}
|
|
308
319
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
320
|
+
const etag = getEtag(stat)
|
|
321
|
+
if (req.headers['if-none-match'] === etag) {
|
|
322
|
+
res.statusCode = 304
|
|
323
|
+
res.end()
|
|
324
|
+
return end()
|
|
325
|
+
}
|
|
314
326
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
327
|
+
// only set headers once we're sure we'll be serving this request
|
|
328
|
+
if (!res.getHeader('cache-control') && this._cacheControl) {
|
|
329
|
+
res.setHeader('cache-control', this._cacheControl)
|
|
330
|
+
}
|
|
331
|
+
res.setHeader('last-modified', stat.mtime.toUTCString())
|
|
332
|
+
res.setHeader('etag', etag)
|
|
320
333
|
|
|
321
|
-
|
|
322
|
-
|
|
334
|
+
if (this.opt.cors) {
|
|
335
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
336
|
+
res.setHeader('Access-Control-Allow-Headers',
|
|
337
|
+
'Origin, X-Requested-With, Content-Type, Accept, Range')
|
|
338
|
+
}
|
|
323
339
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
340
|
+
return isDirectory
|
|
341
|
+
? this.index(p, req, res)
|
|
342
|
+
: this.file(p, fd, stat, etag, req, res, end)
|
|
343
|
+
})
|
|
344
|
+
})
|
|
329
345
|
|
|
330
|
-
|
|
331
|
-
// pattern of express and ErrorPage
|
|
332
|
-
return res.error(res.statusCode, er)
|
|
346
|
+
return true
|
|
333
347
|
}
|
|
334
348
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
error (er, res) {
|
|
350
|
+
res.statusCode = typeof er === 'number' ? er
|
|
351
|
+
: er.code === 'ENOENT' || er.code === 'EISDIR' ? 404
|
|
352
|
+
: er.code === 'EPERM' || er.code === 'EACCES' ? 403
|
|
353
|
+
: 500
|
|
338
354
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (typeof this._index === 'string') {
|
|
344
|
-
if (!/\/$/.test(req.sturl)) req.sturl += '/'
|
|
345
|
-
req.sturl += this._index
|
|
346
|
-
return this.serve(req, res)
|
|
347
|
-
}
|
|
348
|
-
return this.error(404, res)
|
|
349
|
-
}
|
|
355
|
+
if (typeof res.error === 'function') {
|
|
356
|
+
// pattern of express and ErrorPage
|
|
357
|
+
return res.error(res.statusCode, er)
|
|
358
|
+
}
|
|
350
359
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
res.statusCode = 301
|
|
354
|
-
res.setHeader('location', req.sturl + '/')
|
|
355
|
-
res.end('Moved: ' + req.sturl + '/')
|
|
356
|
-
return
|
|
360
|
+
res.setHeader('content-type', 'text/plain')
|
|
361
|
+
res.end(http.STATUS_CODES[res.statusCode] + '\n')
|
|
357
362
|
}
|
|
358
363
|
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
index (p, req, res) {
|
|
365
|
+
if (this._index === true) {
|
|
366
|
+
return this.autoindex(p, req, res)
|
|
367
|
+
}
|
|
368
|
+
if (typeof this._index === 'string') {
|
|
369
|
+
if (!/\/$/.test(req.sturl)) {
|
|
370
|
+
req.sturl += '/'
|
|
371
|
+
}
|
|
372
|
+
req.sturl += this._index
|
|
373
|
+
return this.serve(req, res)
|
|
374
|
+
}
|
|
375
|
+
return this.error(404, res)
|
|
376
|
+
}
|
|
368
377
|
|
|
378
|
+
autoindex (p, req, res) {
|
|
379
|
+
if (!/\/$/.exec(req.sturl)) {
|
|
380
|
+
res.statusCode = 301
|
|
381
|
+
res.setHeader('location', req.sturl + '/')
|
|
382
|
+
res.end('Moved: ' + req.sturl + '/')
|
|
383
|
+
return
|
|
384
|
+
}
|
|
369
385
|
|
|
370
|
-
|
|
371
|
-
|
|
386
|
+
this.cache.index.get(p, (er, html) => {
|
|
387
|
+
if (er) {
|
|
388
|
+
return this.error(er, res)
|
|
389
|
+
}
|
|
372
390
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
391
|
+
res.statusCode = 200
|
|
392
|
+
res.setHeader('content-type', 'text/html')
|
|
393
|
+
res.setHeader('content-length', html.length)
|
|
394
|
+
res.end(html)
|
|
395
|
+
})
|
|
376
396
|
}
|
|
377
397
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
end()
|
|
381
|
-
this.cachedFile(p, stat, etag, req, res)
|
|
382
|
-
} else {
|
|
383
|
-
this.streamFile(p, fd, stat, etag, req, res, end)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
398
|
+
file (p, fd, stat, etag, req, res, end) {
|
|
399
|
+
const key = stat.size + ':' + etag
|
|
386
400
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
401
|
+
const mt = mime.getType(path.extname(p))
|
|
402
|
+
if (mt !== 'application/octet-stream') {
|
|
403
|
+
res.setHeader('content-type', mt)
|
|
404
|
+
}
|
|
390
405
|
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
res.setHeader('x-from-cache', 'true')
|
|
396
|
-
if (gz && content.gz) {
|
|
397
|
-
res.setHeader('content-encoding', 'gzip')
|
|
398
|
-
res.setHeader('content-length', content.gz.length)
|
|
399
|
-
res.end(content.gz)
|
|
406
|
+
// only use the content cache if it will actually fit there.
|
|
407
|
+
if (this.cache.content.has(key)) {
|
|
408
|
+
end()
|
|
409
|
+
this.cachedFile(p, stat, etag, req, res)
|
|
400
410
|
} else {
|
|
401
|
-
|
|
402
|
-
res.end(content)
|
|
411
|
+
this.streamFile(p, fd, stat, etag, req, res, end)
|
|
403
412
|
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
413
|
+
}
|
|
406
414
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
stream.destroy = function () {}
|
|
415
|
+
cachedFile (p, stat, etag, req, res) {
|
|
416
|
+
const key = stat.size + ':' + etag
|
|
417
|
+
const gz = this.opt.gzip !== false && getGz(p, req)
|
|
411
418
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
419
|
+
this.cache.content.get(key, (er, content) => {
|
|
420
|
+
if (er) {
|
|
421
|
+
return this.error(er, res)
|
|
422
|
+
}
|
|
423
|
+
res.statusCode = 200
|
|
424
|
+
if (this.opt.cachedHeader) {
|
|
425
|
+
res.setHeader('x-from-cache', 'true')
|
|
426
|
+
}
|
|
427
|
+
if (gz && content.gz) {
|
|
428
|
+
res.setHeader('content-encoding', 'gzip')
|
|
429
|
+
res.setHeader('content-length', content.gz.length)
|
|
430
|
+
res.end(content.gz)
|
|
431
|
+
} else {
|
|
432
|
+
res.setHeader('content-length', content.length)
|
|
433
|
+
res.end(content)
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
}
|
|
417
437
|
|
|
418
|
-
|
|
419
|
-
|
|
438
|
+
streamFile (p, fd, stat, etag, req, res, end) {
|
|
439
|
+
const streamOpt = { fd: fd, start: 0, end: stat.size }
|
|
440
|
+
let stream = fs.createReadStream(p, streamOpt)
|
|
441
|
+
stream.destroy = () => {}
|
|
420
442
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
end()
|
|
427
|
-
})
|
|
443
|
+
// gzip only if not explicitly turned off or client doesn't accept it
|
|
444
|
+
const gzOpt = this.opt.gzip !== false
|
|
445
|
+
const gz = gzOpt && getGz(p, req)
|
|
446
|
+
const cachable = this.cache.content._cache.max > stat.size
|
|
447
|
+
let gzstr
|
|
428
448
|
|
|
429
|
-
|
|
449
|
+
// need a gzipped version for the cache, so do it regardless of what the client wants
|
|
450
|
+
if (gz || (gzOpt && cachable)) {
|
|
451
|
+
gzstr = zlib.Gzip()
|
|
452
|
+
}
|
|
430
453
|
|
|
431
|
-
|
|
454
|
+
// too late to effectively handle any errors.
|
|
455
|
+
// just kill the connection if that happens.
|
|
456
|
+
stream.on('error', (e) => {
|
|
457
|
+
console.error('Error serving %s fd=%d\n%s', p, fd, e.stack || e.message)
|
|
458
|
+
res.socket.destroy()
|
|
459
|
+
end()
|
|
460
|
+
})
|
|
432
461
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
stream.pipe(gzstr).pipe(res)
|
|
437
|
-
} else {
|
|
438
|
-
if (!res.filter) res.setHeader('content-length', stat.size)
|
|
439
|
-
stream.pipe(res)
|
|
440
|
-
if (gzstr)
|
|
441
|
-
stream.pipe(gzstr) // for cache
|
|
442
|
-
}
|
|
462
|
+
if (res.filter) {
|
|
463
|
+
stream = stream.pipe(res.filter)
|
|
464
|
+
}
|
|
443
465
|
|
|
444
|
-
|
|
445
|
-
process.nextTick(end)
|
|
446
|
-
})
|
|
466
|
+
res.statusCode = 200
|
|
447
467
|
|
|
448
|
-
|
|
449
|
-
|
|
468
|
+
if (gz) {
|
|
469
|
+
// we don't know how long it'll be, since it will be compressed.
|
|
470
|
+
res.setHeader('content-encoding', 'gzip')
|
|
471
|
+
stream.pipe(gzstr).pipe(res)
|
|
472
|
+
} else {
|
|
473
|
+
if (!res.filter) {
|
|
474
|
+
res.setHeader('content-length', stat.size)
|
|
475
|
+
}
|
|
476
|
+
stream.pipe(res)
|
|
477
|
+
if (gzstr) {
|
|
478
|
+
stream.pipe(gzstr)
|
|
479
|
+
} // for cache
|
|
480
|
+
}
|
|
450
481
|
|
|
451
|
-
|
|
482
|
+
stream.on('end', () => process.nextTick(end))
|
|
452
483
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
484
|
+
if (cachable) {
|
|
485
|
+
// collect it, and put it in the cache
|
|
486
|
+
|
|
487
|
+
let calls = 0
|
|
488
|
+
|
|
489
|
+
// called by bl() for both the raw stream and gzipped stream if we're
|
|
490
|
+
// caching gzipped data
|
|
491
|
+
const collectEnd = () => {
|
|
492
|
+
if (++calls === (gzOpt ? 2 : 1)) {
|
|
493
|
+
const content = bufs.slice()
|
|
494
|
+
content.gz = gzbufs && gzbufs.slice()
|
|
495
|
+
this.cache.content.set(key, content)
|
|
496
|
+
}
|
|
460
497
|
}
|
|
461
|
-
}.bind(this)
|
|
462
498
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
499
|
+
const key = stat.size + ':' + etag
|
|
500
|
+
const bufs = bl(collectEnd)
|
|
501
|
+
let gzbufs
|
|
466
502
|
|
|
467
|
-
|
|
503
|
+
stream.pipe(bufs)
|
|
468
504
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
505
|
+
if (gzstr) {
|
|
506
|
+
gzbufs = bl(collectEnd)
|
|
507
|
+
gzstr.pipe(gzbufs)
|
|
508
|
+
}
|
|
472
509
|
}
|
|
473
510
|
}
|
|
474
|
-
}
|
|
475
511
|
|
|
512
|
+
// cache-fillers
|
|
476
513
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
var url = p.substr(this.path.length).replace(/\\/g, '/')
|
|
482
|
-
var t = url
|
|
514
|
+
_loadIndex (p, cb) {
|
|
515
|
+
// truncate off the first bits
|
|
516
|
+
const url = p.substr(this.path.length).replace(/\\/g, '/')
|
|
517
|
+
const t = url
|
|
483
518
|
.replace(/"/g, '"')
|
|
484
519
|
.replace(/</g, '<')
|
|
485
520
|
.replace(/>/g, '>')
|
|
486
521
|
.replace(/'/g, ''')
|
|
487
522
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
523
|
+
let str =
|
|
524
|
+
'<!doctype html>' +
|
|
525
|
+
'<html>' +
|
|
526
|
+
'<head><title>Index of ' + t + '</title></head>' +
|
|
527
|
+
'<body>' +
|
|
528
|
+
'<h1>Index of ' + t + '</h1>' +
|
|
529
|
+
'<hr><pre><a href="../">../</a>\n'
|
|
495
530
|
|
|
496
|
-
|
|
497
|
-
|
|
531
|
+
this.cache.readdir.get(p, (er, data) => {
|
|
532
|
+
if (er) {
|
|
533
|
+
return cb(er)
|
|
534
|
+
}
|
|
498
535
|
|
|
499
|
-
|
|
500
|
-
|
|
536
|
+
let nameLen = 0
|
|
537
|
+
let sizeLen = 0
|
|
501
538
|
|
|
502
|
-
|
|
503
|
-
|
|
539
|
+
Object.keys(data).map((f) => {
|
|
540
|
+
const d = data[f]
|
|
504
541
|
|
|
505
|
-
|
|
542
|
+
let name = f
|
|
506
543
|
.replace(/"/g, '"')
|
|
507
544
|
.replace(/</g, '<')
|
|
508
545
|
.replace(/>/g, '>')
|
|
509
546
|
.replace(/'/g, ''')
|
|
510
547
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
548
|
+
if (d.size === '-') {
|
|
549
|
+
name += '/'
|
|
550
|
+
}
|
|
551
|
+
const showName = name.replace(/^(.{40}).{3,}$/, '$1..>')
|
|
552
|
+
const linkName = encodeURIComponent(name)
|
|
553
|
+
.replace(/%2e/ig, '.') // Encoded dots are dots
|
|
554
|
+
.replace(/%2f|%5c/ig, '/') // encoded slashes are /
|
|
555
|
+
.replace(/[/\\]/g, '/') // back slashes are slashes
|
|
556
|
+
|
|
557
|
+
nameLen = Math.max(nameLen, showName.length)
|
|
558
|
+
sizeLen = Math.max(sizeLen, ('' + d.size).length)
|
|
559
|
+
return ['<a href="' + linkName + '">' + showName + '</a>',
|
|
560
|
+
d.mtime, d.size, showName]
|
|
561
|
+
}).sort((a, b) => {
|
|
562
|
+
return a[2] === '-' && b[2] !== '-' ? -1 // dirs first
|
|
563
|
+
: a[2] !== '-' && b[2] === '-' ? 1
|
|
564
|
+
: a[0].toLowerCase() < b[0].toLowerCase() ? -1 // then alpha
|
|
565
|
+
: a[0].toLowerCase() > b[0].toLowerCase() ? 1
|
|
566
|
+
: 0
|
|
567
|
+
}).forEach((line) => {
|
|
568
|
+
const namePad = new Array(8 + nameLen - line[3].length).join(' ')
|
|
569
|
+
const sizePad = new Array(8 + sizeLen - ('' + line[2]).length).join(' ')
|
|
570
|
+
str += line[0] + namePad +
|
|
571
|
+
line[1].toISOString() +
|
|
572
|
+
sizePad + line[2] + '\n'
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
str += '</pre><hr></body></html>'
|
|
576
|
+
cb(null, Buffer.from(str))
|
|
534
577
|
})
|
|
578
|
+
}
|
|
535
579
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
580
|
+
_loadReaddir (p, cb) {
|
|
581
|
+
let len
|
|
582
|
+
let data
|
|
583
|
+
fs.readdir(p, (er, files) => {
|
|
584
|
+
if (er) {
|
|
585
|
+
return cb(er)
|
|
586
|
+
}
|
|
587
|
+
files = files.filter((f) => {
|
|
588
|
+
if (!this.opt.dot) {
|
|
589
|
+
return !/^\./.test(f)
|
|
590
|
+
} else {
|
|
591
|
+
return f !== '.' && f !== '..'
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
len = files.length
|
|
595
|
+
data = {}
|
|
596
|
+
files.forEach((file) => {
|
|
597
|
+
const pf = path.join(p, file)
|
|
598
|
+
this.cache.stat.get(pf, (er, stat) => {
|
|
599
|
+
if (er) {
|
|
600
|
+
return cb(er)
|
|
601
|
+
}
|
|
602
|
+
if (stat.isDirectory()) {
|
|
603
|
+
stat.size = '-'
|
|
604
|
+
}
|
|
605
|
+
data[file] = stat
|
|
606
|
+
next()
|
|
607
|
+
})
|
|
608
|
+
})
|
|
609
|
+
})
|
|
562
610
|
|
|
563
|
-
|
|
564
|
-
|
|
611
|
+
const next = () => {
|
|
612
|
+
if (--len === 0) {
|
|
613
|
+
cb(null, data)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
565
616
|
}
|
|
566
|
-
}
|
|
567
617
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
618
|
+
_loadStat (key, cb) {
|
|
619
|
+
// key is either fd:path or just a path
|
|
620
|
+
const fdp = key.match(/^(\d+):(.*)/)
|
|
621
|
+
if (fdp) {
|
|
622
|
+
const fd = +fdp[1]
|
|
623
|
+
const p = fdp[2]
|
|
624
|
+
fs.fstat(fd, (er, stat) => {
|
|
625
|
+
if (er) {
|
|
626
|
+
return cb(er)
|
|
627
|
+
}
|
|
628
|
+
this.cache.stat.set(p, stat)
|
|
629
|
+
cb(null, stat)
|
|
630
|
+
})
|
|
631
|
+
} else {
|
|
632
|
+
fs.stat(key, cb)
|
|
633
|
+
}
|
|
581
634
|
}
|
|
582
|
-
}
|
|
583
635
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
636
|
+
_loadContent () {
|
|
637
|
+
// this function should never be called.
|
|
638
|
+
// we check if the thing is in the cache, and if not, stream it in
|
|
639
|
+
// manually. this.cache.content.get() should not ever happen.
|
|
640
|
+
throw new Error('This should not ever happen')
|
|
641
|
+
}
|
|
589
642
|
}
|
|
590
643
|
|
|
591
644
|
function getEtag (s) {
|
|
592
645
|
return '"' + s.dev + '-' + s.ino + '-' + s.mtime.getTime() + '"'
|
|
593
646
|
}
|
|
594
647
|
|
|
595
|
-
function getGz (p,req) {
|
|
596
|
-
|
|
648
|
+
function getGz (p, req) {
|
|
649
|
+
let gz = false
|
|
597
650
|
if (!/\.t?gz$/.exec(p)) {
|
|
598
|
-
|
|
651
|
+
const neg = req.negotiator || new Neg(req)
|
|
599
652
|
gz = neg.preferredEncoding(['gzip', 'identity']) === 'gzip'
|
|
600
653
|
}
|
|
601
654
|
return gz
|
|
602
655
|
}
|
|
656
|
+
|
|
657
|
+
module.exports = st
|
|
658
|
+
module.exports.Mount = Mount
|