Skip to content

Commit

Permalink
feat(cli): instance proxy server (#635)
Browse files Browse the repository at this point in the history
* feat(cli): instance proxy server

* docs: add proxy docs
  • Loading branch information
mediremi committed Sep 9, 2021
1 parent dfc729d commit 9df387e
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 4 deletions.
2 changes: 2 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@
"fs-extra": "^8.1.0",
"gaze": "^1.1.3",
"handlebars": "^4.3.3",
"http-proxy": "^1.18.1",
"i18next-conv": "^9",
"i18next-scanner": "^2.10.3",
"inquirer": "^7.3.3",
"lodash": "^4.17.11",
"node-http-proxy-json": "^0.1.9",
"parse-author": "^2.0.0",
"parse-gitignore": "^1.0.1",
"styled-jsx": "<3.3.3",
Expand Down
37 changes: 36 additions & 1 deletion cli/src/commands/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const i18n = require('../lib/i18n')
const loadEnvFiles = require('../lib/loadEnvFiles')
const parseConfig = require('../lib/parseConfig')
const makePaths = require('../lib/paths')
const createProxyServer = require('../lib/proxy')
const { compileServiceWorker } = require('../lib/pwa')
const makeShell = require('../lib/shell')
const { validatePackage } = require('../lib/validatePackage')
Expand All @@ -18,6 +19,8 @@ const handler = async ({
force,
port = process.env.PORT || defaultPort,
shell: shellSource,
proxy,
proxyPort,
}) => {
const paths = makePaths(cwd)

Expand All @@ -36,6 +39,29 @@ const handler = async ({
process.exit(1)
}

const newPort = await detectPort(port)

if (proxy) {
const newProxyPort = await detectPort(proxyPort)
const proxyBaseUrl = `http:https://localhost:${newProxyPort}`

reporter.print('')
reporter.info('Starting proxy server...')
reporter.print(
`The proxy for ${chalk.bold(
proxy
)} is now available on port ${newProxyPort}`
)
reporter.print('')

createProxyServer({
target: proxy,
baseUrl: proxyBaseUrl,
port: newProxyPort,
shellPort: newPort,
})
}

await exitOnCatch(
async () => {
if (!(await validatePackage({ config, paths, offerFix: false }))) {
Expand Down Expand Up @@ -77,7 +103,6 @@ const handler = async ({
reporter.info('Generating manifests...')
await generateManifests(paths, config, process.env.PUBLIC_URL)

const newPort = await detectPort(port)
if (String(newPort) !== String(port)) {
reporter.print('')
reporter.warn(
Expand Down Expand Up @@ -123,6 +148,16 @@ const command = {
type: 'number',
description: 'The port to use when running the development server',
},
proxy: {
alias: 'P',
type: 'string',
description: 'The remote DHIS2 instance the proxy should point to',
},
proxyPort: {
type: 'number',
description: 'The port to use when running the proxy',
default: 8080,
},
},
handler,
}
Expand Down
127 changes: 127 additions & 0 deletions cli/src/lib/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const url = require('url')
const { reporter } = require('@dhis2/cli-helpers-engine')
const httpProxy = require('http-proxy')
const _ = require('lodash')
const transformProxyResponse = require('node-http-proxy-json')

const stripCookieSecure = cookie => {
return cookie
.split(';')
.filter(v => v.trim().toLowerCase() !== 'secure')
.join('; ')
}

const rewriteLocation = ({ location, target, baseUrl }) => {
const parsedLocation = url.parse(location)
const parsedTarget = url.parse(target)
const parsedBaseUrl = url.parse(baseUrl)

if (
parsedLocation.host === parsedTarget.host &&
parsedLocation.pathname.startsWith(parsedTarget.pathname)
) {
return url.format({
...parsedBaseUrl,
pathname: parsedLocation.pathname.replace(
parsedTarget.pathname,
''
),
search: parsedLocation.search,
})
}
return location
}

const isUrl = string => {
try {
const { protocol } = new URL(string)
return protocol === 'http:' || protocol === 'https:'
} catch (error) {
return false
}
}

const transformJsonResponse = (res, { target, baseUrl }) => {
switch (typeof res) {
case 'string':
if (isUrl(res)) {
return rewriteLocation({ location: res, target, baseUrl })
}
return res
case 'object':
if (Array.isArray(res)) {
return res.map(r =>
transformJsonResponse(r, { target, baseUrl })
)
}
return _.transform(
res,
(result, value, key) => {
result[key] = transformJsonResponse(value, {
target,
baseUrl,
})
},
{}
)
default:
return res
}
}

exports = module.exports = ({ target, baseUrl, port, shellPort }) => {
const proxyServer = httpProxy.createProxyServer({
target,
changeOrigin: true,
secure: false,
protocolRewrite: 'http',
cookieDomainRewrite: '',
cookiePathRewrite: '/',
})

proxyServer.on('proxyRes', (proxyRes, req, res) => {
if (proxyRes.headers['access-control-allow-origin']) {
res.setHeader(
'access-control-allow-origin',
`http:https://localhost:${shellPort}`
)
}

if (proxyRes.headers.location) {
proxyRes.headers.location = rewriteLocation({
location: proxyRes.headers.location,
target,
baseUrl,
})
}

const sc = proxyRes.headers['set-cookie']
if (Array.isArray(sc)) {
proxyRes.headers['set-cookie'] = sc.map(stripCookieSecure)
}

if (
proxyRes.headers['content-type'] &&
proxyRes.headers['content-type'].includes('application/json')
) {
transformProxyResponse(res, proxyRes, body => {
if (body) {
return transformJsonResponse(body, {
target,
baseUrl,
})
}
return body
})
}
})

proxyServer.on('error', error => {
reporter.warn(error)
})

proxyServer.listen(port)
}

exports.rewriteLocation = rewriteLocation
exports.transformJsonResponse = transformJsonResponse
91 changes: 91 additions & 0 deletions cli/src/lib/proxy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const { rewriteLocation, transformJsonResponse } = require('./proxy')

describe('transformJsonResponse', () => {
it('rewrites URLs in responses if they match the proxy target', () => {
const transformedResponse = transformJsonResponse(
{
a: {
b: {
c: 'https://play.dhis2.org/dev/api/endpoint',
},
},
},
{
target: 'https://play.dhis2.org/dev',
baseUrl: 'http:https://localhost:8080',
}
)

expect(transformedResponse.a.b.c).toBe(
'http:https://localhost:8080/api/endpoint'
)
})
})

describe('rewriteLocation', () => {
it('rewrites locations if they match the proxy target', () => {
const baseUrl = 'http:https://localhost:8080'

expect(
rewriteLocation({
location: 'https://play.dhis2.org/dev/login.action',
target: 'https://play.dhis2.org/dev',
baseUrl,
})
).toBe(`${baseUrl}/login.action`)

expect(
rewriteLocation({
location: 'https://play.dhis2.org/dev/page?param=value',
target: 'https://play.dhis2.org/dev',
baseUrl,
})
).toBe(`${baseUrl}/page?param=value`)

expect(
rewriteLocation({
location: 'https://server.com:1234',
target: 'https://server.com:5678',
baseUrl,
})
).toBe('https://server.com:1234')

expect(
rewriteLocation({
location: 'https://server.com',
target: 'http:https://server.com',
baseUrl,
})
).toBe(baseUrl)

expect(
rewriteLocation({
location: 'https://play.dhis2.org/dev/api/dev',
target: 'https://play.dhis2.org/dev',
baseUrl,
})
).toBe(`${baseUrl}/api/dev`)
})

it('does not rewrite locations if they do not match the proxy target', () => {
;[
{
location: 'https://example.com/path',
target: 'https://play.dhis2.org/dev',
baseUrl: 'http:https://localhost:8080',
},
{
location: 'https://play.dhis2.org/2.35dev',
target: 'https://play.dhis2.org/dev',
baseUrl: 'http:https://localhost:8080',
},
{
location: 'http:https://server.com:1234',
target: 'http:https://server.com:5678',
baseUrl: 'http:https://localhost:8080',
},
].forEach(args => {
expect(rewriteLocation(args)).toBe(args.location)
})
})
})
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
- [**PWA**](pwa/pwa)
- [**Architecture**](architecture)
- [**Troubleshooting**](troubleshooting.md)
- [**Proxy**](proxy.md)
- &nbsp;
- [Changelog](CHANGELOG.md)
14 changes: 14 additions & 0 deletions docs/proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Proxy

When developing against a remote instance, some browsers may block cookies due
to the cross-site nature of requests.

As a workaround, the [`start`](scripts/start.md) command provides a `--proxy`
option. The value of this option is the remote instance that a local proxy will
route requests to.

## Usage

```
d2-app-scripts start --proxy <remote instance URL>
```
2 changes: 2 additions & 0 deletions docs/scripts/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ Global Options:
Options:
--cwd working directory to use (defaults to cwd)
--port, -p The port to use when running the development server [number]
--proxy, -P The remote DHIS2 instance the proxy should point to [string]
--proxyPort The port to use when running the proxy [number] [default: 8080]
```
7 changes: 5 additions & 2 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ A DHIS2 application needs to talk to a [DHIS2 server](https://www.dhis2.org/down

You should specify the fully-qualified base URL of a running DHIS2 server (including the `http:https://` or `https://` protocol) in the **Server** field of the login dialog.

Make sure that the the URL of your application (`localhost:3000` in this case) is included in the DHIS2 server's CORS whitelist (System Settings -> Access). Also ensure that the server
you specify doesn't have any domain-restricting proxy settings (such as the `SameSite=Lax`).
Make sure that the the URL of your application (`localhost:3000` in this case)
is included in the DHIS2 server's CORS whitelist (System Settings -> Access). If
your server or browser has domain-restricting settings (such as the
`SameSite=Lax`), the [`--proxy`](proxy.md) option of the
[`start`](scripts/start.md) command may be helpful.

For testing purposes, `https://play.dhis2.org/dev` will NOT work because it is configured for secure same-site access, while `https://debug.dhis2.org/dev` should work.

Expand Down
24 changes: 23 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4851,6 +4851,11 @@ buffer@^5.1.0, buffer@^5.5.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"

bufferhelper@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/bufferhelper/-/bufferhelper-0.2.1.tgz#fa74a385724a58e242f04ad6646c2366f83b913e"
integrity sha1-+nSjhXJKWOJC8ErWZGwjZvg7kT4=

builtin-modules@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484"
Expand Down Expand Up @@ -5541,7 +5546,7 @@ [email protected]:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=

concat-stream@^1.5.0:
concat-stream@^1.5.0, concat-stream@^1.5.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
Expand Down Expand Up @@ -8389,6 +8394,15 @@ http-proxy@^1.17.0:
follow-redirects "^1.0.0"
requires-port "^1.0.0"

http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
dependencies:
eventemitter3 "^4.0.0"
follow-redirects "^1.0.0"
requires-port "^1.0.0"

http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
Expand Down Expand Up @@ -11215,6 +11229,14 @@ node-gettext@^2.0.0:
dependencies:
lodash.get "^4.4.2"

node-http-proxy-json@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/node-http-proxy-json/-/node-http-proxy-json-0.1.9.tgz#5e744138c189ebd7e0105fe92d035a5486478cd4"
integrity sha512-WrKAR/y09BWaz5WqgbxuE6D/XsdhQFkLkSdnRk0a5uBKSINtApMV085MN7JMh+stiyBBltvgSR9SYVIZIpKKKQ==
dependencies:
bufferhelper "^0.2.1"
concat-stream "^1.5.1"

node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
Expand Down

0 comments on commit 9df387e

Please sign in to comment.