st 3.0.0 → 3.0.2

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.
@@ -0,0 +1,23 @@
1
+ name: Test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ fail-fast: false
7
+ matrix:
8
+ node: [lts/*, current]
9
+ os: [macos-latest, ubuntu-latest, windows-latest]
10
+ runs-on: ${{ matrix.os }}
11
+ steps:
12
+ - name: Checkout Repository
13
+ uses: actions/checkout@v4
14
+ - name: Use Node.js ${{ matrix.node }}
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: ${{ matrix.node }}
18
+ - name: Install Dependencies
19
+ run: |
20
+ npm install --no-progress
21
+ - name: Run tests
22
+ run: |
23
+ npm test
package/bin/server.js CHANGED
@@ -82,6 +82,7 @@ for (let i = 2; i < process.argv.length; i++) {
82
82
  case '--help':
83
83
  help()
84
84
  process.exit(0)
85
+ break
85
86
 
86
87
  case '-nc':
87
88
  case '--no-cache':
@@ -150,9 +151,9 @@ if (isNaN(port)) {
150
151
 
151
152
  const opt = {
152
153
  path: dir,
153
- url: url,
154
- index: index,
155
- dot: dot,
154
+ url,
155
+ index,
156
+ dot,
156
157
  cache: {
157
158
  fd: {},
158
159
  stat: {},
@@ -160,7 +161,7 @@ const opt = {
160
161
  readdir: {},
161
162
  content: {}
162
163
  },
163
- cors: cors
164
+ cors
164
165
  }
165
166
 
166
167
  if (cache === false) {
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "st",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "A module for serving static files. Does etags, caching, etc.",
5
5
  "main": "st.js",
6
6
  "bin": "bin/server.js",
7
7
  "dependencies": {
8
8
  "async-cache": "^1.1.0",
9
- "bl": "^5.0.0",
10
- "fd": "~0.0.3",
11
- "mime": "^2.5.2",
12
- "negotiator": "~0.6.2"
9
+ "bl": "^6.1.0",
10
+ "fd": "^0.0.3",
11
+ "mime": "^3.0.0",
12
+ "negotiator": "^1.0.0"
13
13
  },
14
14
  "optionalDependencies": {
15
15
  "graceful-fs": "^4.2.3"
@@ -17,8 +17,8 @@
17
17
  "devDependencies": {
18
18
  "request": "^2.88.2",
19
19
  "rimraf": "^3.0.2",
20
- "standard": "^16.0.3",
21
- "tap": "^15.0.9"
20
+ "standard": "^17.1.2",
21
+ "tap": "^15.2.3"
22
22
  },
23
23
  "scripts": {
24
24
  "lint": "standard",
package/st.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const mime = require('mime')
2
2
  const path = require('path')
3
- const url = require('url')
4
3
  let fs
5
4
  try {
6
5
  fs = require('graceful-fs')
@@ -180,22 +179,13 @@ class Mount {
180
179
 
181
180
  // get the path component from a URI
182
181
  getUriPath (u) {
183
- let p = url.parse(u).pathname // eslint-disable-line
182
+ let p = new URL(u, 'http://base').pathname
184
183
 
185
- // Encoded dots are dots
186
- p = p.replace(/%2e/ig, '.')
184
+ // Convert any backslashes to forward slashes (for consistency)
185
+ p = p.replace(/\\/g, '/')
187
186
 
188
- // encoded slashes are /
189
- p = p.replace(/%2f|%5c/ig, '/')
190
-
191
- // back slashes are slashes
192
- p = p.replace(/[/\\]/g, '/')
193
-
194
- // Make sure it starts with a slash
195
- p = p.replace(/^\//, '/')
187
+ // Remove the redundant leading slash replacement - URL().pathname always starts with /
196
188
  if ((/[/\\]\.\.([/\\]|$)/).test(p)) {
197
- // traversal urls not ever even slightly allowed. clearly shenanigans
198
- // send a 403 on that noise, do not pass go, do not collect $200
199
189
  return 403
200
190
  }
201
191
 
@@ -204,15 +194,18 @@ class Mount {
204
194
  return false
205
195
  }
206
196
 
197
+ // URL constructor already decoded, but we might need additional decoding
198
+ // for edge cases. Only do it if it would actually change something.
207
199
  try {
208
- u = decodeURIComponent(u)
200
+ const decoded = decodeURIComponent(u)
201
+ if (decoded !== u) {
202
+ u = decoded
203
+ }
209
204
  } catch (e) {
210
- // if decodeURIComponent failed, we weren't given a valid URL to begin with.
205
+ // if decodeURIComponent failed, we weren't given a valid URL to begin with.
211
206
  return false
212
207
  }
213
208
 
214
- // /a/b/c mounted on /path/to/z/d/x
215
- // /a/b/c/d --> /path/to/z/d/x/d
216
209
  u = u.substr(this.url.length)
217
210
  if (u.charAt(0) !== '/') {
218
211
  u = '/' + u
@@ -223,7 +216,10 @@ class Mount {
223
216
 
224
217
  // get a path from a url
225
218
  getPath (u) {
226
- return path.join(this.path, u)
219
+ // trailing slash removal to fix Node.js v23 bug
220
+ // https://github.com/nodejs/node/pull/55527
221
+ // can be removed when this is resolved and released
222
+ return path.join(this.path, u.replace(/\/+$/, ''))
227
223
  }
228
224
 
229
225
  // get a url from a path
@@ -442,7 +438,7 @@ class Mount {
442
438
  }
443
439
 
444
440
  streamFile (p, fd, stat, etag, req, res, end) {
445
- const streamOpt = { fd: fd, start: 0, end: stat.size }
441
+ const streamOpt = { fd, start: 0, end: stat.size }
446
442
  let stream = fs.createReadStream(p, streamOpt)
447
443
  stream.destroy = () => {}
448
444
 
@@ -454,7 +450,7 @@ class Mount {
454
450
 
455
451
  // need a gzipped version for the cache, so do it regardless of what the client wants
456
452
  if (gz || (gzOpt && cachable)) {
457
- gzstr = zlib.Gzip()
453
+ gzstr = zlib.createGzip()
458
454
  }
459
455
 
460
456
  // too late to effectively handle any errors.
package/test/basic.js CHANGED
@@ -97,6 +97,14 @@ test('multiball!', (t) => {
97
97
  }
98
98
  })
99
99
 
100
+ test('trailing slash', (t) => {
101
+ req('/test/test/fixtures/', (er, res, body) => {
102
+ t.equal(res.statusCode, 200)
103
+ t.ok(/<html>.*Index of \/test\/fixtures<[\s\S]+index\.html[\s\S]+space in filename\.txt[\s\S]+<\/html>/.test(body.toString()))
104
+ t.end()
105
+ })
106
+ })
107
+
100
108
  test('space in filename', (t) => {
101
109
  req('/test/test/fixtures/space in filename.txt', (er, res, body) => {
102
110
  t.equal(res.statusCode, 200)
@@ -117,17 +125,8 @@ test('shenanigans', (t) => {
117
125
  if (er) {
118
126
  throw er
119
127
  }
120
- t.equal(res.statusCode, 403)
121
- t.end()
122
- })
123
- })
124
-
125
- test('shenanigans2', (t) => {
126
- req('/test//foo/%2e%2E', (er, res) => {
127
- if (er) {
128
- throw er
129
- }
130
- t.equal(res.statusCode, 403)
128
+ // resolves to simply ./etc/passwd
129
+ t.equal(res.statusCode, 404)
131
130
  t.end()
132
131
  })
133
132
  })
@@ -91,8 +91,8 @@ function serve (args, cbRequests, cbDone) {
91
91
  ++thingsToDo
92
92
  request({
93
93
  encoding: null,
94
- url: url,
95
- headers: headers
94
+ url,
95
+ headers
96
96
  }, (...args) => {
97
97
  try {
98
98
  cb.apply(null, args)
@@ -1,4 +1,5 @@
1
1
  const os = require('os')
2
+ const dns = require('dns')
2
3
  let { test, fail, comment } = require('tap')
3
4
  const { serve } = require('./common')
4
5
  const port = 1338
@@ -73,12 +74,6 @@ testServer(
73
74
  ['127.0.0.1', 'localhost', otherAddress], []
74
75
  )
75
76
 
76
- testServer(
77
- 'Restricted to localhost',
78
- ['--localhost'], 'localhost',
79
- ['127.0.0.1', 'localhost'], [otherAddress]
80
- )
81
-
82
77
  testServer(
83
78
  'Restricted to non-local host',
84
79
  ['--host', otherAddress], otherAddress,
@@ -90,3 +85,14 @@ testServer(
90
85
  ['--host', '127.0.0.1'], '127.0.0.1',
91
86
  ['127.0.0.1'], ['::1']
92
87
  )
88
+
89
+ dns.lookup('localhost', (err, address) => {
90
+ if (err) {
91
+ throw err
92
+ }
93
+ testServer(
94
+ 'Restricted to localhost',
95
+ ['--localhost'], 'localhost',
96
+ [address, 'localhost'], [otherAddress]
97
+ )
98
+ })
package/test/common.js CHANGED
@@ -32,7 +32,7 @@ function req (url, headers, cb) {
32
32
  request({
33
33
  encoding: null,
34
34
  url: `http://${host}:${address.port}${url}`,
35
- headers: headers,
35
+ headers,
36
36
  followRedirect: false
37
37
  }, cb)
38
38
  }
@@ -12,7 +12,10 @@ test('does not gzip first response', (t) => {
12
12
  t.notOk(res.headers['content-encoding'])
13
13
  t.notOk(res.headers['x-from-cache'])
14
14
  t.equal(body.toString(), stExpect)
15
- t.end()
15
+ // response is delivered before cache is set, so we'll give it a tiny bit
16
+ // of grace to close out on the server side before starting the next
17
+ // test
18
+ setTimeout(t.end, 100)
16
19
  })
17
20
  })
18
21
 
@@ -47,13 +47,13 @@ function req (url, headers, cb) {
47
47
  request({
48
48
  encoding: null,
49
49
  url: `http://localhost:${port}${url}`,
50
- headers: headers,
50
+ headers,
51
51
  agentOptions: { maxSockets: 50 }
52
52
  }, next)
53
53
  request({
54
54
  encoding: null,
55
55
  url: `http://localhost:${(port + 1)}${url}`,
56
- headers: headers,
56
+ headers,
57
57
  agentOptions: { maxSockets: 50 }
58
58
  }, next)
59
59
 
@@ -76,7 +76,7 @@ function req (url, headers, cb) {
76
76
  assert.strictEqual(`${body}`, `${prev.body}`)
77
77
  return cb(er, res, body)
78
78
  }
79
- prev = { res: res, body: body }
79
+ prev = { res, body }
80
80
  }
81
81
  }
82
82
 
@@ -90,7 +90,7 @@ test('setup middleware server', function (t) {
90
90
  })
91
91
  })
92
92
  })
93
- middlewareServer.listen(port, '127.0.0.1', function () {
93
+ middlewareServer.listen(port, 'localhost', function () {
94
94
  t.pass('listening')
95
95
  t.end()
96
96
  })
@@ -103,7 +103,7 @@ test('setup regular server', function (t) {
103
103
  return res.end(`Not a match: ${req.url}`)
104
104
  }
105
105
  })
106
- server.listen(port + 1, '127.0.0.1', function () {
106
+ server.listen(port + 1, 'localhost', function () {
107
107
  t.pass('listening')
108
108
  t.end()
109
109
  })
@@ -114,6 +114,7 @@ const stExpect = fs.readFileSync(require.resolve('../st.js')).toString()
114
114
 
115
115
  test('/test/st.js', function (t) {
116
116
  req('/test/st.js', function (er, res, body) {
117
+ t.error(er)
117
118
  t.equal(res.statusCode, 200)
118
119
  t.ok(res.headers.etag)
119
120
  stEtag = res.headers.etag
@@ -23,7 +23,7 @@ function req (url, headers, cb) {
23
23
  request({
24
24
  encoding: null,
25
25
  url: 'http://localhost:' + port + url,
26
- headers: headers
26
+ headers
27
27
  }, cb)
28
28
  }
29
29