1 | const url = require('url')
|
---|
2 | const { Agent: httpAgent } = require('http')
|
---|
3 | const { Agent: httpsAgent } = require('https')
|
---|
4 | const httpProxy = require('http-proxy')
|
---|
5 | const _ = require('lodash')
|
---|
6 |
|
---|
7 | const log = require('../logger').create('proxy')
|
---|
8 |
|
---|
9 | function parseProxyConfig (proxies, config) {
|
---|
10 | proxies = proxies || []
|
---|
11 | return _.sortBy(_.map(proxies, function (proxyConfiguration, proxyPath) {
|
---|
12 | if (typeof proxyConfiguration === 'string') {
|
---|
13 | proxyConfiguration = { target: proxyConfiguration }
|
---|
14 | }
|
---|
15 | let proxyUrl = proxyConfiguration.target
|
---|
16 | // eslint-disable-next-line node/no-deprecated-api
|
---|
17 | const proxyDetails = url.parse(proxyUrl)
|
---|
18 | let pathname = proxyDetails.pathname
|
---|
19 |
|
---|
20 | if (proxyPath.endsWith('/') && !proxyUrl.endsWith('/')) {
|
---|
21 | log.warn(`proxy "${proxyUrl}" normalized to "${proxyUrl}/"`)
|
---|
22 | proxyUrl += '/'
|
---|
23 | pathname += '/'
|
---|
24 | }
|
---|
25 |
|
---|
26 | if (!proxyPath.endsWith('/') && proxyUrl.endsWith('/')) {
|
---|
27 | log.warn(`proxy "${proxyPath}" normalized to "${proxyPath}/"`)
|
---|
28 | proxyPath += '/'
|
---|
29 | }
|
---|
30 |
|
---|
31 | if (pathname === '/' && !proxyUrl.endsWith('/')) {
|
---|
32 | pathname = ''
|
---|
33 | }
|
---|
34 |
|
---|
35 | const hostname = proxyDetails.hostname || config.hostname
|
---|
36 | const protocol = proxyDetails.protocol || config.protocol
|
---|
37 | const defaultPorts = {
|
---|
38 | 'http:': '80',
|
---|
39 | 'https:': '443'
|
---|
40 | }
|
---|
41 | const port = proxyDetails.port || defaultPorts[proxyDetails.protocol] || config.port
|
---|
42 | const changeOrigin = proxyConfiguration.changeOrigin || false
|
---|
43 | const Agent = protocol === 'https:' ? httpsAgent : httpAgent
|
---|
44 | const agent = new Agent({ keepAlive: true })
|
---|
45 | const proxy = httpProxy.createProxyServer({
|
---|
46 | target: { host: hostname, port, protocol },
|
---|
47 | xfwd: true,
|
---|
48 | changeOrigin: changeOrigin,
|
---|
49 | secure: config.proxyValidateSSL,
|
---|
50 | agent
|
---|
51 | })
|
---|
52 |
|
---|
53 | ;['proxyReq', 'proxyRes'].forEach(function (name) {
|
---|
54 | const callback = proxyDetails[name] || config[name]
|
---|
55 | if (callback) {
|
---|
56 | proxy.on(name, callback)
|
---|
57 | }
|
---|
58 | })
|
---|
59 |
|
---|
60 | proxy.on('error', function proxyError (err, req, res) {
|
---|
61 | if (err.code === 'ECONNRESET' && req.socket.destroyed) {
|
---|
62 | log.debug(`failed to proxy ${req.url} (browser hung up the socket)`)
|
---|
63 | } else {
|
---|
64 | log.warn(`failed to proxy ${req.url} (${err.message})`)
|
---|
65 | }
|
---|
66 |
|
---|
67 | res.destroy()
|
---|
68 | })
|
---|
69 |
|
---|
70 | return { path: proxyPath, baseUrl: pathname, host: hostname, port, proxy, agent }
|
---|
71 | }), 'path').reverse()
|
---|
72 | }
|
---|
73 |
|
---|
74 | /**
|
---|
75 | * Returns a handler which understands the proxies and its redirects, along with the proxy to use
|
---|
76 | * @param proxies An array of proxy record objects
|
---|
77 | * @param urlRoot The URL root that karma is mounted on
|
---|
78 | * @return {Function} handler function
|
---|
79 | */
|
---|
80 | function createProxyHandler (proxies, urlRoot) {
|
---|
81 | if (!proxies.length) {
|
---|
82 | const nullProxy = (request, response, next) => next()
|
---|
83 | nullProxy.upgrade = () => {}
|
---|
84 | return nullProxy
|
---|
85 | }
|
---|
86 |
|
---|
87 | function createProxy (request, response, next) {
|
---|
88 | const proxyRecord = proxies.find((p) => request.url.startsWith(p.path))
|
---|
89 | if (proxyRecord) {
|
---|
90 | log.debug(`proxying request - ${request.url} to ${proxyRecord.host}:${proxyRecord.port}`)
|
---|
91 | request.url = request.url.replace(proxyRecord.path, proxyRecord.baseUrl)
|
---|
92 | proxyRecord.proxy.web(request, response)
|
---|
93 | } else {
|
---|
94 | return next()
|
---|
95 | }
|
---|
96 | }
|
---|
97 |
|
---|
98 | createProxy.upgrade = function (request, socket, head) {
|
---|
99 | // special-case karma's route to avoid upgrading it
|
---|
100 | if (request.url.startsWith(urlRoot)) {
|
---|
101 | log.debug(`NOT upgrading proxyWebSocketRequest ${request.url}`)
|
---|
102 | return
|
---|
103 | }
|
---|
104 |
|
---|
105 | const proxyRecord = proxies.find((p) => request.url.startsWith(p.path))
|
---|
106 | if (proxyRecord) {
|
---|
107 | log.debug(`upgrade proxyWebSocketRequest ${request.url} to ${proxyRecord.host}:${proxyRecord.port}`)
|
---|
108 | request.url = request.url.replace(proxyRecord.path, proxyRecord.baseUrl)
|
---|
109 | proxyRecord.proxy.ws(request, socket, head)
|
---|
110 | }
|
---|
111 | }
|
---|
112 |
|
---|
113 | return createProxy
|
---|
114 | }
|
---|
115 |
|
---|
116 | exports.create = function (/* config */config, /* config.proxies */proxies, /* emitter */emitter) {
|
---|
117 | const proxyRecords = parseProxyConfig(proxies, config)
|
---|
118 | emitter.on('exit', (done) => {
|
---|
119 | log.debug('Destroying proxy agents')
|
---|
120 | proxyRecords.forEach((proxyRecord) => {
|
---|
121 | proxyRecord.agent.destroy()
|
---|
122 | })
|
---|
123 | done()
|
---|
124 | })
|
---|
125 | return createProxyHandler(proxyRecords, config.urlRoot)
|
---|
126 | }
|
---|