From 60bc1498ae06d52f0b08829d76119a59c70af775 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A0=9A=E7=84=B6?= <yanran.wwj@alipay.com>
Date: Tue, 12 Dec 2017 20:07:06 +0800
Subject: [PATCH 1/3] add basic support for websocket proxy

---
 bin/anyproxy                                  |   9 +-
 lib/httpsServerMgr.js                         |  17 +-
 lib/log.js                                    |  23 ++
 lib/recorder.js                               |  73 ++++++
 lib/requestHandler.js                         | 242 +++++++++++++++++-
 lib/rule_default.js                           |  43 ++--
 lib/util.js                                   |  19 +-
 lib/webInterface.js                           |  18 ++
 lib/wsServer.js                               |  20 +-
 lib/wsServerMgr.js                            |  39 +++
 proxy.js                                      |  21 +-
 test/server/server.js                         |   2 +-
 test/spec_rule/no_rule_websocket_spec.js      | 126 +++++----
 test/util/CommonUtil.js                       |   3 +-
 test/util/ProxyServerUtil.js                  |   1 +
 web/src/common/WsUtil.js                      |  14 +-
 web/src/common/commonUtil.js                  |   5 +-
 web/src/component/record-detail.jsx           |  63 ++++-
 web/src/component/record-detail.less          |   3 +
 web/src/component/record-response-detail.jsx  |   2 +-
 .../component/record-ws-message-detail.jsx    | 147 +++++++++++
 .../component/record-ws-message-detail.less   |  56 ++++
 web/src/saga/rootSaga.js                      |   3 +
 web/src/style/common.less                     |   8 +-
 24 files changed, 808 insertions(+), 149 deletions(-)
 create mode 100644 lib/wsServerMgr.js
 create mode 100644 web/src/component/record-ws-message-detail.jsx
 create mode 100644 web/src/component/record-ws-message-detail.less

diff --git a/bin/anyproxy b/bin/anyproxy
index 330e083..108bc5d 100755
--- a/bin/anyproxy
+++ b/bin/anyproxy
@@ -1,6 +1,7 @@
 #!/usr/bin/env node
 
-'use strict'
+'use strict';
+
 const program = require('commander'),
   color = require('colorful'),
   packageInfo = require('../package.json'),
@@ -17,6 +18,7 @@ program
   .option('-i, --intercept', 'intercept(decrypt) https requests when root CA exists')
   .option('-s, --silent', 'do not print anything into terminal')
   .option('-c, --clear', 'clear all the certificates and temp files')
+  .option('--ws-intercept', 'intercept websocket')
   .option('--ignore-unauthorized-ssl', 'ignore all ssl error')
   .parse(process.argv);
 
@@ -62,7 +64,8 @@ if (program.clear) {
       webInterface: {
         enable: true,
         webPort: program.web,
-      },        
+      },
+      wsIntercept: program.wsIntercept,
       forceProxyHttps: program.intercept,
       dangerouslyIgnoreUnauthorized: !!program.ignoreUnauthorizedSsl,
       silent: program.silent
@@ -107,7 +110,7 @@ if (program.clear) {
     } catch (e) {}
     logUtil.printLog(errorTipText, logUtil.T_ERR);
     try {
-      proxyServer && proxyServer.close();        
+      proxyServer && proxyServer.close();
     } catch (e) {}
     process.exit();
   });
diff --git a/lib/httpsServerMgr.js b/lib/httpsServerMgr.js
index d57c14a..e66c840 100644
--- a/lib/httpsServerMgr.js
+++ b/lib/httpsServerMgr.js
@@ -9,12 +9,12 @@ const async = require('async'),
   certMgr = require('./certMgr'),
   logUtil = require('./log'),
   util = require('./util'),
+  wsServerMgr = require('./wsServerMgr'),
   co = require('co'),
   constants = require('constants'),
   asyncTask = require('async-task-mgr');
 
 const createSecureContext = tls.createSecureContext || crypto.createSecureContext;
-
 //using sni to avoid multiple ports
 function SNIPrepareCert(serverName, SNICallback) {
   let keyContent,
@@ -80,7 +80,6 @@ function createHttpsServer(config) {
         key: keyContent,
         cert: crtContent
       }, config.handler).listen(config.port);
-
       resolve(server);
     });
   });
@@ -131,6 +130,7 @@ class httpsServerMgr {
     this.instanceDefaultHost = '127.0.0.1';
     this.httpsAsyncTask = new asyncTask();
     this.handler = config.handler;
+    this.wsHandler = config.wsHandler
   }
 
   getSharedHttpsServer(hostname) {
@@ -159,12 +159,15 @@ class httpsServerMgr {
             });
           }
 
