Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tunnel fix #143

Merged
merged 9 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}

- run: |
curl https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 --output cloudflared-linux-amd64
chmod +x cloudflared-linux-amd64

- run: yarn
- run: npm run lint

- name: Download cloudflared
run: |
curl https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -L --output cloudflared-linux-amd64
chmod +x cloudflared-linux-amd64

# can be used for debugging:
# - name: Setup tmate session
# uses: mxschmitt/action-tmate@v3

- run: npm run test-all
env:
TRANSLOADIT_KEY: ${{ secrets.TRANSLOADIT_KEY }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jobs:
node: ['10', '12', '14']

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3

- uses: actions/setup-node@v1
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@
"lint": "eslint .",
"next:update": "next-update --keep true --tldr",
"test-unit": "jest --coverage ./test/unit",
"test-integration": "jest ./test/integration",
"test-integration": "jest --runInBand ./test/integration",
"tsd": "tsd",
"test-all": "npm run tsd && jest --coverage --coverageReporters json lcov text clover json-summary --forceExit",
"test-all": "npm run tsd && jest --runInBand --coverage --coverageReporters json lcov text clover json-summary --forceExit",
"test": "npm run tsd && npm run test-unit"
},
"license": "MIT",
Expand Down
99 changes: 3 additions & 96 deletions test/integration/__tests__/live-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* @jest-environment node
*/
// https://github.com/axios/axios/issues/2654
const http = require('http')
const keyBy = require('lodash/keyBy')
const querystring = require('querystring')
const temp = require('temp')
Expand All @@ -19,7 +18,7 @@ const pipeline = promisify(streamPipeline)

const Transloadit = require('../../../src/Transloadit')

const createTunnel = require('../../tunnel')
const { startTestServer } = require('../../testserver')

async function downloadTmpFile (url) {
const { path } = await temp.open('transloadit')
Expand Down Expand Up @@ -69,98 +68,6 @@ function createAssembly (client, params) {
return promise
}

const startServerAsync = async (handler2) => {
let customHandler

function handler (...args) {
if (customHandler) return customHandler(...args)
return handler2(...args)
}

const server = http.createServer(handler)

// Find a port to use
let port = 8000
await new Promise((resolve, reject) => {
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
if (++port >= 65535) {
server.close()
return reject(new Error('Failed to bind to port'))
}
return server.listen(port, '127.0.0.1')
}
return reject(err)
})

server.listen(port, '127.0.0.1')

server.on('listening', resolve)
})

let tunnel
try {
tunnel = createTunnel({ cloudFlaredPath: process.env.CLOUDFLARED_PATH, port })

// eslint-disable-next-line no-console
tunnel.process.on('error', console.error)
tunnel.process.on('close', () => {
// console.log('tunnel closed')
server.close()
})

const url = await tunnel.urlPromise
// console.log('tunnel created', url)

try {
let requestPromise
await new Promise((resolve, reject) => {
let curPath
let done = false

customHandler = (req, res) => {
// console.log('handler', req.url)

if (req.url !== curPath) throw new Error(`Unexpected path ${req.url}`)

done = true
res.end()
resolve()
}

;(async () => {
for (let i = 0; i < 10; i += 1) {
if (done) return
curPath = `/check${i}`
try {
await got(`${url}${curPath}`, { timeout: { request: 2000 } })
} catch (err) {
// console.error(err)
// eslint-disable-next-line no-shadow
await new Promise((resolve) => setTimeout(resolve, 3000))
}
}
reject(new Error('Timed out checking for a functioning tunnel'))
})()
})
await requestPromise
} finally {
customHandler = undefined
}

// console.log('Tunnel ready')

return {
url,
close: () => tunnel.close(),
}
} catch (err) {
if (tunnel) tunnel.close()
server.close()
throw err
}
}

