Add test cases for the proxy, and do some tiny fixes.

the fixes are:
1. add "content-type" in headers for when dealing with localresponse
2. make a more accurate tip for throttle rate when lower than 1
3. make the crtMgr funcionality a more independent one
4. uppercase the request header before sending it out

update the tip
This commit is contained in:
砚然
2016-08-15 17:48:47 +08:00
parent a925cbed55
commit e489e188f4
31 changed files with 1948 additions and 187 deletions

28
test/data/headers.js Normal file
View File

@@ -0,0 +1,28 @@
/*
* 用于放置所有header信息的测试数据
*
*/
// Get 和 Post共有的header信息
/*eslint max-len: ["off"]*/
const CommonRequestHeader = {
Accept: 'application/json;charset=utf-8,text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Charset': 'utf-8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN',
'Accept-Datetime': 'Thu, 31 May 2007 20:35:00 GMT',
'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Cookie': 'testCookie1=cookie1; testCookie2=cookie2',
'Content-Type': 'application/x-www-form-urlencoded',
'Date': 'Tue, 15 Nov 1994 08:12:31 GMT',
'Origin': 'http://localhost',
'Pragma': 'no-cache',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'
};
module.exports = {
CommonRequestHeader
};

3
test/data/test.css Normal file
View File

@@ -0,0 +1,3 @@
.test {
display: block;
}

BIN
test/data/test.eot Executable file

Binary file not shown.

3
test/data/test.js Normal file
View File

@@ -0,0 +1,3 @@
function test () {
console.info('This is nothing but a js file, to test the js download');
}

3
test/data/test.json Normal file
View File

@@ -0,0 +1,3 @@
{
'testkey': 'this is just a normal json file'
}

BIN
test/data/test.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

14
test/data/test.svg Executable file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2016 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="glass" unicode="&#xe800;" d="M948 746q0-19-24-43l-353-353v-429h179q15 0 25-10t11-25-11-25-25-11h-500q-14 0-25 11t-11 25 11 25 25 10h179v429l-353 353q-24 24-24 43 0 13 10 21t21 9 24 3h786q13 0 24-3t21-9 10-21z" horiz-adv-x="1000" />
<glyph glyph-name="music" unicode="&#xe801;" d="M857 725v-625q0-28-19-50t-48-33-58-18-53-6-54 6-58 18-48 33-19 50 19 50 48 33 58 18 54 6q58 0 107-22v300l-429-132v-396q0-28-19-50t-48-33-58-18-53-6-54 6-58 18-48 33-19 50 19 50 48 34 58 17 54 6q58 0 107-21v539q0 17 10 32t28 20l464 142q7 3 16 3 22 0 38-16t15-38z" horiz-adv-x="857.1" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
test/data/test.ttf Executable file

Binary file not shown.

BIN
test/data/test.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
test/data/test.woff Executable file

Binary file not shown.

BIN
test/data/test.woff2 Executable file

Binary file not shown.

459
test/no_rule_spec.js Normal file
View File

