diff --git a/lib/index.js b/lib/index.js index 4f56c17b0a..4746d6cdcb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,7 @@ const config = require('./config'); const Koa = require('koa'); const fs = require('fs'); const logger = require('./utils/logger'); +require('./utils/request-wrapper'); const onerror = require('./middleware/onerror'); const header = require('./middleware/header'); diff --git a/lib/utils/got.js b/lib/utils/got.js index bb184991dc..7f3ac1e3c0 100644 --- a/lib/utils/got.js +++ b/lib/utils/got.js @@ -1,52 +1,8 @@ const logger = require('./logger'); const config = require('@/config'); -const SocksProxyAgent = require('socks-proxy-agent'); -const tunnel = require('tunnel'); const got = require('got'); const queryString = require('query-string'); -let agent = null; -if (config.proxy && config.proxy.protocol && config.proxy.host && config.proxy.port) { - agent = {}; - const proxyUrl = `${config.proxy.protocol}://${config.proxy.host}:${config.proxy.port}`; - - switch (config.proxy.protocol) { - case 'socks': - agent.http = new SocksProxyAgent(proxyUrl); - agent.https = new SocksProxyAgent(proxyUrl); - break; - case 'http': - process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; - agent.http = tunnel.httpOverHttp({ - proxy: { - host: config.proxy.host, - port: parseInt(config.proxy.port), - }, - }); - agent.https = tunnel.httpsOverHttp({ - proxy: { - host: config.proxy.host, - port: parseInt(config.proxy.port), - }, - }); - break; - case 'https': - agent.http = tunnel.httpOverHttps({ - proxy: { - host: config.proxy.host, - port: parseInt(config.proxy.port), - }, - }); - agent.https = tunnel.httpsOverHttps({ - proxy: { - host: config.proxy.host, - port: parseInt(config.proxy.port), - }, - }); - break; - } -} - const custom = got.extend({ retry: { retries: config.requestRetry, @@ -85,20 +41,11 @@ const custom = got.extend({ options.query = options.query.replace(/([\u4e00-\u9fa5])/g, (str) => encodeURIComponent(str)); options.searchParams = options.query; // for Got v11 after } - if (agent && new RegExp(config.proxy.url_regex).test(options.href)) { - options.agent = agent[options.protocol.slice(0, -1)]; - - if (config.proxy.auth) { - options.headers['Proxy-Authorization'] = `Basic ${config.proxy.auth}`; - } - logger.info(`Proxy for ${options.href}`); - } }, ], }, headers: { 'user-agent': config.ua, - 'x-app': 'RSSHub', }, }); custom.all = (list) => Promise.all(list); diff --git a/lib/utils/request-wrapper.js b/lib/utils/request-wrapper.js new file mode 100644 index 0000000000..01d02a8ccf --- /dev/null +++ b/lib/utils/request-wrapper.js @@ -0,0 +1,99 @@ +const config = require('@/config'); +const SocksProxyAgent = require('socks-proxy-agent'); +const tunnel = require('tunnel'); +const logger = require('./logger'); +const http = require('http'); +const https = require('https'); + +let agent = null; +if (config.proxy && config.proxy.protocol && config.proxy.host && config.proxy.port) { + agent = {}; + const proxyUrl = `${config.proxy.protocol}://${config.proxy.host}:${config.proxy.port}`; + + switch (config.proxy.protocol) { + case 'socks': + agent.http = new SocksProxyAgent(proxyUrl); + agent.https = new SocksProxyAgent(proxyUrl); + break; + case 'http': + process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; + agent.http = tunnel.httpOverHttp({ + proxy: { + host: config.proxy.host, + port: parseInt(config.proxy.port), + }, + }); + agent.https = tunnel.httpsOverHttp({ + proxy: { + host: config.proxy.host, + port: parseInt(config.proxy.port), + }, + }); + break; + case 'https': + agent.http = tunnel.httpOverHttps({ + proxy: { + host: config.proxy.host, + port: parseInt(config.proxy.port), + }, + }); + agent.https = tunnel.httpsOverHttps({ + proxy: { + host: config.proxy.host, + port: parseInt(config.proxy.port), + }, + }); + break; + } +} + +const requestWrapper = (url, options) => { + if (agent && new RegExp(config.proxy.url_regex).test(url)) { + let agentResult; + try { + agentResult = agent[(options.protocol || url.match(/(https?:)/)[1]).slice(0, -1)]; + } catch (error) { + agentResult = null; + } + options.agent = agentResult; + + if (config.proxy.auth) { + if (!options.headers) { + options.headers = {}; + } + options.headers['Proxy-Authorization'] = `Basic ${config.proxy.auth}`; + } + logger.info(`Proxy for ${url}`); + } + let hasUA = false; + for (const header in options.headers) { + if (header.toLowerCase() === 'user-agent') { + hasUA = true; + } + } + if (!hasUA) { + if (!options.headers) { + options.headers = {}; + } + options.headers['user-agent'] = config.ua; + } + options.headers['x-app'] = 'RSSHub'; +}; + +const httpWrap = (func) => { + const origin = func; + return function(url, request) { + if (typeof url === 'object') { + const req = url; + requestWrapper(req.url || req.href || `${req.protocol}//${req.hostname}${req.path}`, req); + } else { + requestWrapper(url, request); + } + return origin.apply(this, arguments); + }; +}; + +http.get = httpWrap(http.get); +https.get = httpWrap(https.get); +http.request = httpWrap(http.request); +https.request = httpWrap(https.request); diff --git a/lib/utils/rss-parser.js b/lib/utils/rss-parser.js index 4768d56745..28c36e7faa 100644 --- a/lib/utils/rss-parser.js +++ b/lib/utils/rss-parser.js @@ -7,7 +7,6 @@ const parser = new Parser({ }, headers: { 'User-Agent': config.ua, - 'X-APP': 'RSSHub', }, }); diff --git a/test/utils/got.js b/test/utils/got.js index 317116cefc..1e1fd3a671 100644 --- a/test/utils/got.js +++ b/test/utils/got.js @@ -1,22 +1,13 @@ -let got = require('../../lib/utils/got'); +const got = require('../../lib/utils/got'); const config = require('../../lib/config'); const nock = require('nock'); -afterEach(() => { - delete process.env.PROXY_PROTOCOL; - delete process.env.PROXY_HOST; - delete process.env.PROXY_PORT; - delete process.env.PROXY_AUTH; - delete process.env.PROXY_URL_REGEX; -}); - describe('got', () => { it('headers', async () => { nock('http://rsshub.test') .get('/test') .reply(function() { expect(this.req.headers['user-agent']).toBe(config.ua); - expect(this.req.headers['x-app']).toBe('RSSHub'); return [200, '']; }); @@ -81,127 +72,4 @@ describe('got', () => { }, }); }); - - it('proxy socks', async () => { - process.env.PROXY_PROTOCOL = 'socks'; - process.env.PROXY_HOST = 'rsshub.proxy'; - process.env.PROXY_PORT = '2333'; - jest.resetModules(); - got = require('../../lib/utils/got'); - nock('http://rsshub.test') - .get('/proxy') - .reply(() => [200, '']); - - await got.get('http://rsshub.test/proxy', { - hooks: { - beforeRequest: [ - (options) => { - expect(options.agent.constructor.name).toBe('SocksProxyAgent'); - expect(options.agent.options.href).toBe('socks://rsshub.proxy:2333'); - }, - ], - }, - }); - }); - - it('proxy http', async () => { - process.env.PROXY_PROTOCOL = 'http'; - process.env.PROXY_HOST = 'rsshub.proxy'; - process.env.PROXY_PORT = '2333'; - jest.resetModules(); - got = require('../../lib/utils/got'); - nock('http://rsshub.test') - .get('/proxy') - .reply(() => [200, '']); - - await got.get('http://rsshub.test/proxy', { - hooks: { - beforeRequest: [ - (options) => { - expect(options.agent.constructor.name).toBe('TunnelingAgent'); - expect(options.agent.options.proxy.host).toBe('rsshub.proxy'); - expect(options.agent.options.proxy.port).toBe(2333); - }, - ], - }, - }); - }); - - it('proxy https', async () => { - process.env.PROXY_PROTOCOL = 'https'; - process.env.PROXY_HOST = 'rsshub.proxy'; - process.env.PROXY_PORT = '2333'; - jest.resetModules(); - got = require('../../lib/utils/got'); - nock('http://rsshub.test') - .get('/proxy') - .reply(() => [200, '']); - - await got.get('http://rsshub.test/proxy', { - hooks: { - beforeRequest: [ - (options) => { - expect(options.agent.constructor.name).toBe('TunnelingAgent'); - expect(options.agent.options.proxy.host).toBe('rsshub.proxy'); - expect(options.agent.options.proxy.port).toBe(2333); - }, - ], - }, - }); - }); - - it('auth', async () => { - process.env.PROXY_AUTH = 'testtest'; - process.env.PROXY_PROTOCOL = 'socks'; - process.env.PROXY_HOST = 'rsshub.proxy'; - process.env.PROXY_PORT = '2333'; - jest.resetModules(); - got = require('../../lib/utils/got'); - nock('http://rsshub.test') - .get('/auth') - .reply(function() { - expect(this.req.headers['user-agent']).toBe(config.ua); - expect(this.req.headers['proxy-authorization']).toBe('Basic testtest'); - return [200, '']; - }); - - await got.get('http://rsshub.test/auth'); - }); - - it('url_regex', async () => { - process.env.PROXY_URL_REGEX = 'url_regex'; - process.env.PROXY_PROTOCOL = 'socks'; - process.env.PROXY_HOST = 'rsshub.proxy'; - process.env.PROXY_PORT = '2333'; - jest.resetModules(); - got = require('../../lib/utils/got'); - - nock('http://rsshub.test') - .get('/url_regex') - .reply(() => [200, '']); - nock('http://rsshub.test') - .get('/proxy') - .reply(() => [200, '']); - - await got.get('http://rsshub.test/url_regex', { - hooks: { - beforeRequest: [ - (options) => { - expect(options.agent.constructor.name).toBe('SocksProxyAgent'); - expect(options.agent.options.href).toBe('socks://rsshub.proxy:2333'); - }, - ], - }, - }); - - await got.get('http://rsshub.test/proxy', { - hooks: { - beforeRequest: [ - (options) => { - expect(options.agent).toBe(undefined); - }, - ], - }, - }); - }); }); diff --git a/test/utils/request-wrapper.js b/test/utils/request-wrapper.js new file mode 100644 index 0000000000..1577f79930 --- /dev/null +++ b/test/utils/request-wrapper.js @@ -0,0 +1,167 @@ +const got = require('../../lib/utils/got'); +const parser = require('../../lib/utils/rss-parser'); +const nock = require('nock'); +require('../../lib/utils/request-wrapper'); +let check = () => {}; +const simpleResponse = ''; + +afterEach(() => { + delete process.env.PROXY_PROTOCOL; + delete process.env.PROXY_HOST; + delete process.env.PROXY_PORT; + delete process.env.PROXY_AUTH; + delete process.env.PROXY_URL_REGEX; + + nock.restore(); + nock.activate(); + check = () => {}; + + const http = require('http'); + const httpWrap = (func) => { + const origin = func; + return function(url, request) { + if (typeof url === 'object') { + check(url); + } else { + check(request); + } + return origin.apply(this, arguments); + }; + }; + http.get = httpWrap(http.get); + http.request = httpWrap(http.request); +}); + +describe('got', () => { + it('headers', async () => { + nock('http://rsshub.test') + .get('/test') + .times(2) + .reply(function() { + expect(this.req.headers['x-app']).toBe('RSSHub'); + return [200, simpleResponse]; + }); + + await got.get('http://rsshub.test/test'); + await parser.parseURL('http://rsshub.test/test'); + }); + + it('proxy socks', async () => { + process.env.PROXY_PROTOCOL = 'socks'; + process.env.PROXY_HOST = 'rsshub.proxy'; + process.env.PROXY_PORT = '2333'; + + jest.resetModules(); + require('../../lib/utils/request-wrapper'); + check = (request) => { + expect(request.agent.constructor.name).toBe('SocksProxyAgent'); + expect(request.agent.options.href).toBe('socks://rsshub.proxy:2333'); + }; + + nock('http://rsshub.test') + .get('/proxy') + .times(2) + .reply(200, simpleResponse); + + await got.get('http://rsshub.test/proxy'); + await parser.parseURL('http://rsshub.test/proxy'); + }); + + it('proxy http', async () => { + process.env.PROXY_PROTOCOL = 'http'; + process.env.PROXY_HOST = 'rsshub.proxy'; + process.env.PROXY_PORT = '2333'; + + jest.resetModules(); + require('../../lib/utils/request-wrapper'); + check = (request) => { + expect(request.agent.constructor.name).toBe('TunnelingAgent'); + expect(request.agent.options.proxy.host).toBe('rsshub.proxy'); + expect(request.agent.options.proxy.port).toBe(2333); + }; + + nock('http://rsshub.test') + .get('/proxy') + .times(2) + .reply(200, simpleResponse); + + await got.get('http://rsshub.test/proxy'); + await parser.parseURL('http://rsshub.test/proxy'); + }); + + it('proxy https', async () => { + process.env.PROXY_PROTOCOL = 'https'; + process.env.PROXY_HOST = 'rsshub.proxy'; + process.env.PROXY_PORT = '2333'; + + jest.resetModules(); + require('../../lib/utils/request-wrapper'); + check = (request) => { + expect(request.agent.constructor.name).toBe('TunnelingAgent'); + expect(request.agent.options.proxy.host).toBe('rsshub.proxy'); + expect(request.agent.options.proxy.port).toBe(2333); + }; + + nock('http://rsshub.test') + .get('/proxy') + .times(2) + .reply(200, simpleResponse); + + await got.get('http://rsshub.test/proxy'); + await parser.parseURL('http://rsshub.test/proxy'); + }); + + it('auth', async () => { + process.env.PROXY_AUTH = 'testtest'; + process.env.PROXY_PROTOCOL = 'socks'; + process.env.PROXY_HOST = 'rsshub.proxy'; + process.env.PROXY_PORT = '2333'; + + jest.resetModules(); + require('../../lib/utils/request-wrapper'); + + nock('http://rsshub.test') + .get('/auth') + .times(2) + .reply(function() { + expect(this.req.headers['proxy-authorization']).toBe('Basic testtest'); + return [200, simpleResponse]; + }); + + await got.get('http://rsshub.test/auth'); + await parser.parseURL('http://rsshub.test/auth'); + }); + + it('url_regex', async () => { + process.env.PROXY_URL_REGEX = 'url_regex'; + process.env.PROXY_PROTOCOL = 'socks'; + process.env.PROXY_HOST = 'rsshub.proxy'; + process.env.PROXY_PORT = '2333'; + + jest.resetModules(); + require('../../lib/utils/request-wrapper'); + check = (request) => { + if (request.path === '/url_regex') { + expect(request.agent.constructor.name).toBe('SocksProxyAgent'); + expect(request.agent.options.href).toBe('socks://rsshub.proxy:2333'); + } else if (request.path === '/proxy') { + expect(request.agent).toBe(undefined); + } + }; + + nock('http://rsshub.test') + .get('/url_regex') + .times(2) + .reply(() => [200, simpleResponse]); + nock('http://rsshub.test') + .get('/proxy') + .times(2) + .reply(() => [200, simpleResponse]); + + await got.get('http://rsshub.test/url_regex'); + await parser.parseURL('http://rsshub.test/url_regex'); + + await got.get('http://rsshub.test/proxy'); + await parser.parseURL('http://rsshub.test/proxy'); + }); +}); diff --git a/test/utils/rss-parser.js b/test/utils/rss-parser.js new file mode 100644 index 0000000000..b72763c5d1 --- /dev/null +++ b/test/utils/rss-parser.js @@ -0,0 +1,16 @@ +const parser = require('../../lib/utils/rss-parser'); +const config = require('../../lib/config'); +const nock = require('nock'); + +describe('got', () => { + it('headers', async () => { + nock('http://rsshub.test') + .get('/test') + .reply(function() { + expect(this.req.headers['user-agent']).toBe(config.ua); + return [200, '']; + }); + + await parser.parseURL('http://rsshub.test/test'); + }); +});