# 研(kàn)究(kàn) 👨‍💻vscode 中的 websocket

# 前言

因项目要求,要对 vscodewebsocket 传输的数据进行加密。由于对 tsnode 不够熟悉,在撞破了几个脑袋之后,记录下自己的一些理解。

vscode 是基于 typescriptnode (桌面版:electron),在看下面的内容前,建议提前了解一下 Buffer、ArrayBuffer、TypedArray (opens new window) 🚀 以及位运算等,会更加容易理解

# 打包流程

  • 环境选择:Ubuntu、debian
  • 进入项目根目录 code-sever >
  • 执行 yarn 安装依赖
  • 打包 yarn build 1.39.2 { codeservername } 名字随意取,这一步将生成 /build/code-server{ codeservername }-vsc1.39.2-linux-x86_64-built (比较花时间)
  • 执行 node /path/to/output/build/out/vs/server/main.js 这一步是跑 demo,但注意 window 下跑不了的
  • 执行 yarn binary 1.39.2 { codeservername } 打包二进制文件
  • 执行打包好的二进制文件即可访问

# 关键代码解析

# browserSocketFactory.ts

路径:browserSocketFactory.ts (opens new window) 🚀 -- BrowserWebSocket 下的 send 函数
功能:vscode 客户端发送数据的出入口
分析:

class BrowserWebSocket extends Disposable implements IWebSocket {
  // 这里new一个事件触发器,包含fire方法
  // The Emitter can be used to expose an Event to the public to fire it from the insides.
  // 引用自 event.ts 的 Emitter 类
  // 这里主要是注册事件,并存在 this._store 中 this_store 是 DisposableStore 对象数据
  private readonly _onData = new Emitter<ArrayBuffer>()
  public readonly onData = this._onData.event

  public readonly onOpen: Event<void>

  private readonly _onClose = this._register(new Emitter<void>())
  public readonly onClose = this._onClose.event

  private readonly _onError = this._register(new Emitter<any>())
  public readonly onError = this._onError.event

  private readonly _socket: WebSocket
  private readonly _fileReader: FileReader
  private readonly _queue: Blob[]
  private _isReading: boolean
  private _isClosed: boolean

  private readonly _socketMessageListener: (ev: MessageEvent) => void

  constructor(socket: WebSocket) {
    super()
    this._socket = socket
    this._fileReader = new FileReader()
    this._queue = []
    this._isReading = false
    this._isClosed = false

    this._fileReader.onload = event => {
      this._isReading = false
      const buff = <ArrayBuffer>(<any>event.target).result
      this._onData.fire(buff)
      // 当 queue 还有数据的时候,继续执行
      if (this._queue.length > 0) {
        enqueue(this._queue.shift()!)
      }
    }

    const enqueue = (blob: Blob) => {
      // 这里做了个缓冲处理,当正在读取数据时,将 Blob 缓存在 queue 中
      if (this._isReading) {
        this._queue.push(blob)
        return
      }
      this._isReading = true
      // 这里将收到的 Blob 数据转换成 ArrayBuffer
      this._fileReader.readAsArrayBuffer(blob)
    }

    this._socketMessageListener = (ev: MessageEvent) => {
      enqueue(<Blob>ev.data)
    }
    // websocket 的 message 事件,接收服务端传回来的数据
    this._socket.addEventListener("message", this._socketMessageListener)

    this.onOpen = Event.fromDOMEventEmitter(this._socket, "open")

    let pendingErrorEvent: any | null = null

    const sendPendingErrorNow = () => {
      const err = pendingErrorEvent
      pendingErrorEvent = null
      this._onError.fire(err)
    }

    const errorRunner = this._register(new RunOnceScheduler(sendPendingErrorNow, 0))

    const sendErrorSoon = (err: any) => {
      errorRunner.cancel()
      pendingErrorEvent = err
      errorRunner.schedule()
    }

    const sendErrorNow = (err: any) => {
      errorRunner.cancel()
      pendingErrorEvent = err
      sendPendingErrorNow()
    }

    this._register(
      dom.addDisposableListener(this._socket, "close", (e: CloseEvent) => {
        this._isClosed = true

        if (pendingErrorEvent) {
          if (!window.navigator.onLine) {
            // The browser is offline => this is a temporary error which might resolve itself
            sendErrorNow(
              new RemoteAuthorityResolverError(
                "Browser is offline",
                RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable,
                e
              )
            )
          } else {
            // An error event is pending
            // The browser appears to be online...
            if (!e.wasClean) {
              // Let's be optimistic and hope that perhaps the server could not be reached or something
              sendErrorNow(
                new RemoteAuthorityResolverError(
                  e.reason || `WebSocket close with status code ${e.code}`,
                  RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable,
                  e
                )
              )
            } else {
              // this was a clean close => send existing error
              errorRunner.cancel()
              sendPendingErrorNow()
            }
          }
        }

        this._onClose.fire()
      })
    )

    this._register(dom.addDisposableListener(this._socket, "error", sendErrorSoon))
  }