@@ -0,0 +1,459 @@
const http = require('http');
const querystring = require('querystring');
const path = require('path');
const fs = require('fs');
const Buffer = require('buffer').Buffer;
const Server = require('./server/server.js');
const
{
proxyGet,
proxyPost,
directGet,
directPost,
directUpload,
proxyUpload,
generateUrl,
proxyPut,
directPut,
proxyDelete,
directDelete,
directHead,
proxyHead,
directOptions,
proxyOptions,
proxyPutUpload,
directPutUpload
} = require('./util/HttpUtil.js');
const { CommonRequestHeader } = require('./data/headers.js');
const { isCommonResHeaderEqual, isCommonReqEqual, printLog } = require('./util/CommonUtil.js');
const color = require('colorful');
const streamEqual = require('stream-equal');
const WebSocket = require('ws');
const ProxyServerUtil = require('./util/ProxyServerUtil.js');
const wsHost = 'ws://localhost:3000/test/socket';
testRequest('http');
testRequest('https');
// Test suites for http and https request
function testRequest(protocol = 'http') {
function constructUrl(urlPath) {
return generateUrl(protocol, urlPath);
}
describe('Test request without proxy rules in protocol ' + protocol, () => {
let proxyServer ;
let serverInstance;
beforeAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000;
printLog('Start server for no_rule_spec');
serverInstance = new Server();
proxyServer = ProxyServerUtil.defaultProxyServer();
setTimeout(function() {
done();
}, 2000);
});
afterAll(() => {
serverInstance && serverInstance.close();
proxyServer && proxyServer.close();
printLog('Closed server for no_rule_spec');
});
it('Get should work as direct without proxy rules', (done) => {
const url = constructUrl('/test');
const getParam = {
param: 'nothing'
};
proxyGet(url, getParam, CommonRequestHeader).then((proxyRes) => {
directGet(url, getParam, CommonRequestHeader).then(directRes => {
expect(proxyRes.statusCode).toEqual(200);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
expect(proxyRes.statusCode).toEqual(directRes.statusCode);
expect(directRes.body).toEqual(proxyRes.body);
done();
}, error => {
console.error('error happend in direct get:', error);
done.fail('error happend in direct get');
});
}, error => {
console.log('error happened in proxy get:', error);
done.fail('error happend in proxy get');
});
});
it('Post should work as direct without proxy rules', (done) => {
const url = constructUrl('/test/getuser');
const param = {
param: 'postnothing'
};
proxyPost(url, param, CommonRequestHeader).then(proxyRes => {
directPost(url, param, CommonRequestHeader).then(directRes => {
expect(proxyRes.statusCode).toEqual(200);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(proxyRes.statusCode).toEqual(directRes.statusCode);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error in direct post:', error);
done.fail('error happend in direct post');
});
}, error => {
console.log('error happened in proxy post,', error);
done.fail('error happend in proxy post');
});
});
it('PUT should work as direct without proxy rules', done => {
const url = constructUrl('/test/put');
const param = {
param: 'putsomething'
};
proxyPut(url, param, CommonRequestHeader).then(proxyRes => {
directPut(url, param, CommonRequestHeader).then(directRes => {
expect(directRes.statusCode).toEqual(200);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(directRes.statusCode).toEqual(proxyRes.statusCode);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error happened in direct put', error);
done.fail('error happened in direct put');
});
}, error => {
console.error('error happened in proxy put', error);
done.fail('error happened in proxy put');
});
});
it('DELETE rquest should work as direct without proxy rules', (done) => {
const url = constructUrl('/test/delete/123456');
proxyDelete(url, {}, CommonRequestHeader).then(proxyRes => {
directDelete(url, {}, CommonRequestHeader).then(directRes => {
expect(directRes.statusCode).toEqual(200);
expect(directRes.statusCode).toEqual(proxyRes.statusCode);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error happened in direct delete :', error);
done.fail('error happened in direct delete');
});
}, error => {
console.error('error happened in proxy delete :', error);
done.fail('error happened in proxy delete');
});
});
it('HEAD request should work as direct without proxy rules', (done) => {
const url = constructUrl('/test/head');
proxyHead(url, CommonRequestHeader)
.then(proxyRes => {
directHead(url, CommonRequestHeader)
.then(directRes => {
expect(directRes.statusCode).toEqual(200);
expect(directRes.body).toEqual('');
expect(directRes.statusCode).toEqual(proxyRes.statusCode);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error happened in direct head request:', error);
done.fail('error happened in direct head request');
});
}, error => {
console.error('error happened in proxy head request:', error);
done.fail('error happened in proxy head request');
});
});
it('OPTIONS request should work as direct without proxy rules', (done) => {
const url = constructUrl('/test/options');
proxyOptions(url, CommonRequestHeader)
.then(proxyRes => {
directOptions(url, CommonRequestHeader)
.then(directRes => {
expect(directRes.statusCode).toEqual(200);
expect(directRes.body).toEqual('could_be_empty');
expect(directRes.statusCode).toEqual(proxyRes.statusCode);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error happened in direct options request:', error);
done.fail('error happened in direct options request');
});
}, error => {
console.error('error happened in proxy options request:', error);
done.fail('error happened in proxy options request');
});
});
describe('Response code should be honored as direct without proxy rules', () => {
[301, 302, 303].forEach(code => {
testRedirect(code);
});
function testRedirect (redirectCode) {
it(`${redirectCode} response should work as direct without proxy rules`, (done) => {
const url = constructUrl(`/test/response/${redirectCode}`);
proxyGet(url)
.then(proxyRes => {
directGet(url)
.then(directRes => {
const redirects = directRes.request._redirect.redirects || [];
const proxyRedirects = proxyRes.request._redirect.redirects || [];
expect(redirects.length).toEqual(1);
expect(proxyRedirects.length).toEqual(1);
expect(redirects[0].statusCode).toEqual(redirectCode);
expect(redirects[0].redirectUri).toEqual(proxyRedirects[0].redirectUri);
expect(redirects[0].statusCode).toEqual(proxyRedirects[0].statusCode);
if (protocol === 'https') {
expect(redirects[0].redirectUri).toEqual('https://localhost:3001/test');
} else {
expect(redirects[0].redirectUri).toEqual('http://localhost:3000/test');
}
done();
}, error => {
console.log(`error happened in direct ${redirectCode}:`, error);
done.fail(`error happened in direct ${redirectCode}`);
});
}, error => {
console.log(`error happened in proxy ${redirectCode}:`, error);
done.fail(`error happened in proxy ${redirectCode}`);
});
});
}
});
describe('Test file download ', () => {
const testArray = [
{
url: constructUrl('/test/download/png'),
type: 'png',
contentType: 'image/png'
},
{
url: constructUrl('/test/download/webp'),
type: 'WEBP',
contentType: 'image/webp'
},
{
url: constructUrl('/test/download/json'),
type: 'JSON',
contentType: 'application/json; charset=utf-8'
},
{
url: constructUrl('/test/download/css'),
type: 'CSS',
contentType: 'text/css; charset=utf-8'
},
{
url: constructUrl('/test/download/ttf'),
type: 'TTF',
contentType: 'application/x-font-ttf'
},
{
url: constructUrl('/test/download/eot'),
type: 'EOT',
contentType: 'application/vnd.ms-fontobject'
},
{
url: constructUrl('/test/download/svg'),
type: 'SVG',
contentType: 'image/svg+xml'
},
{
url: constructUrl('/test/download/woff'),
type: 'WOFF',
contentType: 'application/font-woff'
},
{
url: constructUrl('/test/download/woff2'),
type: 'WOFF2',
contentType: 'application/font-woff2'
}
];
testArray.forEach(item => {
testFileDownload(item.url, item.type, item.contentType);
});
// 封装测试文件下载的测试工具类
function testFileDownload (url, filetype, contentType) {
const describe = `${filetype} file download without rules should be work as direct download`;
const param = {};
it(describe, (done) => {
proxyGet(url, param).then(proxyRes => {
directGet(url, param).then(directRes => {
expect(proxyRes.statusCode).toEqual(200);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(proxyRes.statusCode).toEqual(directRes.statusCode);
expect(proxyRes.body).toEqual(directRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error in direct get :', filetype, error);
done.fail(`error happend in direct get ${filetype}`);
});
}, error => {
console.error('error in proxy get :', filetype, error);
done.fail(`error happend in proxy get ${filetype}`);
});
});
}
});
describe('Test file upload', () => {
const formParams = {
param1: 'param_1',
param2: 'param2'
};
it('POST upload should be working', (done) => {
const url = constructUrl('/test/upload/png');
const filePath = path.resolve('./test/data/test.png');
proxyUpload(url, filePath, formParams)
.then(proxyRes => {
directUpload(url, filePath, formParams)
.then((directRes) => {
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
assertReponse(proxyRes, directRes, filePath, done);
}, error => {
console.error('error in direct upload:', error);
done.fail('error in direct upload');
});
}, error => {
console.error('error in proxy upload:', error);
done.fail('error in proxy upload:');
});
});
it('PUT upload should be working', (done) => {
const url = constructUrl('/test/upload/putpng');
const filePath = path.resolve('./test/data/test.png');
proxyPutUpload(url, filePath, formParams)
.then(proxyRes => {
directPutUpload(url, filePath, formParams)
.then((directRes) => {
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
assertReponse(proxyRes, directRes, filePath, done);
}, error => {
console.error('error in direct upload:', error);
done.fail('error in direct upload');
});
}, error => {
console.error('error in proxy upload:', error);
done.fail('error in proxy upload:');
});
});
function assertReponse (proxyRes, directRes, originFilePath, done) {
expect(proxyRes.statusCode).toEqual(200);
expect(proxyRes.statusCode).toEqual(directRes.statusCode);
// expect(proxyRes.headers.reqbody).toEqual(directRes.headers.reqbody);
// the body will be the file path
const directUploadedStream = fs.createReadStream(directRes.body);
const proxyUploadedStream = fs.createReadStream(proxyRes.body);
const localFileStream = fs.createReadStream(originFilePath);
streamEqual(directUploadedStream, localFileStream)
.then(isLocalEqual => {
expect(isLocalEqual).toBe(true);
streamEqual(directUploadedStream, proxyUploadedStream)
.then(isUploadedEqual => {
expect(isUploadedEqual).toBe(true);
done();
}, error => {
console.error('error in comparing directUpload with proxy:\n',error);
done.fail('error in comparing directUpload with proxy');
});
done();
}, error => {
console.error('error in comparing directUpload with local:\n',error);
done.fail('error in comparing directUpload with local');
});
}
});
// describe('Test Big file download', () => {
// // const url = '/test/download/bigfile';
// const url = 'http://yunpan.alibaba-inc.com/downloadService.do?token=pZWiXMXUguIUQDvR098qnUVqVAWhNVY6';
// const contentType = 'application/octet-stream';
// const param = {};
// it('BIG file downlaod should be working', (done) => {
// directGet(url, param, CommonRequestHeader).then(proxyRes => {
// console.info('proxyRes body:', proxyRes.body);
// directGet(url, param, CommonRequestHeader).then(directRes => {
// expect(proxyRes.statusCode).toEqual(200);
// expect(proxyRes.headers['content-type']).toEqual(contentType);
// expect(proxyRes.statusCode).toEqual(directRes.statusCode);
// expect(proxyRes.headers['content-type']).toEqual(directRes.headers['content-type']);
// expect(proxyRes.body).toEqual(directRes.body);
// done();
// }, error => {
// console.error('error in direct get bigfile :', error);
// done.fail(`error happend in direct get bigfile`);
// });
// }, error => {
// console.error('error in proxy get bigfile :', error);
// done.fail(`error happend in proxy get bigfile`);
// });
// });
// });
});
}