// https://transloadit.com/demos/importing-files/import-a-file-over-http
const genericImg = 'https://demos.transloadit.com/66/01604e7d0248109df8c7cc0f8daef8/snowflake.jpg'
const sampleSvg = '<?xml version="1.0" standalone="no"?><svg height="100" width="100"><circle cx="50" cy="50" r="40" fill="red" /></svg>'
Expand Down Expand Up @@ -488,7 +395,7 @@ describe('API integration', () => {
got.stream(genericImg).pipe(res)
}

const server = await startServerAsync(handleRequest)
const server = await startTestServer(handleRequest)

try {
const params = {
Expand Down Expand Up @@ -640,7 +547,7 @@ describe('API integration', () => {
}

try {
server = await startServerAsync(onNotificationRequest)
server = await startTestServer(onNotificationRequest)
await createAssembly(client, { params: { ...genericParams, notify_url: server.url } })
} catch (err) {
onError(err)
Expand Down
103 changes: 103 additions & 0 deletions test/testserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const http = require('http')
const got = require('got')

const createTunnel = require('./tunnel')

async function startTestServer (handler2) {
let customHandler

function handler (...args) {
if (customHandler) return customHandler(...args)
return handler2(...args)
}

const server = http.createServer(handler)

// Find a free port to use
let port = 8000
await new Promise((resolve, reject) => {
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
if (++port >= 65535) {
server.close()
return reject(new Error('Failed to bind to port'))
}
return server.listen(port, '127.0.0.1')
}
return reject(err)
})

server.listen(port, '127.0.0.1')

server.on('listening', resolve)
})

let tunnel
try {
tunnel = createTunnel({ cloudFlaredPath: process.env.CLOUDFLARED_PATH, port })

// eslint-disable-next-line no-console
tunnel.process.on('error', console.error)
tunnel.process.on('close', () => {
// console.log('tunnel closed')
server.close()
})

// console.log('waiting for tunnel to be created')
const url = await tunnel.urlPromise
// console.log('tunnel created', url)

try {
let curPath
let done = false

const promise1 = new Promise((resolve) => {
customHandler = (req, res) => {
// console.log('handler', req.url)

if (req.url !== curPath) throw new Error(`Unexpected path ${req.url}`)

done = true
res.end()
resolve()
}
})

const promise2 = (async () => {
// try connecting to the tunnel and resolve when connection successfully passed through
for (let i = 0; i < 10; i += 1) {
if (done) return
curPath = `/check${i}`
try {
await got(`${url}${curPath}`, { timeout: { request: 2000 } })
return
} catch (err) {
// console.error(err.message)
// eslint-disable-next-line no-shadow
await new Promise((resolve) => setTimeout(resolve, 3000))
}
}
throw new Error('Timed out checking for a functioning tunnel')
})()

await Promise.all([promise1, promise2])
} finally {
customHandler = undefined
}

// console.log('Tunnel ready')

return {
url,
close: () => tunnel.close(),
}
} catch (err) {
if (tunnel) tunnel.close()
server.close()
throw err
}
}

module.exports = {
startTestServer,
}
14 changes: 8 additions & 6 deletions test/tunnel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ const { Resolver } = require('dns')
const { promisify } = require('util')

module.exports = ({ cloudFlaredPath = 'cloudflared', port }) => {
// console.log('starting tunnel', port)
const process = execa(cloudFlaredPath, ['tunnel', '--url', `http://localhost:${port}`, '--no-autoupdate'], { buffer: false, stdout: 'ignore' })
const rl = readline.createInterface({ input: process.stderr })

process.on('error', (err) => console.error(err))

const urlPromise = (async () => {
const url = await new Promise((resolve) => {
let foundUrl
rl.on('line', (line) => {
// console.log('line', line)
if (!foundUrl) {
const match = line.match(/(https:\/\/[^.]+\.trycloudflare\.com)/)
if (!match) return
Expand All @@ -23,24 +23,26 @@ module.exports = ({ cloudFlaredPath = 'cloudflared', port }) => {
}
})
})
// console.log('Found url')

// We need to wait for DNS to be resolvable.
// If we don't, the operating system dns cache will be poisoned by the not yet valid dns
// and forever fail for that subdomain name
// If we don't, the operating system's dns cache will be poisoned by the not yet valid resolved entry
// and it will forever fail for that subdomain name...
const resolver = new Resolver()
resolver.setServers(['8.8.8.8']) // if we don't explicitly specify DNS server, it will also poison the OS dns cache
const resolve4 = promisify(resolver.resolve4.bind(resolver))
for (let i = 0; i < 10; i += 1) {
try {
const host = new URL(url).hostname
// console.log('checking dns', host)
await resolve4(host)
return url
} catch (err) {
// console.error(err)
// console.error(err.message)
await new Promise((resolve) => setTimeout(resolve, 3000))
}
}
throw new Error('Timed out trying to resolve tunnel host')
throw new Error('Timed out trying to resolve tunnel dns')
})()

function close () {
Expand Down