  send(data: ArrayBuffer | ArrayBufferView): void {
    if (this._isClosed) {
      // Refuse to write data to closed WebSocket...
      return
    }
    this._socket.send(data)
  }

  close(): void {
    this._isClosed = true
    this._socket.close()
    this._socket.removeEventListener("message", this._socketMessageListener)
    this.dispose()
  }
}

# ipc.net.ts

路径:ipc.net.ts (opens new window) 🚀 -- WebSocketNodeSocket 下的 _acceptChunk 函数(this.socket.onData(data => this._acceptChunk(data))
功能:初步确认是服务端接收 ws 传输的数据之后的处理函数
分析:

private _acceptChunk(data: VSBuffer): void {
 if (data.byteLength === 0) {
  return;
 }
 this._incomingData.acceptChunk(data);
 // 定义的 chunkNum 类型数据,以 VSBuffer[] 形式存储所有接收到的数据
 while (this._incomingData.byteLength >= this._state.readLen) {
  // 只要 _incomingData 的字节长度大于最小头部长度(即2)时,遍历读数据
  // 下面的判断条件是为了限制读取顺序:确认头部信息-> 提取头部信息 -> 提取data数据
  if (this._state.state === ReadState.PeekHeader) {
   // chunkNum 类型方法,peek 是读取数据的方法,开始读取最小头部长度的数据(读2位数据)
   const peekHeader = this._incomingData.peek(this._state.readLen);
   // secondByte 为第二字节数据,readUInt8 是指读取第 offset 位的无符号的8位整数值
   // 可以参考nodejs api中文文档:http://nodejs.cn/api/buffer.html
   const secondByte = peekHeader.readUInt8(1);
   // 将第二字节数据(最大为255)与 0b10000000 做 ‘与’ 的操作,然后右移七位得到一个 ‘标志数字’
   // 这个标志数字为mask,也就是 websocket 中的掩码,主要针对安全方面的优化,避免被中间设备攻击
   // 这一项仅可以在客户端设置,如果在服务端设置了会报错:只有客户端发送的数据才需要掩码处理
   const hasMask = (secondByte & 0b10000000) >>> 7;
   const len = (secondByte & 0b01111111);
   // 手动调整 _state 的数据,使进入下一个逻辑判断
   this._state.state = ReadState.ReadHeader;
   // 计算下一步读取数据的长度
   this._state.readLen = Constants.MinHeaderByteSize + (hasMask ? 4 : 0) + (len === 126 ? 2 : 0) + (len === 127 ? 8 : 0);
   this._state.mask = 0;
  } else if (this._state.state === ReadState.ReadHeader) {
   // read entire header
   // read 方法会修改源数据:取出 chunks(VSBuffer[]) 中的第一项数据 - shift()
   const header = this._incomingData.read(this._state.readLen);
   const secondByte = header.readUInt8(1);
   const hasMask = (secondByte & 0b10000000) >>> 7;
   let len = (secondByte & 0b01111111);

   let offset = 1;
   // 2 ** 8 是计算2的n次方
   if (len === 126) {
    // 读取header第二字节的数字,然后再 * 2 ** 8 + 第三字节的数字,计算出长度len
    len = (
     header.readUInt8(++offset) * 2 ** 8
     + header.readUInt8(++offset)
    );
   } else if (len === 127) {
    // 同上
    len = (
     header.readUInt8(++offset) * 0
     + header.readUInt8(++offset) * 0
     + header.readUInt8(++offset) * 0
     + header.readUInt8(++offset) * 0
     + header.readUInt8(++offset) * 2 ** 24
     + header.readUInt8(++offset) * 2 ** 16
     + header.readUInt8(++offset) * 2 ** 8
     + header.readUInt8(++offset)
    );
   }
   let mask = 0;
   if (hasMask) {
    // 计算完len之后,offset递增,再计算mask
    mask = (
     header.readUInt8(++offset) * 2 ** 24
     + header.readUInt8(++offset) * 2 ** 16
     + header.readUInt8(++offset) * 2 ** 8
     + header.readUInt8(++offset)
    );
   }
   // 这里同样设置一个定值,用于下一步的逻辑判断,使之进入下一阶段处理
   this._state.state = ReadState.ReadBody;
   // 存储下一步读取数据的长度
   this._state.readLen = len;
   this._state.mask = mask;
  } else if (this._state.state === ReadState.ReadBody) {
   // 同样是 shift() 数组中的第一项存储在 body
   // 到这里的 VSBuffer 还不是最终的可以转换的数据
   const body = this._incomingData.read(this._state.readLen);
   // 这里执行 unmask方法,最终的数据在这个方法体可以拿到
   unmask(body, this._state.mask);
   // 恢复 _state 为开始的数据,重新开始新一轮数据的处理
   this._state.state = ReadState.PeekHeader;
   this._state.readLen = Constants.MinHeaderByteSize;
   this._state.mask = 0;
   // TODO: fire的作用?类似触发所有关联事件?
   this._onData.fire(body);
  }
 }
}

// 位置:WebSocketNodeSocket 类
private readonly _state = {
 state: ReadState.PeekHeader, // eNum类型数据
 readLen: Constants.MinHeaderByteSize, // 最小头部长度
 mask: 0 // 标志位
};

// ChunkStream class
// 位置: https://github.com/microsoft/vscode/tree/master/src/vs/base/parts/ipc/common/ipc.net.ts
export class ChunkStream {
 // 相当于 Buffer 数组
 private _chunks: VSBuffer[]
 // 字节长度
 private _totalLength: number

 public get byteLength() {
  // 获取字节长度
  return this._totalLength
 }

 constructor() {
  this._chunks = []
  this._totalLength = 0
 }

 public acceptChunk(buff: VSBuffer) {
  // 缓冲接收到的 buffer,存在数组里 _chunks
  this._chunks.push(buff)
  this._totalLength += buff.byteLength
 }

 public read(byteCount: number): VSBuffer {
  // 读取并截取第一项数据,会改变源 Buffer
  return this._read(byteCount, true)
 }

 public peek(byteCount: number): VSBuffer {
  // 仅读取第一项数据,不改变源 Buffer
  return this._read(byteCount, false)
 }

 private _read(byteCount: number, advance: boolean): VSBuffer {
  // 实际执行读取操作的函数,根据字节长度区分不同处理方法
  // 先处理边界情况
  if (byteCount === 0) {
   return getEmptyBuffer()
  }

  if (byteCount > this._totalLength) {
   // 将读取的长度超过总长,会报错
   throw new Error(`Cannot read so many bytes!`)
  }

  if (this._chunks[0].byteLength === byteCount) {
   // super fast path, precisely first chunk must be returned
   // 取第一项数据
   const result = this._chunks[0]
   if (advance) {
    // shift 掉第一项数据
    this._chunks.shift()
    // 同时修改总长
    this._totalLength -= byteCount
   }
   return result
  }

  if (this._chunks[0].byteLength > byteCount) {
   // fast path, the reading is entirely within the first chunk
   // 如果第一项数据长度超过要读取的字节长度,那么只需要读 byteCount 长度的数据
   const result = this._chunks[0].slice(0, byteCount)
   if (advance) {
    this._chunks[0] = this._chunks[0].slice(byteCount)
    this._totalLength -= byteCount
   }
   return result
  }

  // VSBuffer.alloc: 创建 byteCount 长的 Buffer 数据,并初始化每一项为0
  // TODO: 什么情况下会执行下面代码?
  let result = VSBuffer.alloc(byteCount)
  let resultOffset = 0
  let chunkIndex = 0
  while (byteCount > 0) {
   // 依旧是取第一项数据
   const chunk = this._chunks[chunkIndex]
   if (chunk.byteLength > byteCount) {
    // this chunk will survive
    const chunkPart = chunk.slice(0, byteCount)
    // result这里调用set方法,实际是调用 Uint8Array 的 set 方法。在 VSBuffer 内还是用的 Uint8Array 类型数据
    // 参考:https://github.com/microsoft/vscode/tree/master/src/vs/base/common/buffer.ts 中的 Buffer 类
    result.set(chunkPart, resultOffset)
    resultOffset += byteCount

    if (advance) {
     // 这里进行一次裁剪,超过byteCount长度的都被裁掉
     this._chunks[chunkIndex] = chunk.slice(byteCount)
     this._totalLength -= byteCount
    }

    byteCount -= byteCount
   } else {
    // 这里的处理 同上
    // this chunk will be entirely read
    result.set(chunk, resultOffset)
    resultOffset += chunk.byteLength

    if (advance) {
     this._chunks.shift()
     this._totalLength -= chunk.byteLength
    } else {
     chunkIndex++
    }

    byteCount -= chunk.byteLength
   }
  }
  // 注意这里返回的就是 VSBuffer 格式数据,可以直接调用 toString 方法 decode
  return result
 }
}

路径:ipc.net.ts (opens new window) 🚀 -- WebSocketNodeSocket 下的 write 函数
功能:初步确认是服务端发送 ws 的函数
分析:

// class WebSocketNodeSocket
public write(buffer: VSBuffer): void {
 // 初始化 headerLen 为最小头部长度
 let headerLen = Constants.MinHeaderByteSize;
 // 根据字节长度,确定 headerLen 的大小
 if (buffer.byteLength < 126) {
  headerLen += 0;
 } else if (buffer.byteLength < 2 ** 16) {
  headerLen += 2;
 } else {
  headerLen += 8;
 }
 // 创建 headerLen 长度的字节序列
 const header = VSBuffer.alloc(headerLen);
 // 第一位无符号整数修改为 0b10000010
 header.writeUInt8(0b10000010, 0);
 if (buffer.byteLength < 126) {
  header.writeUInt8(buffer.byteLength, 1);
 } else if (buffer.byteLength < 2 ** 16) {
  header.writeUInt8(126, 1);
  let offset = 1;
  header.writeUInt8((buffer.byteLength >>> 8) & 0b11111111, ++offset);
  header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
 } else {
  header.writeUInt8(127, 1);
  let offset = 1;
  header.writeUInt8(0, ++offset);
  header.writeUInt8(0, ++offset);
  header.writeUInt8(0, ++offset);
  header.writeUInt8(0, ++offset);
  header.writeUInt8((buffer.byteLength >>> 24) & 0b11111111, ++offset);
  header.writeUInt8((buffer.byteLength >>> 16) & 0b11111111, ++offset);
  header.writeUInt8((buffer.byteLength >>> 8) & 0b11111111, ++offset);
  header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
 }
 // 这里返回的数据时 VSBuffer,但实际上客户端接收到的是 MessageEvent 格式数据,包含其他如:ws来源信息等
 // 数据都存在 e.data 内,并且 MessageEvent 的格式为 Blob 数据
 this.socket.write(VSBuffer.concat([header, buffer]));
}

# 踩过的无底洞 🕳

  • 修改的源码要确保不能出现错误如: tslint 提示语法错误的代码等
  • 打包的 patch 文件一定要注意,编码是 UTF-8,且换行符的格式是:LF(默认是CRLF
  • 如果遇到 patch 失败问题,可以先试试清除服务器的缓存(重新 clone 项目代码),然后再重新执行 yarn build
  • 注意 vscode 内部同时有 window 环境和 node 环境,要注意使用属性的兼容性,避免报错
  • 运行过程中有时会一直重连,然后突然崩溃就一直连不上,具体什么原因不清楚
  • trailing whitespace 报错是指,代码最后一个字符必须以 ';' 结尾,否则会报错

# 猜测

  • code-server 基本依赖都是在 vscode 上,在浏览器控制台的 source 板块是搜不到任何 code-server 目录下的相关代码,只有存在于 vscode 的才能搜到
  • vscodewebsocket 发送数据方法在 browserSocketFactory.tssend 方法,可以 console.log 打印到控制台看看。可以用 TextDecoder 去解码数据,数据的加密也可以在这里处理
  • vscodewebsocket 接收数据方法在 browserSocketFactory.ts_socketMessageListener 方法,参数 evBlob 格式内容,并且这里是最先接收到 code-server 返回的内容,可以在这里解密
  • ipc 是进程之间的交互方式,
  • 两个 websocket 中第一个主要是数据交互,第二个主要是用于心跳检测
  • buffer.ts 中定义了一个 hasBuffer 变量,用于判断是否有 Buffer 对象。(浏览器下为undefined),然后对数据的格式进行对应的改变。在node环境下用Buffer,在浏览器环境下使用TextDecoder

# 其他

  • 下面应该是 ipc 之间交互的数据报文格式
/**
 * A message has the following format:
 * ```
 *     /-------------------------------|------\
 *     |             HEADER            |      |
 *     |-------------------------------| DATA |
 *     | TYPE | ID | ACK | DATA_LENGTH |      |
 *     \-------------------------------|------/
 * ```
 * The header is 9 bytes and consists of:
 *  - TYPE is 1 byte (ProtocolMessageType) - the message type
 *  - ID is 4 bytes (u32be) - the message id (can be 0 to indicate to be ignored)
 *  - ACK is 4 bytes (u32be) - the acknowledged message id (can be 0 to indicate to be ignored)
 *  - DATA_LENGTH is 4 bytes (u32be) - the length in bytes of DATA
 *
 * Only Regular messages are counted, other messages are not counted, nor acknowledged.
 */

# 参考文档

Last Updated: 8/16/2022, 10:49:19 AM