View File

@@ -0,0 +1,144 @@
/*
* Test suites for WebSocket.
* ONLY TO ENSURE THE REQUEST WILL BE BYPASSED SUCCESSFULLY, WE HAVEN'T SUPPORTTED WEBSOCKET YET.
*
*/
const ProxyServerUtil = require('./util/ProxyServerUtil.js');
const { generateWsUrl, directWs, proxyWs } = require('./util/HttpUtil.js');
const Server = require('./server/server.js');
const { printLog } = require('./util/CommonUtil.js');
const wsHost = 'ws://localhost:3000/test/socket';
testWebsocket('ws');
testWebsocket('wss');
function testWebsocket(protocol) {
describe('Test WebSocket in protocol : ' + protocol , () =>{
const url = generateWsUrl(protocol, '/test/socket');
let serverInstance ;
let proxyServer ;
beforeAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000;
printLog('Start server for no_rule_websocket_spec');
serverInstance = new Server();
proxyServer = ProxyServerUtil.proxyServerWithoutHttpsIntercept();
setTimeout(function() {
done();
}, 2000);
});
afterAll(() => {
serverInstance && serverInstance.close();
proxyServer && proxyServer.close();
printLog('Closed server for no_rule_websocket_spec');
});
it('Default websocket option', done => {
const sendMessage = 'Send the message with default option';
let directMessage ; // set the flag for direct message, compare when both direct and proxy got message
let proxyMessage;
const ws = directWs(url);
const porxyWsRef = proxyWs(url);
ws.on('open', () => {
ws.send(sendMessage);
});
porxyWsRef.on('open', () => {
porxyWsRef.send(sendMessage);
});
ws.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
directMessage = message.content;
compareMessageIfReady();
}
});
porxyWsRef.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
proxyMessage = message.content;
compareMessageIfReady();
}
});
ws.on('error', error => {
console.error('error happened in direct websocket:', error);
done.fail('Error happened in direct websocket');
});
porxyWsRef.on('error', error => {
console.error('error happened in proxy websocket:', error);
done.fail('Error happened in proxy websocket');
});
function compareMessageIfReady () {
if (directMessage && proxyMessage) {
expect(directMessage).toEqual(proxyMessage);
expect(directMessage).toEqual(sendMessage);
done();
}
}
});
it('masked:true', done => {
const sendMessage = 'Send the message with option masked:true';
let directMessage ; // set the flag for direct message, compare when both direct and proxy got message
let proxyMessage;
const ws = directWs(url);
const porxyWsRef = proxyWs(url);
ws.on('open', () => {
ws.send(sendMessage, { masked: true });
});
porxyWsRef.on('open', () => {
porxyWsRef.send(sendMessage, { masked: true });
});
ws.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
directMessage = message.content;
compareMessageIfReady();
}
});
porxyWsRef.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
proxyMessage = message.content;
compareMessageIfReady();
}
});
ws.on('error', error => {
console.error('error happened in direct websocket:', error);
done.fail('Error happened in direct websocket');
});
porxyWsRef.on('error', error => {
console.error('error happened in proxy websocket:', error);
done.fail('Error happened in proxy websocket');
});
function compareMessageIfReady () {
if (directMessage && proxyMessage) {
expect(directMessage).toEqual(proxyMessage);
expect(directMessage).toEqual(sendMessage);
done();
}
}
});
});
}

