add basic support for websocket proxy

This commit is contained in:
砚然
2017-12-12 20:07:06 +08:00
parent e54fb23daa
commit 60bc1498ae
24 changed files with 808 additions and 149 deletions

View File

@@ -4,12 +4,21 @@
*/
import { message } from 'antd';
export function initWs(wsPort = 8003, key = '') {
/**
* Initiate a ws connection.
* The default pay `do-not-proxy` means the ws do not need to be proxied.
* This is very important for AnyProxy its' own server, such as WEB UI, and the
* websocket detail panel, to prevent a recursive proxy.
* @param {wsPort} wsPort the port of websocket
* @param {key} path the path of the ws url
*
*/
export function initWs(wsPort = 8003, path = 'do-not-proxy') {
if(!WebSocket){
throw (new Error('WebSocket is not supportted on this browser'));
}
const wsClient = new WebSocket(`ws://${location.hostname}:${wsPort}/${key}`);
const wsClient = new WebSocket(`ws://${location.hostname}:${wsPort}/${path}`);
wsClient.onerror = (error) => {
console.error(error);
@@ -30,4 +39,3 @@ export function initWs(wsPort = 8003, key = '') {
export default {
initWs: initWs
};

View File

@@ -11,10 +11,11 @@ export function formatDate(date, formatter) {
if (typeof date !== 'object') {
date = new Date(date);
}
const transform = function(value) {
return value < 10 ? '0' + value : value;
};
return formatter.replace(/^YYYY|MM|DD|hh|mm|ss/g, function(match) {
return formatter.replace(/^YYYY|MM|DD|hh|mm|ss|ms/g, function(match) {
switch (match) {
case 'YYYY':
return transform(date.getFullYear());
@@ -28,6 +29,8 @@ export function formatDate(date, formatter) {
return transform(date.getHours());
case 'ss':
return transform(date.getSeconds());
case 'ms':
return transform(date.getMilliseconds());
}
});
}

View File

@@ -5,15 +5,12 @@
import React, { PropTypes } from 'react';
import ClassBind from 'classnames/bind';
import { Menu, Table, notification, Spin } from 'antd';
import clipboard from 'clipboard-js'
import JsonViewer from 'component/json-viewer';
import { Menu, Spin } from 'antd';
import ModalPanel from 'component/modal-panel';
import RecordRequestDetail from 'component/record-request-detail';
import RecordResponseDetail from 'component/record-response-detail';
import RecordWsMessageDetail from 'component/record-ws-message-detail';
import { hideRecordDetail } from 'action/recordAction';
import { selectText } from 'common/CommonUtil';
import { curlify } from 'common/curlUtil';
import Style from './record-detail.less';
import CommonStyle from '../style/common.less';
@@ -21,7 +18,8 @@ import CommonStyle from '../style/common.less';
const StyleBind = ClassBind.bind(Style);
const PageIndexMap = {
REQUEST_INDEX: 'REQUEST_INDEX',
RESPONSE_INDEX: 'RESPONSE_INDEX'
RESPONSE_INDEX: 'RESPONSE_INDEX',
WEBSOCKET_INDEX: 'WEBSOCKET_INDEX'
};
// the maximum length of the request body to decide whether to offer a download link for the request body
@@ -54,6 +52,10 @@ class RecordDetail extends React.Component {
});
}
hasWebSocket (recordDetail = {}) {
return recordDetail && recordDetail.method && recordDetail.method.toLowerCase() === 'websocket';
}
getRequestDiv(recordDetail) {
return <RecordRequestDetail recordDetail={recordDetail} />;
}
@@ -62,18 +64,45 @@ class RecordDetail extends React.Component {
return <RecordResponseDetail recordDetail={recordDetail} />;
}
getRecordContentDiv(recordDetail, fetchingRecord) {
getWsMessageDiv(recordDetail) {
const { globalStatus } = this.props;
return <RecordWsMessageDetail recordDetail={recordDetail} wsPort={globalStatus.wsPort} />;
}
getRecordContentDiv(recordDetail = {}, fetchingRecord) {
const getMenuBody = () => {
const menuBody = this.state.pageIndex === PageIndexMap.REQUEST_INDEX ?
this.getRequestDiv(recordDetail) : this.getResponseDiv(recordDetail);
let menuBody = null;
switch (this.state.pageIndex) {
case PageIndexMap.REQUEST_INDEX: {
menuBody = this.getRequestDiv(recordDetail);
break;
}
case PageIndexMap.RESPONSE_INDEX: {
menuBody = this.getResponseDiv(recordDetail);
break;
}
case PageIndexMap.WEBSOCKET_INDEX: {
menuBody = this.getWsMessageDiv(recordDetail);
break;
}
default: {
menuBody = this.getRequestDiv(recordDetail);
break;
}
}
return menuBody;
}
const websocketMenu = (
<Menu.Item key={PageIndexMap.WEBSOCKET_INDEX}>WebSocket</Menu.Item>
);
return (
<div className={Style.wrapper} >
<Menu onClick={this.onMenuChange} mode="horizontal" selectedKeys={[this.state.pageIndex]} >
<Menu.Item key={PageIndexMap.REQUEST_INDEX}>Request</Menu.Item>
<Menu.Item key={PageIndexMap.RESPONSE_INDEX}>Response</Menu.Item>
{this.hasWebSocket(recordDetail) ? websocketMenu : null}
</Menu>
<div className={Style.detailWrapper} >
{fetchingRecord ? this.getLoaingDiv() : getMenuBody()}
@@ -92,8 +121,9 @@ class RecordDetail extends React.Component {
}
getRecordDetailDiv() {
const recordDetail = this.props.requestRecord.recordDetail;
const fetchingRecord = this.props.globalStatus.fetchingRecord;
const { requestRecord, globalStatus } = this.props;
const recordDetail = requestRecord.recordDetail;
const fetchingRecord = globalStatus.fetchingRecord;
if (!recordDetail && !fetchingRecord) {
return null;
@@ -101,6 +131,17 @@ class RecordDetail extends React.Component {
return this.getRecordContentDiv(recordDetail, fetchingRecord);
}
componentWillReceiveProps(nextProps) {
const { requestRecord } = nextProps;
const { pageIndex } = this.state;
// if this is not websocket, reset the index to RESPONSE_INDEX
if (!this.hasWebSocket(requestRecord.recordDetail) && pageIndex === PageIndexMap.WEBSOCKET_INDEX) {
this.setState({
pageIndex: PageIndexMap.RESPONSE_INDEX
});
}
}
render() {
return (
<ModalPanel

View File

@@ -2,6 +2,7 @@
.wrapper {
padding: 5px 15px;
height: 100%;
word-wrap: break-word;
}
@@ -16,6 +17,8 @@
}
.detailWrapper {
position: relative;
min-height: 100%;
padding: 5px;
}

View File

@@ -31,7 +31,7 @@ class RecordResponseDetail extends React.Component {
}
static propTypes = {
requestRecord: PropTypes.object
recordDetail: PropTypes.object
}
onSelectText(e) {

View File

@@ -0,0 +1,147 @@
/**
* The panel to display the detial of the record
*
*/
import React, { PropTypes } from 'react';
import { message, Button, Icon } from 'antd';
import { formatDate } from 'common/CommonUtil';
import { initWs } from 'common/WsUtil';
import ClassBind from 'classnames/bind';
import Style from './record-ws-message-detail.less';
import CommonStyle from '../style/common.less';
const ToMessage = (props) => {
const { message: wsMessage } = props;
return (
<div className={Style.toMessage}>
<div className={`${Style.time} ${CommonStyle.right}`}>{formatDate(wsMessage.time, 'hh:mm:ss:ms')}</div>
<div className={Style.content}>{wsMessage.message}</div>
</div>
);
}
const FromMessage = (props) => {
const { message: wsMessage } = props;
return (
<div className={Style.fromMessage}>
<div className={Style.time}>{formatDate(wsMessage.time, 'hh:mm:ss:ms')}</div>
<div className={Style.content}>{wsMessage.message}</div>
</div>
);
}
class RecordWsMessageDetail extends React.Component {
constructor() {
super();
this.state = {
stateCheck: false, // a prop only to trigger state check
autoRefresh: true,
socketMessages: [] // the messages from websocket listening
};
this.updateStateRef = null; // a timeout ref to reduce the calling of update state
this.wsClient = null; // ref to the ws client
this.onMessageHandler = this.onMessageHandler.bind(this);
this.receiveNewMessage = this.receiveNewMessage.bind(this);
this.toggleRefresh = this.toggleRefresh.bind(this);
}
static propTypes = {
recordDetail: PropTypes.object,
wsPort: PropTypes.number
}
toggleRefresh () {
const { autoRefresh } = this.state;
this.state.autoRefresh = !autoRefresh;
this.setState({
stateCheck: true
});
}
receiveNewMessage (message) {
this.state.socketMessages.push(message);
this.updateStateRef && clearTimeout(this.updateStateRef);
this.updateStateRef = setTimeout(() => {
this.setState({
stateCheck: true
});
}, 100);
}
getMessageList () {
const { recordDetail } = this.props;
const { socketMessages } = this.state;
const { wsMessages = [] } = recordDetail;
const targetMessage = wsMessages.concat(socketMessages);
return targetMessage.map((messageItem, index) => {
return messageItem.isToServer ?
<ToMessage key={index} message={messageItem} /> : <FromMessage key={index} message={messageItem} />;
});
}
refreshPage () {
const { autoRefresh } = this.state;
if (autoRefresh && this.messageRef && this.messageContentRef) {
this.messageRef.scrollTop = this.messageContentRef.scrollHeight;
}
}
onMessageHandler (event) {
const { recordDetail } = this.props;
const data = JSON.parse(event.data);
const content = data.content;
if (data.type === 'updateLatestWsMsg' ) {
if (recordDetail.id === content.id) {
this.receiveNewMessage(content.message);
}
}
}
componentDidUpdate () {
this.refreshPage();
}
componentWillUnmount () {
this.wsClient && this.wsClient.removeEventListener('message', this.onMessageHandler);
}
componentDidMount () {
const { wsPort, recordDetail } = this.props;
if (!wsPort) {
return;
}
this.refreshPage();
this.wsClient = initWs(wsPort);
this.wsClient.addEventListener('message', this.onMessageHandler);
}
render() {
const { recordDetail } = this.props;
const { autoRefresh } = this.state;
if (!recordDetail) {
return null;
}
const playIcon = <Icon type="play-circle" />;
const pauseIcon = <Icon type="pause-circle" />;
return (
<div className={Style.wrapper} ref={(_ref) => this.messageRef = _ref}>
<div className={Style.contentWrapper} ref={(_ref) => this.messageContentRef = _ref}>
{this.getMessageList()}
</div>
<div className={Style.refreshBtn} onClick={this.toggleRefresh} >
{autoRefresh ? pauseIcon : playIcon}
</div>
</div>
);
}
}
export default RecordWsMessageDetail;

View File

@@ -0,0 +1,56 @@
@import '../style/constant.less';
.wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
}
.contentWrapper {
overflow: hidden;
}
.toMessage {
float: right;
clear: both;
margin: 5px auto;
.content {
background-color: @primary-color;
}
}
.fromMessage {
float: left;
clear: both;
max-width: 40%;
margin: 5px auto;
.content {
background: @success-color;
}
}
.time {
font-size: @font-size-xs;
color: @tip-color;
}
.content {
clear: both;
border-radius: @border-radius-base;
color: #fff;
padding: 7px 8px;
font-size: @font-size-sm;
word-wrap: break-word;
word-break: break-all;
}
.refreshBtn {
position: fixed;
right: 20px;
bottom: 5px;
opacity: 0.53;
font-size: @font-size-large;
cursor: pointer;
}

View File

@@ -50,6 +50,9 @@ function* doFetchRecordBody(recordId) {
// const recordBody = { id: recordId };
yield put(updateFechingRecordStatus(true));
const recordBody = yield call(getJSON, '/fetchBody', { id: recordId });
if (recordBody.method && recordBody.method.toLowerCase() === 'websocket') {
recordBody.wsMessages = yield call(getJSON, '/fetchWsMessages', { id: recordId});
}
recordBody.id = parseInt(recordBody.id, 10);
yield put(updateFechingRecordStatus(false));

View File

@@ -54,13 +54,13 @@ body {
}
:global {
.ant-btn {
min-width: 100px;
}
// .ant-btn {
// min-width: 100px;
// }
}
.relativeWrapper {
position: relative;
width: 100%;
height: 100%;
}
}