add ui config server

This commit is contained in:
加里 2014-09-11 10:22:08 +08:00
parent bc5659b14f
commit b51236d678
14 changed files with 604 additions and 40 deletions

View File

@ -4,6 +4,8 @@ A fully configurable proxy in NodeJS, which can handle HTTPS requests perfectly.
(Chinese in this doc is nothing but translation of some key points. Be relax if you dont understand.)
因为一些集团开源/专利限制我们暂时把anyproxy迁回了gitlab。如果喜欢欢迎到[https://github.com/alipay-ct-wd/anyproxy](https://github.com/alipay-ct-wd/anyproxy)为我们点赞。
![](https://i.alipayobjects.com/i/ecmng/png/201409/3NKRCRk2Uf.png_250x.png)
Feature
@ -12,6 +14,7 @@ Feature
* fully configurable, you can modify a request at any stage by your own javascript code
* when working as https proxy, it can generate and intercept https requests for any domain without complaint by browser (after you trust its root CA)
* a web interface is availabe for you to view request details
* (beta)a web UI interface for you to replace some remote response with local data
![screenshot](http://gtms03.alicdn.com/tps/i3/TB1ddyqGXXXXXbXXpXXihxC1pXX-1000-549.jpg_640x640q90.jpg)
@ -73,19 +76,24 @@ How to write your own rule file
```javascript
module.exports = {
/*
these functions will overwrite the default ones, write your own when necessary.
*/
summary:function(){
console.log("this is a blank rule for anyproxy");
return "this is a blank rule for anyproxy";
},
//=======================
//when getting a request from user
//收到用户请求之后
//=======================
//是否在本地直接发送响应(不再向服务器发出请求)
//whether to intercept this request by local logic
//if the return value is true, anyproxy will call dealLocalResponse to get response data and will not send request to remote server anymore
shouldUseLocalResponse : function(req,reqBody){
return false;
},
//如果shouldUseLocalResponse返回true会调用这个函数来获取本地响应内容
//you may deal the response locally instead of sending it to server
//this function be called when shouldUseLocalResponse returns true
//callback(statusCode,resHeader,responseData)
@ -94,6 +102,13 @@ module.exports = {
callback(statusCode,resHeader,responseData)
},
//=======================
//when ready to send a request to server
//向服务端发出请求之前
//=======================
//替换向服务器发出的请求协议http和https的替换
//replace the request protocol when sending to the real server
//protocol : "http" or "https"
replaceRequestProtocol:function(req,protocol){
@ -101,6 +116,7 @@ module.exports = {
return newProtocol;
},
//替换向服务器发出的请求参数option)
//req is user's request which will be sent to the proxy server, docs : http://nodejs.org/api/http.html#http_http_request_options_callback
//you may return a customized option to replace the original option
//you should not write content-length header in options, since anyproxy will handle it for you
@ -109,17 +125,25 @@ module.exports = {
return newOption;
},
//替换请求的body
//replace the request body
replaceRequestData: function(req,data){
return data;
},
//=======================
//when ready to send the response to user after receiving response from server
//向用户返回服务端的响应之前
//=======================
//替换服务器响应的http状态码
//replace the statusCode before it's sent to the user
replaceResponseStatusCode: function(req,res,statusCode){
var newStatusCode = statusCode;
return newStatusCode;
},
//替换服务器响应的http头
//replace the httpHeader before it's sent to the user
//Here header == res.headers
replaceResponseHeader: function(req,res,header){
@ -127,6 +151,7 @@ module.exports = {
return newHeader;
},
//替换服务器响应的数据
//replace the response from the server before it's sent to the user
//you may return either a Buffer or a string
//serverResData is a Buffer, you may get its content by calling serverResData.toString()
@ -134,12 +159,18 @@ module.exports = {
return serverResData;
},
//在请求返回给用户前的延迟时间
//add a pause before sending response to user
pauseBeforeSendingResponse : function(req,res){
var timeInMS = 1; //delay all requests for 1ms
return timeInMS;
},
//=======================
//https config
//=======================
//是否截获https请求
//should intercept https request, or it will be forwarded to real server
shouldInterceptHttpsReq :function(req){
return false;

View File

@ -79,7 +79,7 @@ function userRequestHandler(req,userRes){
}
resourceInfo.resHeader = resHeader || {};
resourceInfo.resBody = resBody;
resourceInfo.length = resBody.length;
resourceInfo.length = resBody ? resBody.length : 0;
resourceInfo.statusCode = statusCode;
GLOBAL.recorder && GLOBAL.recorder.updateRecord(resourceInfoId,resourceInfo);
@ -171,12 +171,12 @@ function userRequestHandler(req,userRes){
//send response
},function(callback){
if(404 == statusCode){
var html404path = pathUtil.join(__dirname, '..', 'web', '404.html');
userRes.end(fs.readFileSync(html404path));
}else{
// if(404 == statusCode){
// var html404path = pathUtil.join(__dirname, '..', 'web', '404.html');
// userRes.end(fs.readFileSync(html404path));
// }else{
userRes.end(serverResData);
}
// }
callback();
//udpate record info
@ -304,14 +304,26 @@ function setRules(newRule){
if(!newRule){
return;
}else{
userRule = util.merge(defaultRule,newRule);
'function' == typeof(userRule.summary) && userRule.summary();
if(!newRule.summary){
newRule.summary = function(){
return "this rule file does not have a summary";
};
}
userRule = util.merge(defaultRule,newRule);
'function' == typeof(userRule.summary) && console.log(userRule.summary());
}
}
function getRuleSummary(){
return userRule.summary();
}
module.exports.userRequestHandler = userRequestHandler;
module.exports.connectReqHandler = connectReqHandler;
module.exports.setRules = setRules;
module.exports.getRuleSummary = getRuleSummary;
/*
note

View File

@ -1,9 +1,10 @@
module.exports = {
summary:function(){
console.log("this is the default rule for anyproxy, which supports CORS request");
return "the default rule for anyproxy, which supports CORS request";
},
shouldUseLocalResponse : function(req,reqBody){
//intercept all options request
if(req.method == "OPTIONS"){
return true;

View File

@ -1,6 +1,6 @@
{
"name": "anyproxy",
"version": "2.2.1",
"version": "2.3.0",
"description": "a charles/fiddler like proxy written in NodeJs, which can handle HTTPS requests and CROS perfectly.",
"main": "proxy.js",
"bin": {
@ -14,18 +14,15 @@
"entities": "^1.1.1",
"express": "^4.8.5",
"iconv-lite": "^0.4.4",
"juicer": "^0.6.6-stable",
"nedb": "^0.11.0",
"ws": "^0.4.32",
"iconv-lite": "^0.4.4"
},
"devDependencies": {
"ws": "^0.4.32"
},
"devDependencies": {},
"scripts": {
"test": "nodeunit test.js"
},
"repository": {
},
"repository": {},
"author": "ottomao@gmail.com",
"license": "ISC"
}

115
proxy.js
View File

@ -17,10 +17,13 @@ var http = require('http'),
getPort = require("./lib/getPort"),
requestHandler = require("./lib/requestHandler"),
Recorder = require("./lib/Recorder"),
inherits = require("util").inherits,
util = require("./lib/util"),
entities = require("entities"),
express = require("express"),
path = require("path"),
juicer = require('juicer'),
events = require("events"),
WebSocketServer= require('ws').Server;
GLOBAL.recorder = new Recorder();
@ -30,6 +33,7 @@ var T_TYPE_HTTP = 0,
DEFAULT_PORT = 8001,
DEFAULT_WEB_PORT = 8002,
DEFAULT_WEBSOCKET_PORT = 8003,
DEFAULT_CONFIG_PORT = 8080,
DEFAULT_HOST = "localhost",
DEFAULT_TYPE = T_TYPE_HTTP;
@ -95,7 +99,15 @@ function proxyServer(option){
//start web interface
function(callback){
startWebServer();
var webServer = new proxyWebServer();
var wss = webServer.wss;
var configServer = new UIConfigServer(DEFAULT_CONFIG_PORT);
configServer.on("rule_changed",function() {
console.log(arguments);
})
// var wss = proxyWebServer();
callback(null);
}
],
@ -119,7 +131,90 @@ function proxyServer(option){
}
}
function startWebServer(port){
// doing
function UIConfigServer(port){
var self = this;
var app = express(),
customerRule = {
summary: function(){
console.log("replace some response with local response");
return "replace some response with local response";
}
},
userKey;
customerRule.shouldUseLocalResponse = function(req,reqBody){
var url = req.url;
if(userKey){
var ifMatch = false;
userKey.map(function(item){
if(ifMatch) return;
var matchCount = 0;
if( !item.urlKey && !item.reqBodyKey){
ifMatch = false;
return;
}else{
if(!item.urlKey || (item.urlKey && url.indexOf(item.urlKey) >= 0 ) ){
++matchCount;
}
if(!item.reqBodyKey || (item.reqBodyKey && reqBody.toString().indexOf(item.reqBodyKey) >= 0) ){
++matchCount;
}
ifMatch = (matchCount==2);
if(ifMatch){
req.willResponse = item.localResponse;
}
}
});
return ifMatch;
}else{
return false;
}
};
customerRule.dealLocalResponse = function(req,reqBody,callback){
callback(200,{"content-type":"text/html"},req.willResponse)
return req.willResponse;
};
app.post("/update",function(req,res){
var data = "";
req.on("data",function(chunk){
data += chunk;
});
req.on("end",function(){
userKey = JSON.parse(data);
res.statusCode = 200;
res.setHeader("Content-Type", "application/json;charset=UTF-8");
res.end(JSON.stringify({success : true}));
requestHandler.setRules(customerRule);
self.emit("rule_changed");
});
});
app.use(express.static(__dirname + "/web_uiconfig"));
app.listen(port);
self.app = app;
}
inherits(UIConfigServer, events.EventEmitter);
function proxyWebServer(port){
var self = this;
port = port || DEFAULT_WEB_PORT;
//web interface
@ -150,13 +245,22 @@ function startWebServer(port){
res.end(entities.encodeHTML(body));
});
app.use(function(req,res,next){
var indexHTML = fs.readFileSync("./web/index.html",{encoding:"utf8"});
if(req.url == "/"){
res.setHeader("Content-Type", "text/html");
res.end(indexHTML.replace("{{rule}}",requestHandler.getRuleSummary()) );
}else{
next();
}
});
app.use(express.static(__dirname + '/web'));
app.listen(port);
var tipText = "web interface started at port " + port;
console.log(color.green(tipText));
//web socket interface
var wss = new WebSocketServer({port: DEFAULT_WEBSOCKET_PORT});
@ -169,6 +273,9 @@ function startWebServer(port){
GLOBAL.recorder.on("update",function(data){
wss.broadcast( JSON.stringify(data) );
});
self.app = app;
self.wss = wss;
}

View File

@ -2,10 +2,10 @@ module.exports = {
/*
These functions will overwrite the default ones, write your own when necessary.
Comments in Chinese are nothing but a translation of key points. Be relax if you dont understand.
致中文用户中文注释都是只摘要必要时请参阅英文注释欢迎提出修改建议
致中文用户中文注释都是只摘要必要时请参阅英文文档欢迎提出修改建议
*/
summary:function(){
console.log("this is a blank rule for anyproxy");
return "this is a blank rule for anyproxy";
},

View File

@ -2,10 +2,13 @@
background: #000;
height: 42px;
position: relative;
-webkit-box-shadow: 0px 3px 23px 0px rgba(50, 50, 50, 0.75);
-moz-box-shadow: 0px 3px 23px 0px rgba(50, 50, 50, 0.75);
box-shadow: 0px 3px 23px 0px rgba(50, 50, 50, 0.75);
}
.topHead h1{
color: rgb(204,204,204);
color: #CCCCCC;
display: inline-block;
}
@ -17,6 +20,17 @@
color: #777;
}
.ruleDesc{
background: #88C4FE;
border-bottom: 1px solid #333;
}
.ruleDesc h4{
color: #333;
line-height: 25px;
margin: 0;
}
.mainTableWrapper{
margin-top: 0;
}

3
web/footer.html Normal file
View File

@ -0,0 +1,3 @@
</body>
</html>

2
web/header.html Normal file
View File

@ -0,0 +1,2 @@

View File

@ -5,6 +5,8 @@
<link rel="stylesheet" href="/css/uikit.gradient.min.css" />
<link rel="stylesheet" href="/css/page.css" />
<link rel="icon" type="image/png" href="/favico.png" />
<script charset="utf-8" id="seajsnode"src="http://static.alipayobjects.com/seajs/??seajs/2.2.0/sea.js,seajs-combo/1.0.1/seajs-combo.js,seajs-style/1.0.2/seajs-style.js"></script>
</head>
<body>
<div class="topHead">
@ -12,8 +14,11 @@
<a href="#" class="J_clearBtn"><span class="topBtn">Clear Logs(Ctrl+X)</span></a>
<a href="#" class="J_statusBtn"><span class="topBtn">Stop</span></a>
<a href="#" class="J_statusBtn btn_disable"><span class="topBtn">Resume</span></a>
<a href="http://localhost:8080"><span class="topBtn">Config Local Response(beta)</span></a>
</div>
<div class="ruleDesc">
<h4>rule : <strong>{{rule}}</strong></h4>
</div>
<div class="mainTableWrapper J_mainTable">
<table class="uk-table uk-table-condensed uk-table-hover">
<thead>
@ -90,7 +95,6 @@
<% } %>
</script>
<script charset="utf-8" id="seajsnode"src="http://static.alipayobjects.com/seajs/??seajs/2.2.0/sea.js,seajs-combo/1.0.1/seajs-combo.js,seajs-style/1.0.2/seajs-style.js"></script>
<script src="/page.js"></script>
</body>

View File

@ -5,7 +5,7 @@ seajs.config({
'Backbone' : 'gallery/backbone/1.1.2/backbone.js',
'Underscore': 'gallery/underscore/1.6.0/underscore.js'
}
});
});
seajs.use(['$','Underscore' ,'Backbone'], function($, _, Backbone) {
Backbone.$ = $;

182
web_uiconfig/css/page.css Normal file
View File

@ -0,0 +1,182 @@
.topHead{
background: #000;
height: 42px;
position: relative;
}
.topHead h1{
color: rgb(204,204,204);
display: inline-block;
}
.topHead .topBtn{
margin: 0 5px;
}
.topHead .btn_disable{
color: #777;
}
.mainTableWrapper{
margin-top: 0;
}
.mainTableWrapper table{
table-layout: fixed;
}
.mainTableWrapper td,
.mainTableWrapper th{
padding: 4px 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mainTableWrapper tbody tr{
color: #AAA;
}
.mainTableWrapper tbody tr.record_status_done{
color: #000;
}
.mainTableWrapper .col_id{
width: 25px;
padding-right: 20px;
}
.mainTableWrapper .col_code{
width: 40px;
}
.mainTableWrapper .col_method{
width: 70px;
}
.mainTableWrapper .col_host{
width: 180px;
}
.mainTableWrapper .col_mime{
width: 150px;
}
.mainTableWrapper .col_time{
width: 160px;
}
.mainTableWrapper tr.row_odd{
background: #f5f5f5;
}
.mainTableWrapper tr.row_even{
background: #FFFFFF;
}
.uk-table-hover tbody tr:hover{
cursor: pointer;
background: #CCC;
}
.resHeader{
width: 400px;
}
.resBody textarea{
width: 400px;
height: 280px;
}
.subTitle{
padding-left: 6px;
border-left: 3px solid #1FA2D6;
font-size: 16px;
line-height: 16px;
}
.detail{
padding-left: 12px;
}
.overlay_mask{
position: fixed;
top:0;
left: 0;
width: 100%;
height: 100%;
background: #EEE;
opacity: 0.85;
}
.recordDetailOverlay{
z-index: 1;
height: 100%;
position: fixed;
left: 35%;
right: 0;
background: #FFF;
border-left: 1px solid #CCC;
top: 0;
padding: 10px 10px 20px 10px;
overflow-y:scroll;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
.recordDetailOverlay .escBtn{
position: absolute;
right: 10px;
top: 8px;
color: #777;
cursor: pointer;
text-decoration: underline;
}
.recordDetailOverlay li{
white-space: nowrap;
word-wrap: break-word;
}
.data_id{
color: #777;
}
.http_status{
font-weight: 700;
}
.http_status_200{
color: #408E2F;
}
.http_status_404,
.http_status_500,
.http_status_501,
.http_status_502,
.http_status_503,
.http_status_504
{
color: #910A0A;
}
#dragbar{
position:absolute;
left:0px;
top:0px;
height: 100%;
float: left;
background-color:#CCC;
width: 3px;
cursor: col-resize;
}
#ghostbar{
width:3px;
background-color:#000;
opacity:0.5;
position:absolute;
cursor: col-resize;
z-index:999
}

3
web_uiconfig/css/uikit.gradient.min.css vendored Executable file

File diff suppressed because one or more lines are too long

208
web_uiconfig/index.html Normal file
View File

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html>
<head>
<title>Anyproxy</title>
<link rel="stylesheet" href="/css/uikit.gradient.min.css" />
<link rel="stylesheet" href="/css/page.css" />
<link rel="icon" type="image/png" href="/favico.png" />
<script charset="utf-8" id="seajsnode"src="http://static.alipayobjects.com/seajs/??seajs/2.2.0/sea.js,seajs-combo/1.0.1/seajs-combo.js,seajs-style/1.0.2/seajs-style.js"></script>
</head>
<body>
<div class="topHead">
<h1>Anyproxy - Settings</h1>
</div>
<div>
<h3>Current Rules</h3>
<hr>
<div class="list sectionWrapper">
<ul class="uk-list uk-list-line uk-list-space J_listWrapper">
</ul>
</div>
<h3>Add new rule</h3>
<hr>
<div class="content sectionWrapper">
<form class="uk-form uk-form-stacked J_infoForm">
<div class="uk-form-row">
<label class="uk-form-label" for="form-s-it">Name</label>
<div class="uk-form-controls">
<input type="text" name="name" required placeholder="rule name">
</div>
</div>
<div class="uk-form-row">
<label class="uk-form-label" for="form-s-it">URL keywords</label>
<div class="uk-form-controls">
<input type="text" class="uk-form-width-large" name="urlKey" placeholder="api.sample.com/apiA">
</div>
</div>
<div class="uk-form-row">
<label class="uk-form-label" for="form-s-ip">Body keywords</label>
<div class="uk-form-controls">
<input type="text" class="uk-form-width-large" name="reqBodyKey" placeholder="some keywords in request body">
</div>
</div>
<div class="uk-form-row">
<label class="uk-form-label" for="form-s-t">using Response Body</label>
<div class="uk-form-controls">
<textarea cols="70" rows="8" name="localResponse" placeholder="replace response with data"></textarea>
</div>
</div>
<div class="uk-form-row">
<button class="uk-button J_addBtn" type="button">add</button>
</div>
</form>
</div>
</div>
<style type="text/css">
.removeBtn{
display: inline-block;
margin-left: 10px;
}
.sectionWrapper{
padding: 0px 20px 20px;
}
</style>
<script type="text/template" id="listItemTpl">
<li>
<strong>{{name}}</strong>&nbsp;&nbsp;&nbsp;<a href="#" class="J_remove removeBtn" ruleId="{{id}}">(remove)</a><br>
{{urlKey}} {{reqBodyKey}}
<br>{{localResponse}}
</li>
</script>
<script type="text/javascript">
seajs.config({
base: 'http://static.alipayobjects.com/',
alias: {
'$' : 'jquery/jquery/1.7.2/jquery',
'Backbone' : 'gallery/backbone/1.1.2/backbone.js',
'Underscore': 'gallery/underscore/1.6.0/underscore.js'
}
});
seajs.use(['$','Underscore' ,'Backbone'], function($, _ ,Backbone) {
function dataMgmt(){
var self = this,
currentID = 0,
SAVING_KEY = "anyproxy_local",
currentLSString = localStorage.getItem(SAVING_KEY),
currentData = currentLSString ? JSON.parse(currentLSString) : [];
//init currentID
currentData.map(function(item){
currentID = (item.id >= currentID ? item.id + 1 : currentID);
});
_.extend(self, Backbone.Events);
self.data = currentData;
self.add = function(data){
data.id = currentID;
++currentID;
currentData.push(data);
updateLS();
}
self.remove = function(targetId){
currentData.map(function(item,index){
if(parseInt(item.id) == parseInt(targetId)){
currentData.splice(index,1);
}
});
updateLS();
}
self.syncToServer = function(cb){
$.post("/update",JSON.stringify(currentData),cb);
}
function updateLS(){
localStorage.setItem(SAVING_KEY,JSON.stringify(currentData));
self.syncToServer(function(){
self.trigger("update");
});
}
}
//config.model
function ruleViewController(config){
var self = this,
wrapper = config.wrapper,
liTpl = config.liTpl,
model = config.model;
self.render = function(data){
return substitute(liTpl,data);
}
model.on("update",function(data){
window.location.reload();
});
//init
model.data.map(function(item){
wrapper.append(self.render(item));
console.log(item);
});
}
var dataMgmtInstance = new dataMgmt();
dataMgmtInstance.syncToServer();
var ruleView = new ruleViewController({
model : dataMgmtInstance,
wrapper : $(".J_listWrapper"),
liTpl : $("#listItemTpl").html()
});
$(".J_addBtn").on("click",function(e){
e.preventDefault();
var info = $(".J_infoForm").serializeArray();
var finalData = {};
info.map(function(item){
finalData[item.name] = item.value;
});
dataMgmtInstance.add(finalData);
});
$(".J_listWrapper").on("click",function(e){
var srcNode = $(e.srcElement);
if(srcNode.hasClass("J_remove")){
var id = srcNode.attr("ruleId");
dataMgmtInstance.remove(parseInt(id));
}
});
function substitute(str, object, regexp){
return String(str).replace(regexp || (/\{\{([^{}]+)\}\}/g), function(match, name){
if (match.charAt(0) == '\\') return match.slice(1);
return (object[name] != null) ? object[name] : '';
});
};
});
</script>
</body>
</html>