View File

@@ -0,0 +1,57 @@
/*
* test for rule shouldUseLocal
*
*/
const ProxyServerUtil = require('./util/ProxyServerUtil.js');
const { proxyGet, generateUrl } = require('./util/HttpUtil.js');
const Server = require('./server/server.js');
const { printLog } = require('./util/CommonUtil.js');
const rule = require('./test_rules/shouldUseLocalResponseRule.js');
const expectedLocalBody = 'handled_in_local_response';
testWrapper('http');
testWrapper('https');
function testWrapper(protocol, ) {
describe('Rule shouldUseLocalResponse should be working in :' + protocol, () => {
let proxyServer ;
let serverInstance ;
beforeAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000000;
printLog('Start server for rule_shouldUseLocalResponse_spec');
serverInstance = new Server();
proxyServer = ProxyServerUtil.proxyServerWithRule(rule);
setTimeout(function() {
done();
}, 2000);
});
afterAll(() => {
serverInstance && serverInstance.close();
proxyServer && proxyServer.close();
printLog('Close server for rule_shouldUseLocalResponse_spec');
});
it('Should use local response if the assertion is true', done => {
const url = generateUrl(protocol, '/test/uselocal');
proxyGet(url, {})
.then(res => {
expect(res.body).toEqual(expectedLocalBody);
expect(res.headers['via-proxy-local']).toEqual('true');
done();
}, error => {
console.log('error happened in proxy get for shouldUseLocal: ',error);
done.fail('error happened when test shouldUseLocal rule');
});
});
});
}

274
test/server/server.js Normal file
View File

