mirror of
https://github.com/alibaba/anyproxy.git
synced 2025-07-28 16:37:42 +00:00
add basic support for websocket proxy
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
|
@@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -31,7 +31,7 @@ class RecordResponseDetail extends React.Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
requestRecord: PropTypes.object
|
||||
recordDetail: PropTypes.object
|
||||
}
|
||||
|
||||
onSelectText(e) {
|
||||
|
147
web/src/component/record-ws-message-detail.jsx
Normal file
147
web/src/component/record-ws-message-detail.jsx
Normal 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;
|
56
web/src/component/record-ws-message-detail.less
Normal file
56
web/src/component/record-ws-message-detail.less
Normal 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;
|
||||
}
|
@@ -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));
|
||||
|
@@ -54,13 +54,13 @@ body {
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-btn {
|
||||
min-width: 100px;
|
||||
}
|
||||
// .ant-btn {
|
||||
// min-width: 100px;
|
||||
// }
|
||||
}
|
||||
|
||||
.relativeWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user