-
-          httpsServer.on('upgrade', (req, socket, head) => {
-            const reqHost = req.headers.host || 'unknown host';
-            logUtil.printLog(`wss:// is not supported when intercepting https. This request will be closed by AnyProxy. You may either exclude this domain in your rule file, or stop all https intercepting. (${reqHost})`, logUtil.T_ERR);
-            socket.end();
+          wsServerMgr.getWsServer({
+            server: httpsServer,
+            connHandler: self.wsHandler
           });
+
+          httpsServer.on('upgrade', (req, cltSocket, head) => {
+            logUtil.debug('will let WebSocket server to handle the upgrade event');
+          });
+
           const result = {
             host: finalHost,
             port: instancePort,
diff --git a/lib/log.js b/lib/log.js
index 470a494..18adb3e 100644
--- a/lib/log.js
+++ b/lib/log.js
@@ -25,6 +25,7 @@ function printLog(content, type) {
   if (!ifPrint) {
     return;
   }
+
   const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss');
   switch (type) {
     case LogLevelMap.tip: {
@@ -62,6 +63,7 @@ function printLog(content, type) {
     }
 
     case LogLevelMap.debug: {
+      console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
       return;
     }
 
@@ -73,6 +75,27 @@ function printLog(content, type) {
 }
 
 module.exports.printLog = printLog;
+
+module.exports.debug = (content) => {
+  printLog(content, LogLevelMap.debug);
+};
+
+module.exports.info = (content) => {
+  printLog(content, LogLevelMap.tip);
+};
+
+module.exports.warn = (content) => {
+  printLog(content, LogLevelMap.warn);
+};
+
+module.exports.error = (content) => {
+  printLog(content, LogLevelMap.error);
+};
+
+module.exports.ruleError = (content) => {
+  printLog(content, LogLevelMap.rule_error);
+};
+
 module.exports.setPrintStatus = setPrintStatus;
 module.exports.setLogLevel = setLogLevel;
 module.exports.T_TIP = LogLevelMap.tip;
diff --git a/lib/recorder.js b/lib/recorder.js
index 4bb49c0..94b2a83 100644
--- a/lib/recorder.js
+++ b/lib/recorder.js
@@ -4,11 +4,13 @@
 const Datastore = require('nedb'),
   path = require('path'),
   fs = require('fs'),
+  logUtil = require('./log'),
   events = require('events'),
   iconv = require('iconv-lite'),
   proxyUtil = require('./util');
 
 const BODY_FILE_PRFIX = 'res_body_';
+const WS_MESSAGE_FILE_PRFIX = 'ws_message_';
 const CACHE_DIR_PREFIX = 'cache_r';
 function getCacheDir() {
   const rand = Math.floor(Math.random() * 1000000),
@@ -85,6 +87,10 @@ class Recorder extends events.EventEmitter {
     }
   }
 
+  emitUpdateLatestWsMessage(id, message) {
+    this.emit('updateLatestWsMsg', message);
+  }
+
   updateRecord(id, info) {
     if (id < 0) return;
     const self = this;
@@ -98,6 +104,27 @@ class Recorder extends events.EventEmitter {
     self.emitUpdate(id, finalInfo);
   }
 
+  /**
+  * This method shall be called at each time there are new message
+  *
+  */
+  updateRecordWsMessage(id, message) {
+    const cachePath = this.cachePath;
+    if (id < 0) return;
+    try {
+      const recordWsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
+      fs.appendFile(recordWsMessageFile, JSON.stringify(message) + ',', () => {});
+    } catch (e) {
+      console.error(e);
+      logUtil.error(e.message + e.stack);
+    }
+
+    this.emitUpdateLatestWsMessage(id, {
+      id: id,
+      message: message
+    });
+  }
+
   updateExtInfo(id, extInfo) {
     const self = this;
     const db = self.db;
@@ -138,6 +165,10 @@ class Recorder extends events.EventEmitter {
     fs.writeFile(bodyFile, info.resBody, () => {});
   }
 
+  /**
+  * get body and websocket file
+  *
+  */
   getBody(id, cb) {
     const self = this;
     const cachePath = self.cachePath;
@@ -159,6 +190,7 @@ class Recorder extends events.EventEmitter {
   getDecodedBody(id, cb) {
     const self = this;
     const result = {
+      method: '',
       type: 'unknown',
       mime: '',
       content: ''
@@ -170,6 +202,9 @@ class Recorder extends events.EventEmitter {
         return;
       }
 
+      // also put the `method` back, so the client can decide whether to load ws messages
+      result.method = doc[0].method;
+
       self.getBody(id, (error, bodyContent) => {
         if (error) {
           cb(error);
@@ -212,6 +247,44 @@ class Recorder extends events.EventEmitter {
     });
   }
 
+  /**
+  * get decoded WebSoket messages
+  *
+  */
+  getDecodedWsMessage(id, cb) {
+    const self = this;
+    const cachePath = self.cachePath;
+
+    if (id < 0) {
+      cb && cb([]);
+    }
+
+    const wsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
+    fs.access(wsMessageFile, fs.F_OK || fs.R_OK, (err) => {
+      if (err) {
+        cb && cb(err);
+      } else {
+        fs.readFile(wsMessageFile, 'utf8', (error, content) => {
+          if (error) {
+            cb && cb(err);
+          }
+
+          try {
+            // remove the last dash "," if it has, since it's redundant
+            // and also add brackets to make it a complete JSON structure
+            content = `[${content.replace(/,$/, '')}]`;
+            const messages = JSON.parse(content);
+            cb(null, messages);
+          } catch (e) {
+            console.error(e);
+            logUtil.error(e.message + e.stack);
+            cb(e);
+          }
+        });
+      }
+    });
+  }
+
   getSingleRecord(id, cb) {
     const self = this;
     const db = self.db;
diff --git a/lib/requestHandler.js b/lib/requestHandler.js
index 3c93e7b..7cbf1e3 100644
--- a/lib/requestHandler.js
+++ b/lib/requestHandler.js
@@ -11,6 +11,7 @@ const http = require('http'),
   Stream = require('stream'),
   logUtil = require('./log'),
   co = require('co'),
+  WebSocket = require('ws'),
   HttpsServerMgr = require('./httpsServerMgr'),
   Readable = require('stream').Readable;
 
@@ -175,6 +176,34 @@ function fetchRemoteResponse(protocol, options, reqData, config) {
   });
 }
 
+/**
+* get request info from the ws client, includes:
+ host
+ port
+ path
+ protocol  ws/wss
+
+ @param @required wsClient the ws client of WebSocket
+*
+*/
+function getWsReqInfo(wsClient) {
+  const upgradeReq = wsClient.upgradeReq || {};
+  const header = upgradeReq.headers || {};
+  const host = header.host;
+  const hostName = host.split(':')[0];
+  const port = host.split(':')[1];
+
+  // TODO 如果是windows机器,url是不是全路径?需要对其过滤,取出
+  const path = upgradeReq.url || '/';
+
+  const isEncript = true && upgradeReq.connection && upgradeReq.connection.encrypted;
+  return {
+    hostName: hostName,
+    port: port,
+    path: path,
+    protocol: isEncript ? 'wss' : 'ws'
+  };
+}
 /**
  * get a request handler for http/https server
  *
@@ -452,10 +481,10 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
     const host = req.url.split(':')[0],
       targetPort = req.url.split(':')[1];
     let shouldIntercept;
+    let interceptWsRequest = false;
     let requestDetail;
     let resourceInfo = null;
     let resourceInfoId = -1;
-
     const requestStream = new CommonReadableStream();
 
     /*
@@ -468,14 +497,17 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
     co(function *() {
       // determine whether to use the man-in-the-middle server
       logUtil.printLog(color.green('received https CONNECT request ' + host));
-      if (reqHandlerCtx.forceProxyHttps) {
-        shouldIntercept = true;
-      } else {
-        requestDetail = {
-          host: req.url,
-          _req: req
-        };
-        shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail);
+      requestDetail = {
+        host: req.url,
+        _req: req
+      };
+      // the return value in default rule is null
+      // so if the value is null, will take it as final value
+      shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail);
+
+      // otherwise, will take the passed in option
+      if (shouldIntercept === null) {
+        shouldIntercept = reqHandlerCtx.forceProxyHttps;
       }
     })
     .then(() =>
@@ -494,7 +526,13 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
             try {
               const chunkString = chunk.toString();
               if (chunkString.indexOf('GET ') === 0) {
-                shouldIntercept = false; //websocket
+                shouldIntercept = false; // websocket, do not intercept
+
+                // if there is '/do-not-proxy' in the request, do not intercept the websocket
+                // to avoid AnyProxy itself be proxied
+                if (reqHandlerCtx.wsIntercept && chunkString.indexOf('GET /do-not-proxy') !== 0) {
+                  interceptWsRequest = true;
+                }
               }
             } catch (e) {
               console.error(e);
@@ -531,10 +569,19 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
     .then(() => {
       // determine the request target
       if (!shouldIntercept) {
-        return {
+        // server info from the original request
+        const orginServer = {
           host,
-          port: (targetPort === 80) ? 443 : targetPort,
+          port: (targetPort === 80) ? 443 : targetPort
         }
+
+        const localHttpServer = {
+          host: 'localhost',
+          port: reqHandlerCtx.httpServerPort
+        }
+
+        // for ws request, redirect them to local ws server
+        return interceptWsRequest ? localHttpServer : orginServer;
       } else {
         return httpsServerMgr.getSharedHttpsServer(host).then(serverInfo => ({ host: serverInfo.host, port: serverInfo.port }));
       }
@@ -594,6 +641,158 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
   }
 }
 
+/**
+* get a websocket event handler
+  @param @required {object} wsClient
+*/
+function getWsHandler(userRule, recorder, wsClient) {
+  const self = this;
+  try {
+    let resourceInfoId = -1;
+    const resourceInfo = {
+      wsMessages: [] // all ws messages go through AnyProxy
+    };
+    const clientMsgQueue = [];
+    const serverInfo = getWsReqInfo(wsClient);
+    const wsUrl = `${serverInfo.protocol}://${serverInfo.hostName}:${serverInfo.port}${serverInfo.path}`;
+    const proxyWs = new WebSocket(wsUrl, '', {
+      rejectUnauthorized: !self.dangerouslyIgnoreUnauthorized
+    });
+
+    if (recorder) {
+      Object.assign(resourceInfo, {
+        host: serverInfo.hostName,
+        method: 'WebSocket',
+        path: serverInfo.path,
+        url: wsUrl,
+        req: wsClient.upgradeReq || {},
+        startTime: new Date().getTime()
+      });
+      resourceInfoId = recorder.appendRecord(resourceInfo);
+    }
+
+    /**
+    * store the messages before the proxy ws is ready
+    */
+    const sendProxyMessage = (event) => {
+      const message = event.data;
+      if (proxyWs.readyState === 1) {
+        // if there still are msg queue consuming, keep it going
+        if (clientMsgQueue.length > 0) {
+          clientMsgQueue.push(message);
+        } else {
+          proxyWs.send(message);
+        }
+      } else {
+        clientMsgQueue.push(message);
+      }
+    }
+
+    /**
+    * consume the message in queue when the proxy ws is not ready yet
+    * will handle them from the first one-by-one
+    */
+    const consumeMsgQueue = () => {
+      while (clientMsgQueue.length > 0) {
+        const message = clientMsgQueue.shift();
+        proxyWs.send(message);
+      }
+    }
+
+    /**
+    * When the source ws is closed, we need to close the target websocket.
+    * If the source ws is normally closed, that is, the code is reserved, we need to transfrom them
+    */
+    const getCloseFromOriginEvent = (event) => {
+      const code = event.code || '';
+      const reason = event.reason || '';
+      let targetCode = '';
+      let targetReason = '';
+      if (code >= 1004 && code <= 1006) {
+        targetCode = 1000; // normal closure
+        targetReason = `Normally closed. The origin ws is closed at code: ${code} and reason: ${reason}`;
+      } else {
+        targetCode = code;
+        targetReason = reason;
+      }
+
+      return {
+        code: targetCode,
+        reason: targetReason
+      }
+    }
+
+    /**
+    * consruct a message Record from message event
+    * @param @required {event} messageEvent the event from websockt.onmessage
+    * @param @required {boolean} isIn whether the message is to or from server
+    *
+    */
+    const recordMessage = (messageEvent, isToServer) => {
+      const message = {
+        time: Date.now(),
+        message: messageEvent.data,
+        isToServer: isToServer
+      };
+
+      // resourceInfo.wsMessages.push(message);
+      recorder && recorder.updateRecordWsMessage(resourceInfoId, message);
+    };
+
+    proxyWs.onopen = () => {
+      consumeMsgQueue();
+    }
+
+    // this event is fired when the connection is build and headers is returned
+    proxyWs.on('headers', (headers, response) => {
+      resourceInfo.endTime = new Date().getTime();
+      resourceInfo.res = { //construct a self-defined res object
+        statusCode: response.statusCode,
+        headers: headers,
+      };
+
+      resourceInfo.statusCode = response.statusCode;
+      resourceInfo.resHeader = headers;
+      resourceInfo.resBody = '';
+      resourceInfo.length = resourceInfo.resBody.length;
+
+      recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
+    });
+
+    proxyWs.onerror = (e) => {
+      // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
+      wsClient.close(1001, e.message);
+      proxyWs.close(1001);
+    }
+
+    proxyWs.onmessage = (event) => {
+      recordMessage(event, false);
+      wsClient.readyState === 1 && wsClient.send(event.data);
+    }
+
+    proxyWs.onclose = (event) => {
+      logUtil.debug(`proxy ws closed with code: ${event.code} and reason: ${event.reason}`);
+      const targetCloseInfo = getCloseFromOriginEvent(event);
+      wsClient.readyState !== 3 && wsClient.close(targetCloseInfo.code, targetCloseInfo.reason);
+    }
+
+    wsClient.onmessage = (event) => {
+      recordMessage(event, true);
+      sendProxyMessage(event);
+    }
+
+    wsClient.onclose = (event) => {
+      logUtil.debug(`original ws closed with code: ${event.code} and reason: ${event.reason}`);
+      const targetCloseInfo = getCloseFromOriginEvent(event);
+      proxyWs.readyState !== 3 && proxyWs.close(targetCloseInfo.code, targetCloseInfo.reason);
+    }
+  } catch (e) {
+    logUtil.debug('WebSocket Proxy Error:' + e.message);
+    logUtil.debug(e.stack);
+    console.error(e);
+  }
+}
+
 class RequestHandler {
 
   /**
@@ -602,6 +801,7 @@ class RequestHandler {
    * @param {object} config
    * @param {boolean} config.forceProxyHttps proxy all https requests
    * @param {boolean} config.dangerouslyIgnoreUnauthorized
+     @param {number} config.httpServerPort  the http port AnyProxy do the proxy
    * @param {object} rule
    * @param {Recorder} recorder
    *
@@ -609,22 +809,36 @@ class RequestHandler {
    */
   constructor(config, rule, recorder) {
     const reqHandlerCtx = this;
+    this.forceProxyHttps = false;
+    this.dangerouslyIgnoreUnauthorized = false;
+    this.httpServerPort = '';
+    this.wsIntercept = false;
+
     if (config.forceProxyHttps) {
       this.forceProxyHttps = true;
     }
+
     if (config.dangerouslyIgnoreUnauthorized) {
       this.dangerouslyIgnoreUnauthorized = true;
     }
+
+    if (config.wsIntercept) {
+      this.wsIntercept = config.wsIntercept;
+    }
+
+    this.httpServerPort = config.httpServerPort;
     const default_rule = util.freshRequire('./rule_default');
     const userRule = util.merge(default_rule, rule);
 
     reqHandlerCtx.userRequestHandler = getUserReqHandler.apply(reqHandlerCtx, [userRule, recorder]);
+    reqHandlerCtx.wsHandler = getWsHandler.bind(this, userRule, recorder);
 
     reqHandlerCtx.httpsServerMgr = new HttpsServerMgr({
-      handler: reqHandlerCtx.userRequestHandler
+      handler: reqHandlerCtx.userRequestHandler,
+      wsHandler: reqHandlerCtx.wsHandler // websocket
     });
 
-    this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr])
+    this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr]);
   }
 }
 
diff --git a/lib/rule_default.js b/lib/rule_default.js
index e5c441e..dc74910 100644
--- a/lib/rule_default.js
+++ b/lib/rule_default.js
@@ -1,12 +1,12 @@
 'use strict';
 
 module.exports = {
-  
+
   summary: 'the default rule for AnyProxy',
-  
+
   /**
-   * 
-   * 
+   *
+   *
    * @param {object} requestDetail
    * @param {string} requestDetail.protocol
    * @param {object} requestDetail.requestOptions
@@ -23,8 +23,8 @@ module.exports = {
 
 
   /**
-   * 
-   * 
+   *
+   *
    * @param {object} requestDetail
    * @param {object} responseDetail
    */
@@ -34,21 +34,22 @@ module.exports = {
 
 
   /**
-   * 
-   * 
-   * @param {any} requestDetail 
-   * @returns 
+   * default to return null
+   * the user MUST return a boolean when they do implement the interface in rule
+   *
+   * @param {any} requestDetail
+   * @returns
    */
   *beforeDealHttpsRequest(requestDetail) {
-    return false;
+    return null;
   },
 
   /**
-   * 
-   * 
-   * @param {any} requestDetail 
-   * @param {any} error 
-   * @returns 
+   *
+   *
+   * @param {any} requestDetail
+   * @param {any} error
+   * @returns
    */
   *onError(requestDetail, error) {
     return null;
@@ -56,11 +57,11 @@ module.exports = {
 
 
   /**
-   * 
-   * 
-   * @param {any} requestDetail 
-   * @param {any} error 
-   * @returns 
+   *
+   *
+   * @param {any} requestDetail
+   * @param {any} error
+   * @returns
    */
   *onConnectError(requestDetail, error) {
     return null;
diff --git a/lib/util.js b/lib/util.js
index f101c39..a09e924 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -4,8 +4,8 @@ const fs = require('fs'),
   path = require('path'),
   mime = require('mime-types'),
   color = require('colorful'),
+  crypto = require('crypto'),
   Buffer = require('buffer').Buffer,
-  configUtil = require('./configUtil'),
   logUtil = require('./log');
 const networkInterfaces = require('os').networkInterfaces();
 
@@ -35,7 +35,7 @@ function getUserHome() {
 module.exports.getUserHome = getUserHome;
 
 function getAnyProxyHome() {
-  const home = configUtil.getAnyProxyHome();
+  const home = path.join(getUserHome(), '/.anyproxy/');
   if (!fs.existsSync(home)) {
     fs.mkdirSync(home);
   }
@@ -306,3 +306,18 @@ module.exports.isIpDomain = function (domain) {
 
   return ipReg.test(domain);
 };
+
+/**
+* To generic a Sec-WebSocket-Accept value
+* 1. append the `Sec-WebSocket-Key` request header with `matic string`
+* 2. get sha1 hash of the string
+* 3. get base64 of the sha1 hash
+*/
+module.exports.genericWsSecAccept = function (wsSecKey) {
+  // the string to generate the Sec-WebSocket-Accept
+  const magicString = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
+  const targetString = `${wsSecKey}${magicString}`;
+  const shasum = crypto.createHash('sha1');
+  shasum.update(targetString);
+  return shasum.digest('base64');
+}
diff --git a/lib/webInterface.js b/lib/webInterface.js
index 4ee90a3..6c04b32 100644
--- a/lib/webInterface.js
+++ b/lib/webInterface.js
@@ -128,6 +128,7 @@ class webInterface extends events.EventEmitter {
               res.json({
                 id: query.id,
                 type: result.type,
+                method: result.meethod,
                 fileName: result.fileName,
                 ref: `/downloadBody?id=${query.id}&download=${isDownload}&raw=${!isDownload}`
               });
@@ -143,6 +144,7 @@ class webInterface extends events.EventEmitter {
               res.json({
                 id: query.id,
                 type: result.type,
+                method: result.method,
                 resBody: result.content
               });
             };
@@ -188,6 +190,22 @@ class webInterface extends events.EventEmitter {
         }
       });
 
+      app.get('/fetchWsMessages', (req, res) => {
+        const query = req.query;
+        if (query && query.id) {
+          recorder.getDecodedWsMessage(query.id, (err, messages) => {
+            if (err) {
+              console.error(err);
+              res.json([]);
+              return;
+            }
+            res.json(messages);
+          });
+        } else {
+          res.json([]);
+        }
+      });
+
       app.get('/fetchCrtFile', (req, res) => {
         res.setHeader('Access-Control-Allow-Origin', '*');
         const _crtFilePath = certMgr.getRootCAFilePath();
diff --git a/lib/wsServer.js b/lib/wsServer.js
index e1fa39c..6fe69c5 100644
--- a/lib/wsServer.js
+++ b/lib/wsServer.js
@@ -99,7 +99,11 @@ class wsServer {
 
       wss.broadcast = function (data) {
         if (typeof data === 'object') {
-          data = JSON.stringify(data);
+          try {
+            data = JSON.stringify(data);
+          } catch (e) {
+            console.error('==> errorr when do broadcast ', e, data);
+          }
         }
         for (const client of wss.clients) {
           try {
@@ -137,6 +141,20 @@ class wsServer {
         }
       });
 
+      recorder.on('updateLatestWsMsg', (data) => {
+        try {
+          // console.info('==> update latestMsg ', data);
+          wss && wss.broadcast({
+            type: 'updateLatestWsMsg',
+            content: data
+          });
+        } catch (e) {
+          logUtil.error(e.message);
+          logUtil.error(e.stack);
+          console.error(e);
+        }
+      });
+
       self.wss = wss;
     });
   }
diff --git a/lib/wsServerMgr.js b/lib/wsServerMgr.js
new file mode 100644
index 0000000..62e5a50
--- /dev/null
+++ b/lib/wsServerMgr.js
@@ -0,0 +1,39 @@
+/**
+* manage the websocket server
+*
+*/
+const ws = require('ws');
+const logUtil = require('./log.js');
+
+const WsServer = ws.Server;
+
+/**
+* get a new websocket server based on the server
+* @param @required {object} config
+                   {string} config.server
+                   {handler} config.handler
+*/
+function getWsServer(config) {
+  const wss = new WsServer({
+    server: config.server
+  });
+
+  wss.on('connection', config.connHandler);
+
+  wss.on('headers', (headers) => {
+    headers.push('x-anyproxy-websocket:true');
+  });
+
+  wss.on('error', e => {
+    logUtil.error(`error in websocket proxy: ${e.message},\r\n ${e.stack}`);
+    console.error('error happened in proxy websocket:', e)
+  });
+
+  wss.on('close', e => {
+    console.error('==> closing the ws server');
+  });
+
+  return wss;
+}
+
+module.exports.getWsServer = getWsServer;
diff --git a/proxy.js b/proxy.js
index bf495d5..b907c76 100644
--- a/proxy.js
+++ b/proxy.js
@@ -11,6 +11,7 @@ const http = require('http'),
   events = require('events'),
   co = require('co'),
   WebInterface = require('./lib/webInterface'),
+  wsServerMgr = require('./lib/wsServerMgr'),
   ThrottleGroup = require('stream-throttle').ThrottleGroup;
 
 // const memwatch = require('memwatch-next');
@@ -60,6 +61,7 @@ class ProxyCore extends events.EventEmitter {
    * @param {boolean} [config.silent=false] - if keep the console silent
    * @param {boolean} [config.dangerouslyIgnoreUnauthorized=false] - if ignore unauthorized server response
    * @param {object} [config.recorder] - recorder to use
+   * @param {boolean} [config.wsIntercept] - whether intercept websocket
    *
    * @memberOf ProxyCore
    */
@@ -114,6 +116,8 @@ class ProxyCore extends events.EventEmitter {
     // init request handler
     const RequestHandler = util.freshRequire('./requestHandler');
     this.requestHandler = new RequestHandler({
+      wsIntercept: config.wsIntercept,
+      httpServerPort: config.port, // the http server port for http proxy
       forceProxyHttps: !!config.forceProxyHttps,
       dangerouslyIgnoreUnauthorized: !!config.dangerouslyIgnoreUnauthorized
     }, this.proxyRule, this.recorder);
@@ -185,6 +189,10 @@ class ProxyCore extends events.EventEmitter {
         },
 
         function (callback) {
+          wsServerMgr.getWsServer({
+            server: self.httpProxyServer,
+            connHandler: self.requestHandler.wsHandler
+          });
           // remember all sockets, so we can destory them when call the method 'close';
           self.httpProxyServer.on('connection', (socket) => {
             self.handleExistConnections.call(self, socket);
@@ -324,17 +332,10 @@ class ProxyServer extends ProxyCore {
       this.webServerInstance = new WebInterface(this.proxyWebinterfaceConfig, this.recorder);
     }
 
-    new Promise((resolve) => {
-      // start web server
-      if (this.webServerInstance) {
-        resolve(this.webServerInstance.start());
-      } else {
-        resolve(null);
-      }
-    })
-    .then(() => {
+    // start web server
+    this.webServerInstance.start().then(() => {
       // start proxy core
-      super.start()
+      super.start();
     })
     .catch((e) => {
       this.emit('error', e);
diff --git a/test/server/server.js b/test/server/server.js
index f37c43f..40bd3af 100644
--- a/test/server/server.js
+++ b/test/server/server.js
@@ -305,7 +305,7 @@ KoaServer.prototype.start = function () {
         });
       });
 
-      wss.on('error', e => console.error('erro happened in wss:%s', error));
+      wss.on('error', e => console.error('error happened in wss:%s', e));
 
       self.httpsServer.listen(HTTPS_PORT);
 
diff --git a/test/spec_rule/no_rule_websocket_spec.js b/test/spec_rule/no_rule_websocket_spec.js
index 94bc999..7feae15 100644
--- a/test/spec_rule/no_rule_websocket_spec.js
+++ b/test/spec_rule/no_rule_websocket_spec.js
@@ -6,23 +6,32 @@
 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 { printLog, isArrayEqual } = require('../util/CommonUtil.js');
 
 testWebsocket('ws');
 testWebsocket('wss');
+testWebsocket('ws', true);
+testWebsocket('wss', true);
 
-function testWebsocket(protocol) {
+function testWebsocket(protocol, masked = false) {
   describe('Test WebSocket in protocol : ' + protocol, () => {
     const url = generateWsUrl(protocol, '/test/socket');
     let serverInstance;
     let proxyServer;
+    // the message to
+    const testMessageArray = [
+      'Send the message with default option1',
+      'Send the message with default option2',
+      'Send the message with default option3',
+      'Send the message with default option4'
+    ];
 
     beforeAll((done) => {
       jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000;
       printLog('Start server for no_rule_websocket_spec');
       serverInstance = new Server();
 
-      proxyServer = ProxyServerUtil.proxyServerWithoutHttpsIntercept();
+      proxyServer = ProxyServerUtil.defaultProxyServer();
 
       setTimeout(() => {
         done();
@@ -36,32 +45,57 @@ function testWebsocket(protocol) {
     });
 
     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 directMessages = []; // set the flag for direct message, compare when both direct and proxy got message
+      const proxyMessages = [];
+      let directHeaders;
+      let proxyHeaders;
 
       const ws = directWs(url);
-      const porxyWsRef = proxyWs(url);
+      const proxyWsRef = proxyWs(url);
       ws.on('open', () => {
-        ws.send(sendMessage);
+        ws.send(testMessageArray[0], masked);
+        for (let i = 1; i < testMessageArray.length; i++) {
+          setTimeout(() => {
+            ws.send(testMessageArray[i], masked);
+          }, 1000);
+        }
       });
 
-      porxyWsRef.on('open', () => {
-        porxyWsRef.send(sendMessage);
+      proxyWsRef.on('open', () => {
+        try {
+          proxyWsRef.send(testMessageArray[0], masked);
+          for (let i = 1; i < testMessageArray.length; i++) {
+            setTimeout(() => {
+              proxyWsRef.send(testMessageArray[i], masked);
+            }, 1000);
+          }
+        } catch (e) {
+          console.error(e);
+        }
+      });
+
+      ws.on('headers', (headers) => {
+        directHeaders = headers;
+        compareMessageIfReady();
+      });
+
+      proxyWsRef.on('headers', (headers) => {
+        proxyHeaders = headers;
+        compareMessageIfReady();
       });
 
       ws.on('message', (data, flag) => {
         const message = JSON.parse(data);
         if (message.type === 'onMessage') {
-          directMessage = message.content;
+          directMessages.push(message.content);
           compareMessageIfReady();
         }
       });
 
-      porxyWsRef.on('message', (data, flag) => {
+      proxyWsRef.on('message', (data, flag) => {
         const message = JSON.parse(data);
         if (message.type === 'onMessage') {
-          proxyMessage = message.content;
+          proxyMessages.push(message.content);
           compareMessageIfReady();
         }
       });
@@ -71,70 +105,24 @@ function testWebsocket(protocol) {
         done.fail('Error happened in direct websocket');
       });
 
-      porxyWsRef.on('error', error => {
+      proxyWsRef.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);
+        const targetLen = testMessageArray.length;
+        if (directMessages.length === targetLen
+          && proxyMessages.length === targetLen
+          && directHeaders && proxyHeaders
+        ) {
+          expect(isArrayEqual(directMessages, testMessageArray)).toBe(true);
+          expect(isArrayEqual(directMessages, proxyMessages)).toBe(true);
+          expect(directHeaders['x-anyproxy-websocket']).toBeUndefined();
+          expect(proxyHeaders['x-anyproxy-websocket']).toBe('true');
           done();
         }
       }
     });
   });
 }
-
diff --git a/test/util/CommonUtil.js b/test/util/CommonUtil.js
index 75a3751..89822db 100644
--- a/test/util/CommonUtil.js
+++ b/test/util/CommonUtil.js
@@ -271,5 +271,6 @@ module.exports = {
   printHilite,
   isCommonReqEqual,
   parseUrlQuery,
-  stringSimilarity
+  stringSimilarity,
+  isArrayEqual: _isDeepEqual
 };
diff --git a/test/util/ProxyServerUtil.js b/test/util/ProxyServerUtil.js
index e3ce885..1974040 100644
--- a/test/util/ProxyServerUtil.js
+++ b/test/util/ProxyServerUtil.js
@@ -13,6 +13,7 @@ const DEFAULT_OPTIONS = {
     webPort: 8002,  // optional, port for web interface
     wsPort: 8003,  // optional, internal port for web socket
   },
+  wsIntercept: true,
   throttle: 10000,    // optional, speed limit in kb/s
   forceProxyHttps: true, // intercept https as well
   dangerouslyIgnoreUnauthorized: true,
diff --git a/web/src/common/WsUtil.js b/web/src/common/WsUtil.js
index 1aeec99..95835ba 100644
--- a/web/src/common/WsUtil.js
+++ b/web/src/common/WsUtil.js
@@ -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
 };
-
diff --git a/web/src/common/commonUtil.js b/web/src/common/commonUtil.js
index 812a5eb..5a3f360 100644
--- a/web/src/common/commonUtil.js
+++ b/web/src/common/commonUtil.js
@@ -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());
         }
     });
 }
diff --git a/web/src/component/record-detail.jsx b/web/src/component/record-detail.jsx
index 698a336..a3c4c9a 100644
--- a/web/src/component/record-detail.jsx
+++ b/web/src/component/record-detail.jsx
@@ -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
diff --git a/web/src/component/record-detail.less b/web/src/component/record-detail.less
index b377933..2677f4c 100644
--- a/web/src/component/record-detail.less
+++ b/web/src/component/record-detail.less
@@ -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;
 }
 
diff --git a/web/src/component/record-response-detail.jsx b/web/src/component/record-response-detail.jsx
index 55330cd..4f57391 100644
--- a/web/src/component/record-response-detail.jsx
+++ b/web/src/component/record-response-detail.jsx
@@ -31,7 +31,7 @@ class RecordResponseDetail extends React.Component {
   }
 
   static propTypes = {
-    requestRecord: PropTypes.object
+    recordDetail: PropTypes.object
   }
 
   onSelectText(e) {
diff --git a/web/src/component/record-ws-message-detail.jsx b/web/src/component/record-ws-message-detail.jsx
new file mode 100644
index 0000000..1c0decb
--- /dev/null
+++ b/web/src/component/record-ws-message-detail.jsx
@@ -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;
diff --git a/web/src/component/record-ws-message-detail.less b/web/src/component/record-ws-message-detail.less
new file mode 100644
index 0000000..13084ae
--- /dev/null
+++ b/web/src/component/record-ws-message-detail.less
@@ -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;
+}
diff --git a/web/src/saga/rootSaga.js b/web/src/saga/rootSaga.js
index bf97b96..c4bd376 100644
--- a/web/src/saga/rootSaga.js
+++ b/web/src/saga/rootSaga.js
@@ -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));
diff --git a/web/src/style/common.less b/web/src/style/common.less
index 7129495..2cf688f 100644
--- a/web/src/style/common.less
+++ b/web/src/style/common.less
@@ -54,13 +54,13 @@ body {
 }
 
 :global {
-  .ant-btn {
-    min-width: 100px;
-  }
+  // .ant-btn {
+  //   min-width: 100px;
+  // }
 }
 
 .relativeWrapper {
   position: relative;
   width: 100%;
   height: 100%;
-}
\ No newline at end of file
+}

From c018c775c80f9151f5dc08ddf7e89800fbd946b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A0=9A=E7=84=B6?= <yanran.wwj@alipay.com>
Date: Tue, 12 Dec 2017 21:20:56 +0800
Subject: [PATCH 2/3] update the doc about websocket proxy

---
 docs-src/cn/src_doc.md |  2 ++
 docs-src/en/src_doc.md | 10 ++++++----
 lib/requestHandler.js  |  4 ++--
 3 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/docs-src/cn/src_doc.md b/docs-src/cn/src_doc.md
index 200ada4..625a9ad 100644
--- a/docs-src/cn/src_doc.md
+++ b/docs-src/cn/src_doc.md
@@ -84,6 +84,7 @@ const options = {
   },
   throttle: 10000,
   forceProxyHttps: false,
+  wsIntercept: false, // 不开启websocket代理
   silent: false
 };
 const proxyServer = new AnyProxy.ProxyServer(options);
@@ -110,6 +111,7 @@ proxyServer.close();
     * `forceProxyHttps` {boolean} 是否强制拦截所有的https,忽略规则模块的返回,默认`false`
     * `silent` {boolean} 是否屏蔽所有console输出,默认`false`
     * `dangerouslyIgnoreUnauthorized` {boolean} 是否忽略请求中的证书错误,默认`false`
+    * `wsIntercept` {boolean} 是否开启websocket代理,默认`false`
     * `webInterface` {object} web版界面配置
       * `enable` {boolean} 是否启用web版界面,默认`false`
       * `webPort` {number} web版界面端口号,默认`8002`
diff --git a/docs-src/en/src_doc.md b/docs-src/en/src_doc.md
index 94352eb..8877806 100644
--- a/docs-src/en/src_doc.md
+++ b/docs-src/en/src_doc.md
@@ -83,6 +83,7 @@ const options = {
   },
   throttle: 10000,
   forceProxyHttps: false,
+  wsIntercept: false,
   silent: false
 };
 const proxyServer = new AnyProxy.ProxyServer(options);
@@ -106,11 +107,12 @@ proxyServer.close();
     * `port` {number} required, port number of proxy server
     * `rule` {object} your rule module
     * `throttle` {number} throttle in kb/s, unlimited for default
-    * `forceProxyHttps` {boolean} in force intercept all https request, false for default
-    * `silent` {boolean} if keep silent in console, false for default`false`
-    * `dangerouslyIgnoreUnauthorized` {boolean} if ignore certificate error in request, false for default
+    * `forceProxyHttps` {boolean} in force intercept all https request, default to `false`
+    * `silent` {boolean} if keep silent in console, false for default `false`
+    * `dangerouslyIgnoreUnauthorized` {boolean} if ignore certificate error in request, default to `false`
+    * `wsIntercept` {boolean} whether to intercept websocket, default to `false`
     * `webInterface` {object} config for web interface
-      * `enable` {boolean} if enable web interface, false for default
+      * `enable` {boolean} if enable web interface, default to `false`
       * `webPort` {number} port number for web interface
   * Event: `ready`
     * emit when proxy server is ready
diff --git a/lib/requestHandler.js b/lib/requestHandler.js
index 7cbf1e3..d6b9e83 100644
--- a/lib/requestHandler.js
+++ b/lib/requestHandler.js
@@ -570,7 +570,7 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
       // determine the request target
       if (!shouldIntercept) {
         // server info from the original request
-        const orginServer = {
+        const originServer = {
           host,
           port: (targetPort === 80) ? 443 : targetPort
         }
@@ -581,7 +581,7 @@ function getConnectReqHandler(userRule, recorder, httpsServerMgr) {
         }
 
         // for ws request, redirect them to local ws server
-        return interceptWsRequest ? localHttpServer : orginServer;
+        return interceptWsRequest ? localHttpServer : originServer;
       } else {
         return httpsServerMgr.getSharedHttpsServer(host).then(serverInfo => ({ host: serverInfo.host, port: serverInfo.port }));
       }

From c2760e0bb084f752f3e37442dfd77631aa72dd8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A0=9A=E7=84=B6?= <yanran.wwj@alipay.com>
Date: Sun, 28 Jan 2018 12:43:42 +0800
Subject: [PATCH 3/3] include fast-json to enhance the stringify of ws message

---
 lib/recorder.js       | 20 +++++++++++++++++++-
 lib/requestHandler.js |  2 +-
 package.json          |  1 +
 3 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/lib/recorder.js b/lib/recorder.js
index 94b2a83..1a37739 100644
--- a/lib/recorder.js
+++ b/lib/recorder.js
@@ -7,8 +7,25 @@ const Datastore = require('nedb'),
   logUtil = require('./log'),
   events = require('events'),
   iconv = require('iconv-lite'),
+  fastJson = require('fast-json-stringify'),
   proxyUtil = require('./util');
 
+const wsMessageStingify = fastJson({
+  title: 'ws message stringify',
+  type: 'object',
+  properties: {
+    time: {
+      type: 'integer'
+    },
+    message: {
+      type: 'string'
+    },
+    isToServer: {
+      type: 'boolean'
+    }
+  }
+});
+
 const BODY_FILE_PRFIX = 'res_body_';
 const WS_MESSAGE_FILE_PRFIX = 'ws_message_';
 const CACHE_DIR_PREFIX = 'cache_r';
@@ -113,7 +130,8 @@ class Recorder extends events.EventEmitter {
     if (id < 0) return;
     try {
       const recordWsMessageFile = path.join(cachePath, WS_MESSAGE_FILE_PRFIX + id);
-      fs.appendFile(recordWsMessageFile, JSON.stringify(message) + ',', () => {});
+
+      fs.appendFile(recordWsMessageFile, wsMessageStingify(message) + ',', () => {});
     } catch (e) {
       console.error(e);
       logUtil.error(e.message + e.stack);
diff --git a/lib/requestHandler.js b/lib/requestHandler.js
index 2733d40..9757fd6 100644
--- a/lib/requestHandler.js
+++ b/lib/requestHandler.js
@@ -744,7 +744,7 @@ function getWsHandler(userRule, recorder, wsClient) {
     /**
     * consruct a message Record from message event
     * @param @required {event} messageEvent the event from websockt.onmessage
-    * @param @required {boolean} isIn whether the message is to or from server
+    * @param @required {boolean} isToServer whether the message is to or from server
     *
     */
     const recordMessage = (messageEvent, isToServer) => {
diff --git a/package.json b/package.json
index 0e089ca..3eb4250 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
     "compression": "^1.4.4",
     "es6-promise": "^3.3.1",
     "express": "^4.8.5",
+    "fast-json-stringify": "^0.17.0",
     "iconv-lite": "^0.4.6",
     "inquirer": "^3.0.1",
     "ip": "^0.3.2",