@@ -0,0 +1,274 @@
const Koa = require('koa');
const KoaRouter = require('koa-router');
const koaBody = require('koa-body');
const send = require('koa-send');
const path = require('path');
const https = require('https');
const certMgr = require("../../lib/certMgr");
const fs = require('fs');
const websocket = require('koa-websocket');
const wsRouter = require('koa-router')();
const color = require('colorful');
const WebSocketServer = require('ws').Server;
const DEFAULT_PORT = 3000;
const HTTPS_PORT = 3001;
const UPLOAD_DIR = './test/temp';
const PROXY_KEY_PREFIX = 'proxy-';
function KoaServer() {
this.httpServer = null;
this.httpsServer = null;
this.requestRecordMap = {}; // store all request data to the map
const self = this;
/**
* log the request info, write as
*/
this.logRequest = function* (next) {
const headers = this.request.headers;
let key = this.request.host + this.request.url;
// take proxy data with 'proxy-' + url
if (headers['via-proxy'] === 'true') {
key = PROXY_KEY_PREFIX + key;
}
let body = this.request.body;
body = typeof body === 'object' ? JSON.stringify(body) : body;
self.requestRecordMap[key] = {
headers: headers,
body: body
};
yield next;
};
this.start();
}
KoaServer.prototype.constructRouter = function() {
const router = KoaRouter();
router.post('/test/getuser', this.logRequest, koaBody(), function*(next) {
printLog('requesting post /test/getuser');
this.response.set('reqbody', JSON.stringify(this.request.body));
this.response.body = 'something in post';
});
router.get('/test', this.logRequest, function*(next) {
printLog('request in get: ' + JSON.stringify(this.request));
this.response.body = 'something';
this.response.__req = this.request;
printLog('response in get:' + JSON.stringify(this.response));
});
router.get('/test/uselocal', this.logRequest, function*(next) {
printLog('request in get local:' + JSON.stringify(this.request));
this.response.body = 'something should be in local';
this.response.__req = this.request;
printLog('response in get:' + JSON.stringify(this.response));
});
['png', 'webp', 'json', 'js', 'css', 'ttf', 'eot', 'svg', 'woff', 'woff2'].forEach(item => {
router.get(`/test/download/${item}`, this.logRequest, function* (next) {
printLog(`now downloading the ${item}`);
yield send(this, `test/data/test.${item}`);
});
});
router.get('/test/response/303', function*(next) {
printLog('now to redirect 303');
this.redirect('/test');
this.status = 303;
});
router.get('/test/response/302', function*(next) {
printLog('now to redirect 302');
this.redirect('/test');
});
router.get('/test/response/301', function*(next) {
printLog('now to redirect permanently');
this.redirect('/test');
this.status = 301;
});
const onFileBegin = function(name, file) {
if (!fs.existsSync('./test/temp')) {
try {
fs.mkdirSync('./test/temp', '0777');
} catch (e) {
return null;
}
}
file.name = 'test_upload_' + Date.now() + '.png';
var folder = path.dirname(file.path);
file.path = path.join(folder, file.name);
};
router.post('/test/upload/png',
this.logRequest,
koaBody({
multipart: true,
formidable: {
uploadDir: UPLOAD_DIR,
onFileBegin: onFileBegin
}
}),
function*(next) {
const file = this.request.body.files.file;
this.response.set('reqbody', JSON.stringify(this.request.body.fields));
this.response.body = file.path;
}
);
router.put('/test/upload/putpng',
this.logRequest,
koaBody({
multipart: true,
formidable: {
uploadDir: UPLOAD_DIR,
onFileBegin: onFileBegin
}
}),
function*(next) {
const file = this.request.body.files.file;
this.response.body = file.path;
}
);
router.put('/test/put', koaBody(), this.logRequest, function*(next) {
printLog('requesting put /test/put' + JSON.stringify(this.request));
this.response.body = 'something in put';
});
router.delete('/test/delete/:id', this.logRequest, function*(next) {
printLog('requesting delete /test/delete/:id'+ JSON.stringify(this.params));
this.response.body = 'something in delete';
});
router.head('/test/head', this.logRequest, function*(next) {
printLog('requesting head /test/head');
this.response.body = ''; // the body will not be passed to response, in HEAD request
this.response.set('reqBody', 'head_request_contains_no_resbody');
});
router.options('/test/options', this.logRequest, function*(next) {
printLog('requesting options /test/options');
this.response.body = 'could_be_empty';
this.response.set('Allow', 'GET, HEAD, POST, OPTIONS');
});
// router.connect('/test/connect', function *(next) {
// printLog('requesting connect /test/connect');
// this.response.body = 'connect_established_body';
// });
return router;
};
KoaServer.prototype.constructWsRouter = function() {
const wsRouter = KoaRouter();
const self = this;
wsRouter.get('/test/socket', function*(next) {
const ws = this.websocket;
const messageObj = {
type: 'initial',
content: 'default message'
};
ws.send(JSON.stringify(messageObj));
ws.on('message', message => {
printLog('message from request socket: ' + message);
self.handleRecievedMessage(ws, message);
});
yield next;
});
return wsRouter;
};
KoaServer.prototype.getRequestRecord = function (key) {
return this.requestRecordMap[key] || {};
};
KoaServer.prototype.getProxyRequestRecord = function (key) {
key = PROXY_KEY_PREFIX + key;
return this.requestRecordMap[key] || {};
};
KoaServer.prototype.handleRecievedMessage = function(ws, message) {
const newMessage = {
type: 'onMessage',
content: message
};
ws.send(JSON.stringify(newMessage));
};
KoaServer.prototype.start = function() {
printLog('Starting the server...');
const router = this.constructRouter();
const wsRouter = this.constructWsRouter();
const self = this;
const app = Koa();
websocket(app);
app.use(router.routes());
app.ws.use(wsRouter.routes());
this.httpServer = app.listen(DEFAULT_PORT);
printLog('HTTP is now listening on port :' + DEFAULT_PORT);
certMgr.getCertificate('localhost', function(error, keyContent, crtContent) {
if (error) {
console.error('failed to create https server:', error);
} else {
self.httpsServer = https.createServer({
key: keyContent,
cert: crtContent
}, app.callback());
// create wss server
const wss = new WebSocketServer({
server: self.httpsServer
});
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
printLog('received in wss: ' + message);
self.handleRecievedMessage(ws, message);
});
});
wss.on('error', error => {
console.error('erro happened in wss:%s', error);
});
self.httpsServer.listen(HTTPS_PORT);
printLog('HTTPS is now listening on port :' + HTTPS_PORT);
printLog('Server started successfully');
}
});
return this;
};
KoaServer.prototype.close = function() {
printLog('Closing server now...');
this.httpServer && this.httpServer.close();
this.httpsServer && this.httpsServer.close();
this.requestRecordMap = {};
printLog('Server closed successfully');
};
function printLog(content) {
console.log(color.cyan('===SERVER LOG===' + content));
}
module.exports = KoaServer;

