Skip to content
This repository was archived by the owner on Oct 20, 2022. It is now read-only.

Commit 9ee43cf

Browse files
committed
Parse toml files as well
1 parent 5e94b8a commit 9ee43cf

File tree

7 files changed

+345
-57
lines changed

7 files changed

+345
-57
lines changed

common.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
let URLclass = null
2+
3+
function parseURL(url) {
4+
if (typeof window !== 'undefined' && window.URL) {
5+
return new window.URL(url)
6+
}
7+
8+
URLclass = URLclass || require('url')
9+
return URLclass.parse(url)
10+
}
11+
12+
module.exports = {
13+
FULL_URL_MATCHER: /^(https?):\/\/(.+)$/,
14+
FORWARD_STATUS_MATCHER: /^2\d\d!?$/,
15+
16+
isInvalidSource: function(redirect) {
17+
return redirect.path.match(/^\/\.netlify/)
18+
},
19+
isProxy: function(redirect) {
20+
return redirect.proxy || (redirect.to.match(/^https?:\/\//) && redirect.status === 200)
21+
},
22+
parseFullOrigin: function(origin) {
23+
let url = null
24+
try {
25+
url = parseURL(origin)
26+
} catch (e) {
27+
return null
28+
}
29+
30+
return { host: url.host, scheme: url.protocol.replace(/:$/, ''), path: url.path }
31+
}
32+
}

line-parser.js

Lines changed: 7 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,8 @@
1-
const FULL_URL_MATCHER = /^(https?):\/\/(.+)$/
2-
const FORWARD_STATUS_MATCHER = /^2\d\d!?$/
3-
4-
let URLclass = null
5-
6-
function parseURL(url) {
7-
if (typeof window !== 'undefined' && window.URL) {
8-
return new window.URL(url)
9-
}
10-
11-
URLclass = URLclass || require('url')
12-
return URLclass.parse(url)
13-
}
14-
15-
class Result {
16-
constructor() {
17-
this.success = []
18-
this.errors = []
19-
}
20-
21-
addSuccess(redirect) {
22-
this.success.push(redirect)
23-
}
24-
25-
addError(idx, line, options) {
26-
const reason = options && options.reason
27-
this.errors.push({
28-
lineNum: idx + 1,
29-
line,
30-
reason
31-
})
32-
}
33-
}
34-
35-
function parseFullOrigin(origin) {
36-
let url = null
37-
try {
38-
url = parseURL(origin)
39-
} catch (e) {
40-
return null
41-
}
42-
43-
return { host: url.host, scheme: url.protocol.replace(/:$/, ''), path: url.path }
44-
}
1+
const Result = require('./result')
2+
const common = require('./common')
453

464
function splatForwardRule(redirect, nextPart) {
47-
return redirect.path.match(/\/\*$/) && nextPart.match(FORWARD_STATUS_MATCHER)
5+
return redirect.path.match(/\/\*$/) && nextPart.match(common.FORWARD_STATUS_MATCHER)
486
}
497

508
function arrayToObj(source) {
@@ -77,15 +35,15 @@ function redirectMatch(line) {
7735
}
7836

7937
const origin = parts.shift()
80-
const redirect = origin.match(FULL_URL_MATCHER) ? parseFullOrigin(origin) : { path: origin }
38+
const redirect = origin.match(common.FULL_URL_MATCHER) ? common.parseFullOrigin(origin) : { path: origin }
8139
if (redirect == null || !parts.length) {
8240
return null
8341
}
8442

8543
if (splatForwardRule(redirect, parts[0])) {
8644
redirect.to = redirect.path.replace(/\/\*$/, '/:splat')
8745
} else {
88-
const newHostRuleIdx = parts.findIndex(el => el.match(/^\//) || el.match(FULL_URL_MATCHER))
46+
const newHostRuleIdx = parts.findIndex(el => el.match(/^\//) || el.match(common.FULL_URL_MATCHER))
8947
if (newHostRuleIdx < 0) {
9048
return null
9149
}
@@ -125,14 +83,6 @@ function redirectMatch(line) {
12583
return redirect
12684
}
12785

128-
function isInvalidSource(redirect) {
129-
return redirect.path.match(/^\/\.netlify/)
130-
}
131-
132-
function isProxy(redirect) {
133-
return redirect.proxy || (redirect.to.match(/^https?:\/\//) && redirect.status === 200)
134-
}
135-
13686
function parse(text) {
13787
const result = new Result()
13888

@@ -148,12 +98,12 @@ function parse(text) {
14898
return
14999
}
150100

151-
if (isInvalidSource(redirect)) {
101+
if (common.isInvalidSource(redirect)) {
152102
result.addError(idx, line, { reason: 'Invalid /.netlify path in redirect source' })
153103
return
154104
}
155105

156-
if (isProxy(redirect)) {
106+
if (common.isProxy(redirect)) {
157107
redirect.proxy = true
158108
}
159109

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
],
1212
"author": "Netlify",
1313
"license": "MIT",
14+
"dependencies": {
15+
"@iarna/toml": "^2.2.3"
16+
},
1417
"devDependencies": {
1518
"ava": "^1.3.1"
1619
},

result.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class Result {
2+
constructor() {
3+
this.success = []
4+
this.errors = []
5+
}
6+
7+
addSuccess(redirect) {
8+
this.success.push(redirect)
9+
}
10+
11+
addError(idx, line, options) {
12+
const reason = options && options.reason
13+
this.errors.push({
14+
lineNum: idx + 1,
15+
line,
16+
reason
17+
})
18+
}
19+
}
20+
21+
module.exports = Result

toml-parse.test.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
const test = require('ava')
2+
const parser = require('./toml-parser')
3+
4+
test('simple redirects', t => {
5+
const source = `
6+
redirects = [
7+
{origin = "/home", destination = "/"},
8+
{origin = "/admin/*", status = 200, force = true},
9+
{origin = "/index", destination = "/", status = 302},
10+
{from = "/from", to = "/to", status = 302}
11+
]`
12+
13+
const result = parser.parse(source)
14+
t.deepEqual(
15+
[
16+
{ path: '/home', to: '/' },
17+
{ path: '/admin/*', to: '/admin/:splat', status: 200, force: true },
18+
{ path: '/index', to: '/', status: 302 },
19+
{ path: '/from', to: '/to', status: 302 }
20+
],
21+
result.success
22+
)
23+
})
24+
25+
test('redirects with parameter matches', t => {
26+
const source = `
27+
redirects = [
28+
{origin = "/", destination = "/news", parameters = {page = "news"}},
29+
{origin = "/blog", destination = "/blog/:post_id", parameters = {post = ":post_id"}},
30+
{origin = "/", destination = "/about", parameters = {_escaped_fragment_ = "/about"}, status = 301}
31+
]
32+
`
33+
34+
const result = parser.parse(source)
35+
t.deepEqual(
36+
[
37+
{ path: '/', to: '/news', params: { page: 'news' } },
38+
{ path: '/blog', to: '/blog/:post_id', params: { post: ':post_id' } },
39+
{ path: '/', to: '/about', params: { _escaped_fragment_: '/about' }, status: 301 }
40+
],
41+
result.success
42+
)
43+
})
44+
45+
test('redirects with full hostname', t => {
46+
const source = `
47+
redirects = [
48+
{origin = "http://hello.bitballoon.com/*", destination = "http://www.hello.com/:splat"}
49+
]
50+
`
51+
52+
const result = parser.parse(source)
53+
t.deepEqual(
54+
[{ host: 'hello.bitballoon.com', scheme: 'http', path: '/*', to: 'http://www.hello.com/:splat' }],
55+
result.success
56+
)
57+
})
58+
59+
test('proxy instruction', t => {
60+
const source = `
61+
redirects = [
62+
{origin = "/api/*", destination = "https://api.bitballoon.com/*", status = 200}
63+
]
64+
`
65+
66+
const result = parser.parse(source)
67+
t.deepEqual([{ path: '/api/*', to: 'https://api.bitballoon.com/*', status: 200, proxy: true }], result.success)
68+
})
69+
70+
test('headers on proxy rule', t => {
71+
const source = `
72+
redirects = [
73+
{origin = "/", destination = "https://api.bitballoon.com", status = 200, headers = {anything = "something"}}
74+
]
75+
`
76+
77+
const result = parser.parse(source)
78+
t.deepEqual(
79+
[{ path: '/', to: 'https://api.bitballoon.com', status: 200, headers: { anything: 'something' }, proxy: true }],
80+
result.success
81+
)
82+
})
83+
84+
test('redirect country conditions', t => {
85+
const source = `
86+
redirects = [
87+
{origin = "/", destination = "/china", status = 302, conditions = {Country = ["ch", "tw"]}},
88+
{origin = "/", destination = "/china", status = 302, conditions = {Country = ["il"], Language = ["en"]}}
89+
]
90+
`
91+
92+
const result = parser.parse(source)
93+
t.deepEqual(
94+
[
95+
{ path: '/', to: '/china', status: 302, conditions: { Country: ['ch', 'tw'] } },
96+
{ path: '/', to: '/china', status: 302, conditions: { Country: ['il'], Language: ['en'] } }
97+
],
98+
result.success
99+
)
100+
})
101+
102+
test('rules with no destination', t => {
103+
const source = `
104+
redirects = [
105+
{origin = "/swfobject.html?detectflash=false", status = 301}
106+
]
107+
`
108+
109+
const result = parser.parse(source)
110+
t.is(0, result.success.length)
111+
t.is(1, result.errors.length)
112+
})
113+
114+
test('redirect role conditions', t => {
115+
const source = `
116+
redirects = [
117+
{origin = "/admin/*", destination = "/admin/:splat", status = 200, conditions = {Role = ["admin"]}},
118+
{origin = "/admin/*", destination = "/admin/:splat", status = 200, conditions = {Role = ["admin", "member"]}},
119+
]
120+
`
121+
122+
const result = parser.parse(source)
123+
t.deepEqual(
124+
[
125+
{ path: '/admin/*', to: '/admin/:splat', status: 200, conditions: { Role: ['admin'] } },
126+
{ path: '/admin/*', to: '/admin/:splat', status: 200, conditions: { Role: ['admin', 'member'] } }
127+
],
128+
result.success
129+
)
130+
})
131+
132+
test('invalid headers on proxy rule', t => {
133+
const source = `
134+
redirects = [
135+
{origin = "/", destination = "https://api.bitballoon.com", status = 200, headers = [{anything = "something"}]}
136+
]
137+
`
138+
139+
const result = parser.parse(source)
140+
t.is(0, result.success.length)
141+
t.is(1, result.errors.length)
142+
})
143+
144+
test('missing origin in redirect rule', t => {
145+
const source = `
146+
redirects = [
147+
{destination = "/index.html", status = 200}
148+
]
149+
`
150+
151+
const result = parser.parse(source)
152+
t.is(0, result.success.length)
153+
t.is(1, result.errors.length)
154+
})
155+
156+
test('invalid netlify service source path', t => {
157+
const source = `
158+
redirects = [
159+
{origin = "/.netlify/lfs", destination = "https://pawned.com"},
160+
{origin = "https://example.com/.netlify/lfs", destination = "https://pawned.com"}
161+
]
162+
`
163+
164+
const result = parser.parse(source)
165+
t.is(0, result.success.length)
166+
t.is(2, result.errors.length)
167+
result.errors.forEach(err => {
168+
t.is('Invalid /.netlify path in redirect source', err.reason)
169+
})
170+
})
171+
172+
test('valid netlify service destination path', t => {
173+
const source = `
174+
redirects = [
175+
{origin = "/api/*", destination = "/.netlify/function/:splat"},
176+
]
177+
`
178+
179+
const result = parser.parse(source)
180+
t.is(0, result.errors.length)
181+
t.is(1, result.success.length)
182+
})

0 commit comments

Comments
 (0)