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/test/basic.js ADDED
@@ -0,0 +1,133 @@
1
+ const zlib = require('zlib')
2
+ const { test } = require('tap')
3
+ const { req, stExpect, opts, mount } = require('./common')
4
+
5
+ module.exports.req = req
6
+ module.exports.stExpect = stExpect
7
+ module.exports.mount = mount
8
+
9
+ let stEtag
10
+
11
+ test('simple request', (t) => {
12
+ req('/test/st.js', (er, res, body) => {
13
+ t.error(er)
14
+ t.equal(res.statusCode, 200)
15
+ t.ok(/\/javascript$/.test(res.headers['content-type']))
16
+ t.ok(res.headers.etag)
17
+ stEtag = res.headers.etag
18
+ t.equal(body.toString(), stExpect)
19
+ t.end()
20
+ })
21
+ })
22
+
23
+ test('304 request', (t) => {
24
+ req('/test/st.js', { 'if-none-match': stEtag }, (er, res, body) => {
25
+ t.equal(res.statusCode, 304)
26
+ t.equal(body.length, 0)
27
+ t.end()
28
+ })
29
+ })
30
+
31
+ if (opts.gzip !== false) {
32
+ test('gzip', (t) => {
33
+ req('/test/st.js', { 'accept-encoding': 'gzip' },
34
+ (er, res, body) => {
35
+ t.equal(res.statusCode, 200)
36
+ t.equal(res.headers['content-encoding'], 'gzip')
37
+ zlib.gunzip(body, (er, body) => {
38
+ if (er) {
39
+ throw er
40
+ }
41
+ t.equal(body.toString(), stExpect)
42
+ t.end()
43
+ })
44
+ })
45
+ })
46
+ }
47
+
48
+ test('multiball!', (t) => {
49
+ let n = 6
50
+ req('/test/st.js', then)
51
+ req('/test/README.md', then)
52
+ req('/test/LICENSE', then)
53
+ req('/test/package.json', then)
54
+ req('/test/favicon.ico', then)
55
+ req('/test/bin/server.js', then)
56
+
57
+ function then (er, res) {
58
+ if (er) {
59
+ throw er
60
+ }
61
+ t.equal(res.statusCode, 200)
62
+
63
+ // give them all time to close, then go again.
64
+ if (--n === 0) {
65
+ setTimeout(() => {
66
+ n = 6
67
+ req('/test/st.js', then2)
68
+ req('/test/README.md', then2)
69
+ req('/test/LICENSE', then2)
70
+ req('/test/package.json', then2)
71
+ req('/test/favicon.ico', then2)
72
+ req('/test/bin/server.js', then2)
73
+ }, 200)
74
+ }
75
+ }
76
+
77
+ function then2 (er, res) {
78
+ if (er) {
79
+ throw er
80
+ }
81
+
82
+ t.equal(res.statusCode, 200)
83
+
84
+ if (opts.cache === false) {
85
+ t.equal(res.headers['cache-control'], 'no-cache')
86
+ } else if (opts.cache && opts.cache.content && opts.cache.content.maxAge === false) {
87
+ t.ok(res.headers['cache-control'] === undefined)
88
+ } else if (opts.cache && opts.cache.content && opts.cache.content.cacheControl) {
89
+ t.equal(res.headers['cache-control'], opts.cache.content.cacheControl)
90
+ } else {
91
+ t.equal(res.headers['cache-control'], 'public, max-age=600')
92
+ }
93
+
94
+ if (--n === 0) {
95
+ t.end()
96
+ }
97
+ }
98
+ })
99
+
100
+ test('space in filename', (t) => {
101
+ req('/test/test/fixtures/space in filename.txt', (er, res, body) => {
102
+ t.equal(res.statusCode, 200)
103
+ t.ok(body)
104
+ t.end()
105
+ })
106
+ })
107
+
108
+ test('malformed URL', (t) => {
109
+ req('/test%2E%git', (er, res) => {
110
+ t.equal(res.statusCode, 404)
111
+ t.end()
112
+ })
113
+ })
114
+
115
+ test('shenanigans', (t) => {
116
+ req('/%2e%2E/%2e%2E/%2e%2E/%2e%2E/%2e%2E/%2e%2E/etc/passwd', (er, res) => {
117
+ if (er) {
118
+ throw er
119
+ }
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)
131
+ t.end()
132
+ })
133
+ })
@@ -0,0 +1,32 @@
1
+ const { test } = require('tap')
2
+ const { serve, stExpect } = require('./common')
3
+
4
+ test('Basic cli operation', (t) => {
5
+ serve([], (req) => {
6
+ req('/st.js', (er, res, body) => {
7
+ t.ifError(er) &&
8
+ t.equal(res.statusCode, 200) &&
9
+ t.equal(body.toString(), stExpect)
10
+ })
11
+ }, (er, stdout, stderr) => {
12
+ t.ifError(er)
13
+ t.match(stdout, /^listening at http:\/\/(\[::\]|0\.0\.0\.0):[0-9]+\n$/)
14
+ t.equal(stderr, '')
15
+ t.end()
16
+ })
17
+ })
18
+
19
+ test('Listening on localhost only', (t) => {
20
+ serve(['--localhost'], (req) => {
21
+ req('/st.js', (er, res, body) => {
22
+ t.ifError(er) &&
23
+ t.equal(res.statusCode, 200) &&
24
+ t.equal(body.toString(), stExpect)
25
+ })
26
+ }, (er, stdout, stderr) => {
27
+ t.ifError(er)
28
+ t.match(stdout, /^listening at http:\/\/localhost:[0-9]+\n$/)
29
+ t.equal(stderr, '')
30
+ t.end()
31
+ })
32
+ })
@@ -0,0 +1,108 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+ const fs = require('fs')
5
+ const request = require('request')
6
+ const childProcess = require('child_process')
7
+ const bl = require('bl')
8
+
9
+ const port = process.env.PORT || 1337
10
+
11
+ const stExpect = fs.readFileSync(require.resolve('../../st.js'), 'utf8')
12
+
13
+ // Run server with given command line arguments,
14
+ // then allow cbRequests to schedule a bunch of requests,
15
+ // finally call cbDone.
16
+ // cbRequests gets the req function as an argument.
17
+
18
+ function serve (args, cbRequests, cbDone) {
19
+ args = [require.resolve('../../bin/server.js')].concat(args || [])
20
+ const server = childProcess.spawn(process.execPath, args, {
21
+ cwd: path.dirname(path.dirname(__dirname)),
22
+ stdio: ['ignore', 'pipe', 'pipe'],
23
+ env: { LANG: 'C', LC_ALL: 'C' }
24
+ })
25
+ const stdout = bl()
26
+ const stderr = bl()
27
+ server.stdout.pipe(stdout)
28
+ server.stderr.pipe(stderr)
29
+ let thingsToDo = 4 // cbRequests, exit, stdout, stderr
30
+ let code = null
31
+ let signal = null
32
+ let cbReqEr = null
33
+ let outputSeen = false
34
+ server.once('error', (er) => {
35
+ thingsToDo = -10 // only call cbDone once
36
+ cbDone(er)
37
+ })
38
+ server.once('exit', (c, s) => {
39
+ code = c
40
+ signal = s
41
+ if (!outputSeen) {
42
+ outputSeen = true
43
+ --thingsToDo
44
+ }
45
+ then()
46
+ })
47
+ stdout.once('finish', then)
48
+ stderr.once('finish', then)
49
+ server.stdout.once('data', () => {
50
+ if (outputSeen) return
51
+ outputSeen = true
52
+ try {
53
+ cbRequests(req)
54
+ } catch (er) {
55
+ cbReqEr = er
56
+ } finally {
57
+ then()
58
+ }
59
+ })
60
+
61
+ function then () {
62
+ --thingsToDo
63
+ if (thingsToDo === 3) { // all requests done, one way or another
64
+ server.kill()
65
+ } else if (thingsToDo === 0) {
66
+ let er = null
67
+ if (cbReqEr) {
68
+ er = cbReqEr
69
+ } else if (signal !== null && signal !== 'SIGTERM') {
70
+ er = Error('Terminated by signal ' + signal)
71
+ } else if (code !== null && code !== 0) {
72
+ er = Error('Exited with code ' + code)
73
+ }
74
+ const o = stdout.toString(); const e = stderr.toString()
75
+ if (er) {
76
+ console.info(o)
77
+ console.error(e)
78
+ }
79
+ cbDone(er, o, e)
80
+ }
81
+ }
82
+
83
+ function req (url, headers, cb) {
84
+ if (typeof headers === 'function') {
85
+ cb = headers
86
+ headers = {}
87
+ }
88
+ if (!/:\/\//.test(url)) {
89
+ url = 'http://localhost:' + port + url
90
+ }
91
+ ++thingsToDo
92
+ request({
93
+ encoding: null,
94
+ url: url,
95
+ headers: headers
96
+ }, (...args) => {
97
+ try {
98
+ cb.apply(null, args)
99
+ } finally {
100
+ then()
101
+ }
102
+ })
103
+ }
104
+ }
105
+
106
+ module.exports.port = port
107
+ module.exports.stExpect = stExpect
108
+ module.exports.serve = serve
@@ -0,0 +1,92 @@
1
+ const os = require('os')
2
+ let { test, fail, comment } = require('tap')
3
+ const { serve } = require('./common')
4
+ const port = 1338
5
+
6
+ const otherAddress = (() => {
7
+ const ifaces = os.networkInterfaces()
8
+ for (const iface in ifaces) {
9
+ const addrs = ifaces[iface]
10
+ for (let i = 0; i < addrs.length; ++i) {
11
+ const addr = addrs[i].address
12
+ if (/^127\./.test(addr) || /^::1$/.test(addr)) { // loopback device
13
+ continue
14
+ }
15
+ if (/^fe80:/.test(addr)) { // link-local address
16
+ continue
17
+ }
18
+ return addr
19
+ }
20
+ }
21
+ return null
22
+ })()
23
+ if (!otherAddress) {
24
+ fail('No non-loopback network address found', { skip: true })
25
+ test = () => {}
26
+ } else {
27
+ comment('Using ' + otherAddress + ' as non-localhost address')
28
+ }
29
+
30
+ function addr2url (addr, path) {
31
+ if (/:/.test(addr)) {
32
+ addr = '[' + addr + ']'
33
+ }
34
+ addr = 'http://' + addr + ':' + port
35
+ if (path) {
36
+ addr += path
37
+ }
38
+ return addr
39
+ }
40
+
41
+ function testServer (name, args, addr, canConnect, cannotConnect) {
42
+ test(name, (t) => {
43
+ serve(args.concat(['--port', port]), (req) => {
44
+ canConnect.forEach(checkConnections(t, req, true))
45
+ cannotConnect.forEach(checkConnections(t, req, false))
46
+ }, (err, stdout, stderr) => {
47
+ t.ifError(err)
48
+ t.equal(stderr, '')
49
+ if (addr) {
50
+ t.equal(stdout, 'listening at ' + addr2url(addr) + '\n')
51
+ }
52
+ t.end()
53
+ })
54
+ })
55
+ }
56
+
57
+ function checkConnections (t, req, canConnect) {
58
+ return (addr) => {
59
+ const url = addr2url(addr, '/st.js')
60
+ req(url, (er, res, body) => {
61
+ if (canConnect) {
62
+ t.ifError(er, url) && t.equal(res.statusCode, 200, url)
63
+ } else {
64
+ t.ok(er, url)
65
+ }
66
+ })
67
+ }
68
+ }
69
+
70
+ testServer(
71
+ 'Listening on all ports by default',
72
+ [], null,
73
+ ['127.0.0.1', 'localhost', otherAddress], []
74
+ )
75
+
76
+ testServer(
77
+ 'Restricted to localhost',
78
+ ['--localhost'], 'localhost',
79
+ ['127.0.0.1', 'localhost'], [otherAddress]
80
+ )
81
+
82
+ testServer(
83
+ 'Restricted to non-local host',
84
+ ['--host', otherAddress], otherAddress,
85
+ [otherAddress], ['127.0.0.1']
86
+ )
87
+
88
+ testServer(
89
+ 'Restricted to IPv4',
90
+ ['--host', '127.0.0.1'], '127.0.0.1',
91
+ ['127.0.0.1'], ['::1']
92
+ )
package/test/common.js ADDED
@@ -0,0 +1,66 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const http = require('http')
4
+ const request = require('request')
5
+ const { test, tearDown } = require('tap')
6
+
7
+ const st = require('../st.js')
8
+
9
+ let address
10
+ let server
11
+
12
+ const opts = Object.assign({
13
+ autoindex: true,
14
+ path: path.dirname(__dirname),
15
+ url: '/test'
16
+ }, global.options || {})
17
+
18
+ const stExpect = fs.readFileSync(require.resolve('../st.js'), 'utf8')
19
+ const mount = st(opts)
20
+
21
+ function req (url, headers, cb) {
22
+ if (typeof headers === 'function') {
23
+ cb = headers
24
+ headers = {}
25
+ }
26
+
27
+ let host = address.address
28
+ if (address.family === 'IPv6') {
29
+ host = `[${host}]`
30
+ }
31
+
32
+ request({
33
+ encoding: null,
34
+ url: `http://${host}:${address.port}${url}`,
35
+ headers: headers,
36
+ followRedirect: false
37
+ }, cb)
38
+ }
39
+
40
+ test('setup', (t) => {
41
+ server = http.createServer((req, res) => {
42
+ try {
43
+ if (!mount(req, res)) {
44
+ res.statusCode = 404
45
+ return res.end(`Not a match: ${req.url}`)
46
+ }
47
+ } catch (e) {
48
+ res.statusCode = 500
49
+ console.error(e)
50
+ return res.end(`Internal error: ${e.message}`)
51
+ }
52
+ }).listen(0, '127.0.0.1', () => {
53
+ t.pass('listening')
54
+ address = server.address()
55
+ t.end()
56
+ })
57
+ })
58
+
59
+ tearDown(() => {
60
+ server.close()
61
+ })
62
+
63
+ module.exports.mount = mount
64
+ module.exports.req = req
65
+ module.exports.stExpect = stExpect
66
+ module.exports.opts = opts
package/test/cors.js ADDED
@@ -0,0 +1,19 @@
1
+ global.options = {
2
+ cors: true
3
+ }
4
+
5
+ const { test } = require('tap')
6
+ const { req } = require('./common')
7
+
8
+ test('CORS headers', (t) => {
9
+ req('/test/st.js', (er, res) => {
10
+ t.equal(res.headers['access-control-allow-origin'], '*')
11
+ const headers = res.headers['access-control-allow-headers']
12
+ t.ok(/Origin/.test(headers))
13
+ t.ok(/X-Requested-With/.test(headers))
14
+ t.ok(/Content-Type/.test(headers))
15
+ t.ok(/Accept/.test(headers))
16
+ t.ok(/Range/.test(headers))
17
+ t.end()
18
+ })
19
+ })
@@ -0,0 +1,44 @@
1
+ const path = require('path')
2
+ const http = require('http')
3
+ const request = require('request')
4
+ const { test, tearDown } = require('tap')
5
+
6
+ const st = require('../st.js')
7
+
8
+ let address
9
+ let server
10
+
11
+ const opts = {
12
+ dot: global.dot,
13
+ path: path.join(__dirname, 'fixtures', '.dotted-dir')
14
+ }
15
+
16
+ const mount = st(opts)
17
+
18
+ const req = (url, cb) => {
19
+ let host = address.address
20
+ if (address.family === 'IPv6') {
21
+ host = `[${host}]`
22
+ }
23
+
24
+ request({ url: `http://${host}:${address.port}${url}` }, cb)
25
+ }
26
+
27
+ test('setup', (t) => {
28
+ server = http.createServer((req, res) => {
29
+ if (!mount(req, res)) {
30
+ res.statusCode = 404
31
+ return res.end(`Not a match: ${req.url}`)
32
+ }
33
+ }).listen(0, '127.0.0.1', () => {
34
+ t.pass('listening')
35
+ address = server.address()
36
+ t.end()
37
+ })
38
+ })
39
+
40
+ tearDown(() => {
41
+ server.close()
42
+ })
43
+
44
+ module.exports.req = req
@@ -0,0 +1,19 @@
1
+ global.dot = false
2
+
3
+ const { test } = require('tap')
4
+ const { req } = require('./dot-common')
5
+
6
+ // failing per https://github.com/isaacs/st/issues/67
7
+ test('non-dotted file', (t) => {
8
+ req('/index.html', (er, res, body) => {
9
+ t.equal(res.statusCode, 200)
10
+ t.end()
11
+ })
12
+ })
13
+
14
+ test('dotted file', (t) => {
15
+ req('/.index.html', (er, res, body) => {
16
+ t.equal(res.statusCode, 403)
17
+ t.end()
18
+ })
19
+ })
@@ -0,0 +1,20 @@
1
+ global.dot = true
2
+
3
+ const { test } = require('tap')
4
+ const { req } = require('./dot-common')
5
+
6
+ test('non-dotted file', (t) => {
7
+ req('/index.html', (er, res, body) => {
8
+ t.error(er)
9
+ t.equal(res.statusCode, 200)
10
+ t.end()
11
+ })
12
+ })
13
+
14
+ test('dotted file', (t) => {
15
+ req('/.index.html', (er, res, body) => {
16
+ t.error(er)
17
+ t.equal(res.statusCode, 200)
18
+ t.end()
19
+ })
20
+ })
@@ -0,0 +1,12 @@
1
+ global.options = {
2
+ cache: {
3
+ content: {
4
+ // testing an arbitrary string can be passed through to override
5
+ // the header value, YES the misspellings are intentional
6
+ cacheControl: 'pubic, marx-aged=-100'
7
+ }
8
+ }
9
+ }
10
+
11
+ // otherwise just the same as basic.
12
+ require('./basic.js')
@@ -0,0 +1,10 @@
1
+ global.options = {
2
+ cache: {
3
+ fd: {
4
+ max: 2
5
+ }
6
+ }
7
+ }
8
+
9
+ // otherwise just the same as basic.
10
+ require('./basic.js')
@@ -0,0 +1,11 @@
1
+ <html>
2
+ <head><title>Index of {url}</title></head>
3
+ <body bgcolor="white">
4
+ <h1>Index of {url}</h1><hr><pre><a href="../">../</a>
5
+ <!-- dirs first -->
6
+ <!-- for each show this:
7
+ <a href="{filename}">{filename}</a> {modified date} {size}
8
+ -->
9
+ <!-- line up the fields nicely -->
10
+ </pre><hr></body>
11
+ </html>
@@ -0,0 +1,11 @@
1
+ <html>
2
+ <head><title>Index of {url}</title></head>
3
+ <body bgcolor="white">
4
+ <h1>Index of {url}</h1><hr><pre><a href="../">../</a>
5
+ <!-- dirs first -->
6
+ <!-- for each show this:
7
+ <a href="{filename}">{filename}</a> {modified date} {size}
8
+ -->
9
+ <!-- line up the fields nicely -->
10
+ </pre><hr></body>
11
+ </html>
@@ -0,0 +1,11 @@
1
+ <html>
2
+ <head><title>Index of {url}</title></head>
3
+ <body bgcolor="white">
4
+ <h1>Index of {url}</h1><hr><pre><a href="../">../</a>
5
+ <!-- dirs first -->
6
+ <!-- for each show this:
7
+ <a href="{filename}">{filename}</a> {modified date} {size}
8
+ -->
9
+ <!-- line up the fields nicely -->
10
+ </pre><hr></body>
11
+ </html>
@@ -0,0 +1 @@
1
+ space in filename
@@ -0,0 +1,38 @@
1
+ global.options = {
2
+ cachedHeader: true // inspect to see if something is served from cache
3
+ }
4
+
5
+ const zlib = require('zlib')
6
+ const { req, stExpect } = require('./common.js')
7
+ const { test } = require('tap')
8
+
9
+ test('does not gzip first response', (t) => {
10
+ req('/test/st.js', { 'accept-encoding': 'none' }, (er, res, body) => {
11
+ t.equal(res.statusCode, 200)
12
+ t.notOk(res.headers['content-encoding'])
13
+ t.notOk(res.headers['x-from-cache'])
14
+ t.equal(body.toString(), stExpect)
15
+ t.end()
16
+ })
17
+ })
18
+
19
+ test('gzips second response', (t) => {
20
+ req('/test/st.js', { 'accept-encoding': 'gzip' }, (er, res, body) => {
21
+ t.error(er, 'no error')
22
+
23
+ t.equal(res.statusCode, 200)
24
+ t.equal(res.headers['content-encoding'], 'gzip')
25
+ t.equal(res.headers['x-from-cache'], 'true')
26
+
27
+ t.ok(body, 'returned a body')
28
+ t.notEqual(body.toString(), stExpect, 'gzipped string')
29
+
30
+ zlib.gunzip(body, (er, body) => {
31
+ if (er) {
32
+ throw er
33
+ }
34
+ t.equal(body.toString(), stExpect)
35
+ t.end()
36
+ })
37
+ })
38
+ })
@@ -0,0 +1,17 @@
1
+ const st = require('../st.js')
2
+ const { test } = require('tap')
3
+ const path = require('path')
4
+
5
+ const opts = Object.assign({
6
+ autoindex: true,
7
+ path: path.dirname(__dirname),
8
+ url: '/test'
9
+ }, global.options || {})
10
+
11
+ const mount = st(opts)
12
+
13
+ test('call next() if asset not found', (t) => {
14
+ const req = { url: '/does-not-exist?a=b' }
15
+ t.plan(1)
16
+ mount(req, req, () => t.equal(req.url, '/does-not-exist?a=b'))
17
+ })