View File

@@ -0,0 +1,3 @@
const Server = require('./server.js');
new Server();

View File

@@ -1,5 +1,8 @@
#!/bin/bash
echo "nodeunit is required to run these test cases"
echo "Begin to run the test suites, JASMINE is required.\n"
echo "Removing test temp directory before running"
rm -rf ./test/temp/*
echo "Removing done, test cases now running"
node -v
nodeunit test.js
jasmine JASMINE_CONFIG_PATH=./jasmine.json

View File

@@ -0,0 +1,22 @@
/*
* Rule defination for shouldUseLocalResponse
*
*/
const dealLocalBody = 'handled_in_local_response';
module.exports = {
shouldUseLocalResponse: function (req, reqBody) {
return req.url.indexOf('uselocal') > -1;
},
shouldInterceptHttpsReq: function () {
return true;
},
dealLocalResponse: function (req, reqBody, callback) {
const header = {
'Via-Proxy-Local': 'true'
};
callback(200, header, dealLocalBody);
}
};

153
test/util/CommonUtil.js Normal file
View File

@@ -0,0 +1,153 @@
/**
*
* The utility class for test
*/
const zlib = require('zlib');
const color = require('colorful');
/*
* Compare whether tow object are equal
*/
function isObjectEqual (source = {} , target = {}, url = '') {
source = Object.assign({}, source);
target = Object.assign({}, target);
let isEqual = true;
for(const key in source) {
isEqual = isEqual && source[key] === target[key];
if (!isEqual) {
console.info('source object :', source);
console.info('target object :', target);
printError(`different key in isObjectEqual is: "${key}", source is "${source[key]}",
target is "${target[key]}" the url is ${url}`);
break;
}
delete source[key];
delete target[key];
}
for(const key in target) {
isEqual = isEqual && source[key] === target[key];
if (!isEqual) {
console.info('source object :', source);
console.info('target object :', target);
printError(`different key in isObjectEqual is: "${key}", source is "${source[key]}",
target is "${target[key]}" the url is ${url}`);
break;
}
delete source[key];
delete target[key];
}
return isEqual;
}
/*
* Compare the header between direct with proxy
* Will exclude the header(s) which modified by proxy
*/
function isCommonResHeaderEqual (directHeaders, proxyHeaders, requestUrl) {
directHeaders = Object.assign({}, directHeaders);
proxyHeaders = Object.assign({}, proxyHeaders);
let isEqual = true;
const mustEqualFileds = []; // the fileds that have to be equal, or the assert will be failed
if (!/gzip/i.test(directHeaders['content-encoding'])) {
// if the content is gzipped, proxy will unzip and remove the header
mustEqualFileds.push('content-encoding');
mustEqualFileds.push('content-length');
}
mustEqualFileds.push('content-type');
mustEqualFileds.push('cache-control');
mustEqualFileds.push('allow');
// ensure the required fileds are same
mustEqualFileds.forEach(filedName => {
isEqual = directHeaders[filedName] === proxyHeaders[filedName];
delete directHeaders[filedName];
delete proxyHeaders[filedName];
});
// remained filed are good to be same, but are allowed to be different
// will warn out those different fileds
for (const key in directHeaders) {
if (directHeaders[key] !== proxyHeaders[key]) {
printWarn(`key "${key}" of two response headers are different in request "${requestUrl}" :
direct is: "${directHeaders[key]}", proxy is: "${proxyHeaders[key]}"`);
}
continue;
}
return isEqual;
}
/*
* Compare the request between direct with proxy
*
*/
function isCommonReqEqual(url, serverInstance) {
try{
url = url.replace('https://', '').replace('http://', ''); // only the remained path is required
let isEqual = true;
const directReqObj = serverInstance.getRequestRecord(url);
const proxyReqObj = serverInstance.getProxyRequestRecord(url);
// ensure the proxy header is correct
isEqual = isEqual && proxyReqObj.headers['via-proxy'] === 'true';
delete proxyReqObj.headers['via-proxy'];
// exclued accept-encoding from comparing, since the proxy will remove it before sending it out
delete directReqObj.headers['accept-encoding'];
// per undefined header, proxy will set it with 0, and an empty request body
if (typeof directReqObj.headers['content-length'] === 'undefined') {
directReqObj.headers['content-length'] = "0";
}
directReqObj.headers['content-type'] = trimFormContentType(directReqObj.headers['content-type']);
proxyReqObj.headers['content-type'] = trimFormContentType(proxyReqObj.headers['content-type']);
isEqual = isEqual && directReqObj.url === proxyReqObj.url;
isEqual = isEqual && isObjectEqual(directReqObj.headers, proxyReqObj.headers, url);
isEqual = isEqual && directReqObj.body === proxyReqObj.body;
return isEqual;
} catch (e) {
console.error(e);
}
}
/*
* for multipart-form, the boundary will be different with each update, we trim it here
*/
function trimFormContentType (contentType = '') {
return contentType.replace(/boundary.*/, '');
}
function printLog (content) {
console.log(color.blue('==LOG==: ' + content));
}
function printWarn(content) {
console.log(color.magenta('==WARN==: ' + content));
}
function printError(content) {
console.log(color.red('==ERROR==: ' + content));
}
module.exports = {
isObjectEqual,
isCommonResHeaderEqual,
printLog,
printWarn,
printError,
isCommonReqEqual
};

