-
Notifications
You must be signed in to change notification settings - Fork 505
/
Copy pathremote-api.test.js
266 lines (230 loc) · 9.5 KB
/
remote-api.test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/* global ipfs, webuiUrl, page, describe, it, expect, beforeAll, waitForText */
const { createController } = require('ipfsd-ctl')
const getPort = require('get-port')
const http = require('http')
const httpProxy = require('http-proxy')
const basicAuth = require('basic-auth')
const toUri = require('multiaddr-to-uri')
// ipfs is a global 'local' node used for other tests,
// we reuse it for testing the most basic setup with no auth
const localIpfs = ipfs
// Basic Auth Proxy Setup
// -----------------------------------
// Why do we support and test Basic Auth?
// Some users choose to access remote API.
// It requires setting up reverse proxy with correct CORS and Basic Auth headers,
// but when done properly, should work. This test sets up a proxy which
// acts as properly protected and configured remote API to ensure there are no
// regressions for this difficult to test use case.
let ipfsd
let proxyd
let user
let password
let proxyPort
beforeAll(async () => {
await page.goto(webuiUrl)
// spawn an ephemeral local node to ensure we connect to a different, remote node
ipfsd = await createController({
type: 'go',
ipfsBin: require('go-ipfs').path(),
ipfsHttpModule: require('ipfs-http-client'),
test: true,
disposable: true
})
// set up proxy in front of remote API to provide CORS and Basic Auth
user = 'user'
password = 'pass'
const proxy = httpProxy.createProxyServer()
const remoteApiUrl = toUri(ipfsd.apiAddr.toString(), { assumeHttp: true })
proxy.on('proxyReq', (proxyReq, req, res, options) => {
// swap Origin before passing to the real API
// This way internal check of go-ipfs does get triggered (403 Forbidden on Origin mismatch)
proxyReq.setHeader('Origin', remoteApiUrl)
proxyReq.setHeader('Referer', remoteApiUrl)
proxyReq.setHeader('Host', new URL(remoteApiUrl).host)
})
proxy.on('error', function (err, req, res) {
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end(`proxyd error: ${JSON.stringify(err)}`)
})
proxyd = http.createServer((req, res) => {
// console.log(`${req.method}\t\t${req.url}`)
res.oldWriteHead = res.writeHead
res.writeHead = function (statusCode, headers) {
// hardcoded liberal CORS for easier testing
res.setHeader('Access-Control-Allow-Origin', '*')
// usual suspects + 'authorization' header
res.setHeader('Access-Control-Allow-Headers', 'X-Stream-Output, X-Chunked-Output, X-Content-Length, authorization')
res.oldWriteHead(statusCode)
}
const auth = basicAuth(req)
const preflight = req.method === 'OPTIONS' // need preflight working
if (!preflight && !(auth && auth.name === user && auth.pass === password)) {
res.writeHead(401, {
'WWW-Authenticate': 'Basic realm="IPFS API"'
})
res.end('Access denied')
} else {
proxy.web(req, res, { target: remoteApiUrl })
}
})
})
beforeEach(async () => {
// Swap API port for each test, ensure we don't get false-positives
proxyPort = await getPort()
await proxyd.listen(proxyPort)
})
afterEach(async () => {
if (proxyd.listening) await proxyd.close()
})
afterAll(async () => {
if (proxyd.listening) await proxyd.close()
await ipfsd.stop()
})
const switchIpfsApiEndpointViaLocalStorage = async (endpoint) => {
if (endpoint) {
await page.evaluate((a) => localStorage.setItem('ipfsApi', a), endpoint)
} else {
await page.evaluate(() => localStorage.removeItem('ipfsApi'))
}
await waitForIpfsApiEndpoint(endpoint)
}
const switchIpfsApiEndpointViaSettings = async (endpoint) => {
await page.click('a[href="#/settings"]')
const selector = 'input[id="api-address"]'
await expect(page).toHaveSelector(selector)
await page.fill(selector, endpoint)
await page.press(selector, 'Enter')
await waitForIpfsApiEndpoint(endpoint)
}
const waitForIpfsApiEndpoint = async (endpoint) => {
if (endpoint) {
try {
// unwrap port if JSON config is passed
const json = JSON.parse(endpoint)
const uri = new URL(json.url)
endpoint = uri.port || endpoint
} catch (_) {}
try {
// unwrap port if inlined basic auth was passed
// (inlined URL is converted to JSON, so we cant do direct match)
const uri = new URL(endpoint)
if (uri.password) {
endpoint = uri.port || endpoint
}
} catch (_) {}
// await page.waitForFunction(`localStorage.getItem('ipfsApi') && localStorage.getItem('ipfsApi').includes('${endpoint}')`)
await page.waitForFunction(endpoint => window.localStorage.getItem('ipfsApi') && window.localStorage.getItem('ipfsApi').includes(endpoint), endpoint)
return
}
await page.waitForFunction(() => window.localStorage.getItem('ipfsApi') === null)
}
const basicAuthConnectionConfirmation = async (user, password, proxyPort) => {
// (1) confirm API section on Status page includes expected PeerID and API description
// account for JSON config, which we hide from status page
await expectHttpApiAddressOnStatusPage('Custom JSON configuration')
// confirm webui is actually connected to expected node :^)
await expectPeerIdOnStatusPage(ipfsd.api)
// (2) go to Settings and confirm API string includes expected JSON config
const apiOptions = JSON.stringify({
url: `http://127.0.0.1:${proxyPort}/`,
headers: {
authorization: `Basic ${nodeBtoa(user + ':' + password)}`
}
})
await expectHttpApiAddressOnSettingsPage(apiOptions)
}
const expectPeerIdOnStatusPage = async (api) => {
const { id } = await api.id()
await waitForText(id)
}
const expectHttpApiAddressOnStatusPage = async (value) => {
await page.waitForSelector('a[href="#/"]')
await page.click('a[href="#/"]')
await page.reload() // instant addr update for faster CI
await page.waitForSelector('summary', { state: 'visible' })
await page.click('summary')
await page.waitForSelector('div[id="http-api-address"]', { state: 'visible' })
await waitForText(String(value))
}
const expectHttpApiAddressOnSettingsPage = async (value) => {
await expect(page).toHaveSelector('a[href="#/settings"]')
await page.click('a[href="#/settings"]')
await page.waitForSelector('input[id="api-address"]', { state: 'visible' })
const apiAddrInput = await page.$('#api-address')
const apiAddrValue = await page.evaluate(x => x.value, apiAddrInput)
// if API address is defined as JSON, match objects
try {
const json = JSON.parse(apiAddrValue)
const expectedJson = JSON.parse(value)
await expect(json).toMatchObject(expectedJson)
return
} catch (_) {}
// else, match strings (Multiaddr or URL)
await expect(apiAddrValue).toMatch(String(value))
}
const nodeBtoa = (b) => Buffer.from(b).toString('base64')
// ----------------------------------------------------------------------------
// having that out of the way, tests begin below ;^)
// ----------------------------------------------------------------------------
describe('API @ multiaddr', () => {
const localApiMultiaddr = `/ip4/${localIpfs.apiHost}/tcp/${localIpfs.apiPort}`
it('should be possible to set via Settings page', async () => {
await page.goto(webuiUrl + '#/settings')
await switchIpfsApiEndpointViaSettings(localApiMultiaddr)
})
it('should show full multiaddr on Status page', async () => {
await expectHttpApiAddressOnStatusPage(localApiMultiaddr)
await expectPeerIdOnStatusPage(localIpfs)
})
it('should show full multiaddr on Settings page', async () => {
await expectHttpApiAddressOnSettingsPage(localApiMultiaddr)
})
})
describe('API @ URL', () => {
const localApiUrl = new URL(`http://${localIpfs.apiHost}:${localIpfs.apiPort}`).toString()
it('should be possible to set via Settings page', async () => {
await switchIpfsApiEndpointViaSettings(localApiUrl)
})
it('should show full multiaddr on Status page', async () => {
await expectHttpApiAddressOnStatusPage(localApiUrl)
await expectPeerIdOnStatusPage(localIpfs)
})
it('should show full multiaddr on Settings page', async () => {
await expectHttpApiAddressOnSettingsPage(localApiUrl)
})
})
describe('API with CORS and Basic Auth', () => {
afterEach(async () => {
await switchIpfsApiEndpointViaLocalStorage(null)
})
it('should work when localStorage[ipfsApi] is set to URL with inlined Basic Auth credentials', async () => {
await switchIpfsApiEndpointViaLocalStorage(`http://${user}:${password}@127.0.0.1:${proxyPort}/`)
await basicAuthConnectionConfirmation(user, password, proxyPort)
})
it('should work when localStorage[ipfsApi] is set to a JSON string with a custom ipfs-http-client config', async () => {
const apiOptions = JSON.stringify({
url: `http://127.0.0.1:${proxyPort}/`,
headers: {
authorization: `Basic ${nodeBtoa(user + ':' + password)}`
}
})
await switchIpfsApiEndpointViaLocalStorage(apiOptions)
await basicAuthConnectionConfirmation(user, password, proxyPort)
})
it('should work when URL with inlined credentials are entered at the Settings page', async () => {
const basicAuthApiAddr = `http://${user}:${password}@127.0.0.1:${proxyPort}/`
await switchIpfsApiEndpointViaSettings(basicAuthApiAddr)
await basicAuthConnectionConfirmation(user, password, proxyPort)
})
it('should work when JSON with ipfs-http-client config is entered at the Settings page', async () => {
const apiOptions = JSON.stringify({
url: `http://127.0.0.1:${proxyPort}/`,
headers: {
authorization: `Basic ${nodeBtoa(user + ':' + password)}`
}
})
await switchIpfsApiEndpointViaSettings(apiOptions)
await basicAuthConnectionConfirmation(user, password, proxyPort)
})
})