st 1.1.0 → 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 +13 -0
- package/README.md +36 -22
- package/bin/server.js +95 -60
- package/package.json +12 -10
- package/st.js +504 -445
- package/test/basic.js +57 -49
- 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 +42 -30
- package/test/cors.js +11 -13
- package/test/dot-common.js +25 -19
- package/test/dot-false.js +6 -8
- package/test/dot-true.js +8 -8
- package/test/gzip-after-no-gzip.js +12 -19
- package/test/middleware.js +8 -14
- package/test/multi-mount.js +100 -83
- package/test/no-cache.js +8 -10
- package/test/no-cors.js +4 -5
- package/test/no-gzip-accepted-no-cache.js +18 -25
- package/test/no-gzip-accepted.js +18 -24
- package/test/no-gzip.js +4 -9
- package/test/parent-path.js +1 -1
- package/test/passthrough.js +36 -42
- package/test/preset-cache-control.js +31 -27
- package/.npmignore +0 -1
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,537 +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
|
-
}
|
|
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
|
+
}
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
index: none,
|
|
128
|
-
readdir: none,
|
|
129
|
-
content: none
|
|
130
|
-
}
|
|
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)
|
|
140
|
+
}
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
149
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
var d = defaultCacheOptions
|
|
146
|
-
|
|
147
|
-
// should really only ever set max and maxAge here.
|
|
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'),
|
|
155
|
-
}
|
|
150
|
+
if (o === false) {
|
|
151
|
+
o = noCaching
|
|
152
|
+
} else if (!o) {
|
|
153
|
+
o = {}
|
|
154
|
+
}
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
c.fd.load = this.fdman.open.bind(this.fdman)
|
|
156
|
+
const d = defaultCacheOptions
|
|
159
157
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
}
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
var p = url.parse(u).pathname
|
|
168
|
+
c.fd.dispose = this.fdman.close.bind(this.fdman)
|
|
169
|
+
c.fd.load = this.fdman.open.bind(this.fdman)
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
176
|
+
}
|
|
173
177
|
|
|
174
|
-
//
|
|
175
|
-
|
|
178
|
+
// get the path component from a URI
|
|
179
|
+
getUriPath (u) {
|
|
180
|
+
let p = url.parse(u).pathname // eslint-disable-line
|
|
176
181
|
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
// Encoded dots are dots
|
|
183
|
+
p = p.replace(/%2e/ig, '.')
|
|
179
184
|
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
// encoded slashes are /
|
|
186
|
+
p = p.replace(/%2f|%5c/ig, '/')
|
|
182
187
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// send a 403 on that noise, do not pass go, do not collect $200
|
|
186
|
-
return 403
|
|
187
|
-
}
|
|
188
|
+
// back slashes are slashes
|
|
189
|
+
p = p.replace(/[/\\]/g, '/')
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
}
|
|
191
198
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// if decodeURIComponent failed, we weren't given a valid URL to begin with.
|
|
197
|
-
return false
|
|
198
|
-
}
|
|
199
|
+
u = path.normalize(p).replace(/\\/g, '/')
|
|
200
|
+
if (u.indexOf(this.url) !== 0) {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
199
203
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|
|
204
210
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
}
|
|
208
217
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
p = path.resolve(p)
|
|
212
|
-
if (p.indexOf(this.path) !== 0) return false
|
|
213
|
-
p = path.join('/', p.substr(this.path.length))
|
|
214
|
-
var u = path.join(this.url, p).replace(/\\/g, '/')
|
|
215
|
-
return u
|
|
216
|
-
}
|
|
218
|
+
return u
|
|
219
|
+
}
|
|
217
220
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return false
|
|
221
|
+
// get a path from a url
|
|
222
|
+
getPath (u) {
|
|
223
|
+
return path.join(this.path, u)
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
231
|
+
}
|
|
232
|
+
p = path.join('/', p.substr(this.path.length))
|
|
233
|
+
const u = path.join(this.url, p).replace(/\\/g, '/')
|
|
234
|
+
return u
|
|
235
|
+
}
|
|
227
236
|
|
|
228
|
-
|
|
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
|
+
}
|
|
229
244
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (typeof next === 'function') next()
|
|
235
|
-
return false
|
|
236
|
-
}
|
|
245
|
+
// querystrings are of no concern to us
|
|
246
|
+
if (!req.sturl) {
|
|
247
|
+
req.sturl = this.getUriPath(req.url)
|
|
248
|
+
}
|
|
237
249
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
}
|
|
245
257
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return next()
|
|
255
|
-
return this.error(er, res)
|
|
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
|
|
256
266
|
}
|
|
257
267
|
|
|
258
|
-
|
|
259
|
-
this.fdman.checkout(p, fd)
|
|
260
|
-
// a safe end() function that can be called multiple times but
|
|
261
|
-
// only perform a single checkin
|
|
262
|
-
var end = this.fdman.checkinfn(p, fd)
|
|
268
|
+
const p = this.getPath(req.sturl)
|
|
263
269
|
|
|
264
|
-
|
|
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.
|
|
265
276
|
if (er) {
|
|
266
|
-
if (
|
|
277
|
+
if (this.opt.passthrough === true && er.code === 'ENOENT' && next) {
|
|
267
278
|
return next()
|
|
268
279
|
}
|
|
269
|
-
end()
|
|
270
280
|
return this.error(er, res)
|
|
271
281
|
}
|
|
272
282
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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)
|
|
282
296
|
}
|
|
283
|
-
}
|
|
284
297
|
|
|
285
|
-
|
|
286
|
-
if (ims) ims = new Date(ims).getTime()
|
|
287
|
-
if (ims && ims >= stat.mtime.getTime()) {
|
|
288
|
-
res.statusCode = 304
|
|
289
|
-
res.end()
|
|
290
|
-
return end()
|
|
291
|
-
}
|
|
298
|
+
const isDirectory = stat.isDirectory()
|
|
292
299
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
}
|
|
299
309
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
305
319
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
320
|
+
const etag = getEtag(stat)
|
|
321
|
+
if (req.headers['if-none-match'] === etag) {
|
|
322
|
+
res.statusCode = 304
|
|
323
|
+
res.end()
|
|
324
|
+
return end()
|
|
325
|
+
}
|
|
311
326
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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)
|
|
317
333
|
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
}
|
|
320
339
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
340
|
+
return isDirectory
|
|
341
|
+
? this.index(p, req, res)
|
|
342
|
+
: this.file(p, fd, stat, etag, req, res, end)
|
|
343
|
+
})
|
|
344
|
+
})
|
|
326
345
|
|
|
327
|
-
|
|
328
|
-
// pattern of express and ErrorPage
|
|
329
|
-
return res.error(res.statusCode, er)
|
|
346
|
+
return true
|
|
330
347
|
}
|
|
331
348
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
335
354
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (typeof this._index === 'string') {
|
|
341
|
-
if (!/\/$/.test(req.sturl)) req.sturl += '/'
|
|
342
|
-
req.sturl += this._index
|
|
343
|
-
return this.serve(req, res)
|
|
344
|
-
}
|
|
345
|
-
return this.error(404, res)
|
|
346
|
-
}
|
|
355
|
+
if (typeof res.error === 'function') {
|
|
356
|
+
// pattern of express and ErrorPage
|
|
357
|
+
return res.error(res.statusCode, er)
|
|
358
|
+
}
|
|
347
359
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
res.statusCode = 301
|
|
351
|
-
res.setHeader('location', req.sturl + '/')
|
|
352
|
-
res.end('Moved: ' + req.sturl + '/')
|
|
353
|
-
return
|
|
360
|
+
res.setHeader('content-type', 'text/plain')
|
|
361
|
+
res.end(http.STATUS_CODES[res.statusCode] + '\n')
|
|
354
362
|
}
|
|
355
363
|
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
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
|
+
}
|
|
366
385
|
|
|
367
|
-
|
|
368
|
-
|
|
386
|
+
this.cache.index.get(p, (er, html) => {
|
|
387
|
+
if (er) {
|
|
388
|
+
return this.error(er, res)
|
|
389
|
+
}
|
|
369
390
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
391
|
+
res.statusCode = 200
|
|
392
|
+
res.setHeader('content-type', 'text/html')
|
|
393
|
+
res.setHeader('content-length', html.length)
|
|
394
|
+
res.end(html)
|
|
395
|
+
})
|
|
373
396
|
}
|
|
374
397
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
end()
|
|
378
|
-
this.cachedFile(p, stat, etag, req, res)
|
|
379
|
-
} else {
|
|
380
|
-
this.streamFile(p, fd, stat, etag, req, res, end)
|
|
381
|
-
}
|
|
382
|
-
}
|
|
398
|
+
file (p, fd, stat, etag, req, res, end) {
|
|
399
|
+
const key = stat.size + ':' + etag
|
|
383
400
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
401
|
+
const mt = mime.getType(path.extname(p))
|
|
402
|
+
if (mt !== 'application/octet-stream') {
|
|
403
|
+
res.setHeader('content-type', mt)
|
|
404
|
+
}
|
|
387
405
|
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
res.setHeader('x-from-cache', 'true')
|
|
393
|
-
if (gz && content.gz) {
|
|
394
|
-
res.setHeader('content-encoding', 'gzip')
|
|
395
|
-
res.setHeader('content-length', content.gz.length)
|
|
396
|
-
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)
|
|
397
410
|
} else {
|
|
398
|
-
|
|
399
|
-
res.end(content)
|
|
411
|
+
this.streamFile(p, fd, stat, etag, req, res, end)
|
|
400
412
|
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
413
|
+
}
|
|
403
414
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
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)
|
|
408
418
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
+
}
|
|
414
437
|
|
|
415
|
-
|
|
416
|
-
|
|
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 = () => {}
|
|
417
442
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
end()
|
|
424
|
-
})
|
|
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
|
|
425
448
|
|
|
426
|
-
|
|
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
|
+
}
|
|
427
453
|
|
|
428
|
-
|
|
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
|
+
})
|
|
429
461
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
stream.pipe(gzstr).pipe(res)
|
|
434
|
-
} else {
|
|
435
|
-
if (!res.filter) res.setHeader('content-length', stat.size)
|
|
436
|
-
stream.pipe(res)
|
|
437
|
-
if (gzstr)
|
|
438
|
-
stream.pipe(gzstr) // for cache
|
|
439
|
-
}
|
|
462
|
+
if (res.filter) {
|
|
463
|
+
stream = stream.pipe(res.filter)
|
|
464
|
+
}
|
|
440
465
|
|
|
441
|
-
|
|
442
|
-
process.nextTick(end)
|
|
443
|
-
})
|
|
466
|
+
res.statusCode = 200
|
|
444
467
|
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
}
|
|
447
481
|
|
|
448
|
-
|
|
482
|
+
stream.on('end', () => process.nextTick(end))
|
|
449
483
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
}
|
|
457
497
|
}
|
|
458
|
-
}.bind(this)
|
|
459
498
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
499
|
+
const key = stat.size + ':' + etag
|
|
500
|
+
const bufs = bl(collectEnd)
|
|
501
|
+
let gzbufs
|
|
463
502
|
|
|
464
|
-
|
|
503
|
+
stream.pipe(bufs)
|
|
465
504
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
505
|
+
if (gzstr) {
|
|
506
|
+
gzbufs = bl(collectEnd)
|
|
507
|
+
gzstr.pipe(gzbufs)
|
|
508
|
+
}
|
|
469
509
|
}
|
|
470
510
|
}
|
|
471
|
-
}
|
|
472
511
|
|
|
512
|
+
// cache-fillers
|
|
473
513
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
var url = p.substr(this.path.length).replace(/\\/g, '/')
|
|
479
|
-
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
|
|
480
518
|
.replace(/"/g, '"')
|
|
481
519
|
.replace(/</g, '<')
|
|
482
520
|
.replace(/>/g, '>')
|
|
483
521
|
.replace(/'/g, ''')
|
|
484
522
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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'
|
|
492
530
|
|
|
493
|
-
|
|
494
|
-
|
|
531
|
+
this.cache.readdir.get(p, (er, data) => {
|
|
532
|
+
if (er) {
|
|
533
|
+
return cb(er)
|
|
534
|
+
}
|
|
495
535
|
|
|
496
|
-
|
|
497
|
-
|
|
536
|
+
let nameLen = 0
|
|
537
|
+
let sizeLen = 0
|
|
498
538
|
|
|
499
|
-
|
|
500
|
-
|
|
539
|
+
Object.keys(data).map((f) => {
|
|
540
|
+
const d = data[f]
|
|
501
541
|
|
|
502
|
-
|
|
542
|
+
let name = f
|
|
503
543
|
.replace(/"/g, '"')
|
|
504
544
|
.replace(/</g, '<')
|
|
505
545
|
.replace(/>/g, '>')
|
|
506
546
|
.replace(/'/g, ''')
|
|
507
547
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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))
|
|
531
577
|
})
|
|
578
|
+
}
|
|
532
579
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
})
|
|
559
610
|
|
|
560
|
-
|
|
561
|
-
|
|
611
|
+
const next = () => {
|
|
612
|
+
if (--len === 0) {
|
|
613
|
+
cb(null, data)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
562
616
|
}
|
|
563
|
-
}
|
|
564
617
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
+
}
|
|
578
634
|
}
|
|
579
|
-
}
|
|
580
635
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
}
|
|
586
642
|
}
|
|
587
643
|
|
|
588
644
|
function getEtag (s) {
|
|
589
645
|
return '"' + s.dev + '-' + s.ino + '-' + s.mtime.getTime() + '"'
|
|
590
646
|
}
|
|
591
647
|
|
|
592
|
-
function getGz (p,req) {
|
|
593
|
-
|
|
648
|
+
function getGz (p, req) {
|
|
649
|
+
let gz = false
|
|
594
650
|
if (!/\.t?gz$/.exec(p)) {
|
|
595
|
-
|
|
651
|
+
const neg = req.negotiator || new Neg(req)
|
|
596
652
|
gz = neg.preferredEncoding(['gzip', 'identity']) === 'gzip'
|
|
597
653
|
}
|
|
598
654
|
return gz
|
|
599
655
|
}
|
|
656
|
+
|
|
657
|
+
module.exports = st
|
|
658
|
+
module.exports.Mount = Mount
|