262
test/util/HttpUtil.js Normal file
View File

@@ -0,0 +1,262 @@
/**
* An util to make the request out
*
*/
const querystring = require('querystring');
const http = require('http');
const zlib = require('zlib');
const Buffer = require('buffer').Buffer;
const request = require('request');
const fs = require('fs');
const WebSocket = require('ws');
const HttpsProxyAgent = require('https-proxy-agent');
const DEFAULT_HOST = 'localhost';
const PROXY_HOST = 'http://localhost:8001';
const SOCKET_PROXY_HOST = 'http://localhost:8001';
const HTTP_SERVER_BASE = 'http://localhost:3000';
const HTTPS_SERVER_BASE = 'https://localhost:3001';
const WS_SERVER_BASE = 'ws://localhost:3000';
const WSS_SERVER_BASE = 'wss://localhost:3001';
const DEFAULT_PROXY_OPTIONS = {
port: 8001, // proxy的端口
method: 'GET',
host: 'localhost'
};
const DEFAULT_OPTIONS = {
};
function getHostFromUrl (url = '') {
const hostReg = /^(https{0,1}:\/\/)(\w+)/;
const match = url.match(hostReg);
return match && match[2] ? match[2] : '';
}
function getPortFromUrl (url = '') {
const portReg = /^https{0,1}:\/\/\w+(:(\d+)){0,1}/;
const match = url.match(portReg);
let port = match && match[2] ? match[2] : '';
if (!port) {
port = url.indexOf('https://') === 0 ? 443 : 80;
}
return port;
}
/**
* 获取url中的path
*/
function getPathFromUrl (url = '') {
const pathReg = /^https{0,1}:\/\/\w+(:\d+){0,1}(.+)/;
const match = url.match(pathReg);
const path = match && match[3] ? match[2] : url;
return path;
}
function proxyRequest (method = 'GET', url, params, headers = {}) {
return doRequest(method, url, params, headers, true);
}
/*
* 直接请求到真实服务器,不经过代理服务器
*
*/
function directRequest (method = 'GET', url, params, headers = {}) {
return doRequest(method, url, params, headers);
}
function directUpload (url, filepath, formParams = {}, headers = {}) {
return doUpload(url, 'POST', filepath, formParams, headers);
}
function proxyUpload (url, filepath, formParams = {}, headers = {}) {
return doUpload(url, 'POST', filepath, formParams, headers, true);
}
function directPutUpload (url, filepath, formParams = {}, headers = {}) {
return doUpload(url, 'PUT', filepath, formParams, headers);
}
function proxyPutUpload (url, filepath, headers = {}) {
return doUpload(url, 'PUT', filepath, headers, true);
}
function doRequest (method = 'GET', url, params, headers = {}, isProxy) {
headers = Object.assign({}, headers);
const requestData = {
method: method,
form: params,
url: url,
headers: headers,
rejectUnauthorized: false
};
if (isProxy) {
requestData.proxy = PROXY_HOST;
requestData.headers['via-proxy'] = 'true';
}
const requestTask = new Promise((resolve, reject) => {
request(
requestData,
function (error, response, body) {
if (error) {
reject(error);
return;
}
resolve(response);
}
);
});
return requestTask;
}
function doUpload (url, method, filepath, formParams, headers = {}, isProxy) {
let formData = {
file: fs.createReadStream(filepath)
};
formData = Object.assign({}, formData, formParams);
headers = Object.assign({}, headers);
const requestData = {
formData: formData,
url: url,
method: method,
headers: headers,
json: true,
rejectUnauthorized: false
};
if (isProxy) {
requestData.proxy = PROXY_HOST;
requestData.headers['via-proxy'] = 'true';
}
const requestTask = new Promise((resolve, reject) => {
request(
requestData,
function (error, response, body) {
if (error) {
reject(error);
return;
}
resolve(response);
}
);
});
return requestTask;
}
function doWebSocket(url, isProxy) {
let ws;
if (isProxy) {
const agent = new HttpsProxyAgent(SOCKET_PROXY_HOST);
ws = new WebSocket(url, {
agent: agent,
rejectUnauthorized: false
});
} else {
ws = new WebSocket(url, {
rejectUnauthorized: false
});
}
return ws;
}
function proxyGet (url, params, headers = {}) {
return proxyRequest('GET', url, params, headers);
}
function proxyPost (url, params, headers = {}) {
return proxyRequest('POST', url, params, headers);
}
function proxyPut (url, params, headers = {}) {
return proxyRequest('PUT', url, params, headers);
}
function proxyDelete (url, params, headers = {}) {
return proxyRequest('DELETE', url, params, headers);
}
function proxyHead(url, headers = {}) {
return proxyRequest('HEAD', url, {}, headers);
}
function proxyOptions(url, headers = {}) {
return proxyRequest('OPTIONS', url, {}, headers);
}
function directGet (url, params, headers = {}) {
return directRequest('GET', url, params, headers);
}
function directPost (url, params, headers = {}) {
return directRequest('POST', url, params, headers);
}
function directPut (url, params, headers = {}) {
return directRequest('PUT', url, params, headers);
}
function directDelete (url, params, headers = {}) {
return directRequest('DELETE', url, params, headers);
}
function directHead (url, headers = {}) {
return directRequest('HEAD', url, {} , headers);
}
function directOptions (url, headers ={}) {
return directRequest('OPTIONS', url, {}, headers);
}
function proxyWs (url) {
return doWebSocket(url, true);
}
function directWs (url) {
return doWebSocket(url);
}
/**
* generate the final url based on protocol and path
*
*/
function generateUrl (protocol, urlPath) {
return protocol === 'http' ? HTTP_SERVER_BASE + urlPath : HTTPS_SERVER_BASE + urlPath;
}
function generateWsUrl (protocol, urlPath) {
return protocol === 'wss' ? WSS_SERVER_BASE + urlPath : WS_SERVER_BASE + urlPath;
}
module.exports = {
proxyGet,
proxyPost,
directGet,
directPost,
directUpload,
proxyUpload,
generateUrl,
proxyWs,
directWs,
generateWsUrl,
directPut,
proxyPut,
directDelete,
proxyDelete,
directHead,
proxyHead,
directOptions,
proxyOptions,
directPutUpload,
proxyPutUpload
};

View File

@@ -0,0 +1,65 @@
/*
* Utility class for creating proxy server, used to create specfied proxy server
*
*/
let proxy = require('../../proxy.js');
const util = require('../../lib/util.js');
const DEFAULT_OPTIONS = {
type: "http",
port: 8001,
hostname: "localhost",
dbFile: null, // optional, save request data to a specified file, will use in-memory db if not specified
webPort: 8002, // optional, port for web interface
socketPort: 8003, // optional, internal port for web socket, replace this when it is conflict with your own service
throttle: 10000, // optional, speed limit in kb/s
disableWebInterface: false, //optional, set it when you don't want to use the web interface
setAsGlobalProxy: false, //set anyproxy as your system proxy
interceptHttps: true, // intercept https as well
silent: false //optional, do not print anything into terminal. do not set it when you are still debugging.
};
/**
*
* @return An instance of proxy, could be closed by calling `instance.close()`
*/
function defaultProxyServer () {
proxy = util.freshRequire('../proxy.js');
const options = util.merge({}, DEFAULT_OPTIONS);
options.rule = util.freshRequire('./rule_default.js');
return new proxy.proxyServer(options);
}
/*
* Create proxy server with rule
* @param rules
Object, the rule object which contains required intercept method
@return An instance of proxy, could be closed by calling `instance.close()`
*/
function proxyServerWithRule (rule) {
proxy = util.freshRequire('../proxy.js');
const options = util.merge({}, DEFAULT_OPTIONS);
options.rule = rule;
return new proxy.proxyServer(options);
}
function proxyServerWithoutHttpsIntercept (rule) {
proxy = util.freshRequire('../proxy.js');
const options = util.merge({}, DEFAULT_OPTIONS);
if (rule) {
options.rule = rule;
}
options.interceptHttps = false;
return new proxy.proxyServer(options);
}
module.exports = {
defaultProxyServer,
proxyServerWithoutHttpsIntercept,
proxyServerWithRule
};